Intenzívny rýchlokurz o základoch práce v React-e
Milan Herda / máj 2022
Časť používateľského rozhrania so svojím vlastným:
Komponenty vieme navzájom kombinovať a tak vytvárať komplexnejšie rozhrania
yarn create vite hello-react --template=react
# alebo s TypeScriptom
yarn create vite hello-react --template=react-ts
cd hello-react
yarn install
yarn dev
# PRESENTING CODE
yarn add -D prettier
# PRESENTING CODE
// .prettierrc.json
{
"trailingComma": "es5",
"semi": true,
"tabWidth": 4,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"printWidth": 100,
"singleQuote": true
}
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'node:path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@local': path.resolve(__dirname, 'src'),
},
},
});
Ak sa nejaký element použije ako párový, tak jeho "obsah" je dostupný v špeciálnej propse nazvanej children
function Page() {
return (
<div>
<MyButton>Lorem Ipsum</MyButton>
{' '}
<MyButton>
<b>Lorem</b>
{' '}
<i>Ipsum</i>
</MyButton>
</div>
);
};
function MyButton({ children }) {
return <button>{children}</button>;
};
Ak nejakému "HTML" elementu potrebujeme zadať CSS triedu, tak používame className
Dôvod: class je kľúčové slovo v JS
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{ className: 'greeting' },
'Hello, world!'
);
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
# PRESENTING CODE
function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Hello Vite + React!</p>
<p>
<button type="button" onClick={() => setCount((count) => count + 1)}>
count is: {count}
</button>
</p>
</header>
</div>
);
}
# PRESENTING CODE
function Foo() {
return (
<React.Fragment>
<header>
prvý element
</header>
<div>
druhý element
</div>
</React.Fragment>
)
};
function Bar() {
return (
<>
<header>
prvý element
</header>
<div>
druhý element
</div>
</>
);
};
# PRESENTING CODE
{condition && <Hello />}
{condition ? <Hello /> : null}
function Homepage() {
return (
<>
<h1>Homepage</h1>
<div>
Lorem Ipsum...
</div>
</>
);
};
export default Homepage;
# PRESENTING CODE
import Homepage from '@local/pages/Homepage';
function App() {
return (
<div>
<header>
<nav>B2C</nav>
</header>
<Homepage />
</div>
);
};
export default App;
# PRESENTING CODE
React umožňuje nastaviť vlastné event handlery na všetky interaktívne udalosti podporované prehliadačom (kliknutie, hover, odoslanie formuláru, zmena inputu, skrolovanie...)
function handleClick() {
alert('klikol som');
};
// ...
<button onClick={handleClick}>Klikni na mňa</button>
# PRESENTING CODE
<button onClick={handleClick()}>
<button onClick={handleClick}>
<button
onClick={() => {
alert('klikol som');
}}
>
Klikni na mňa
</button>
function Link({ to, children }) {
return <a href={to}>{children}</a>;
};
export default Link;
# PRESENTING CODE
import Link from '@local/components/router/Link';
// ...
<header>
<nav>
<Link to="/listing">
Listing
</Link>
</nav>
</header>
# PRESENTING CODE
function Link({ to, children }) => {
function handleClick(event) {
event.preventDefault();
window.history.pushState({}, '', to);
};
return <a href={to} onClick={handleClick}>{children}</a>;
};
export default Link;
# PRESENTING CODE
Vraveli sme, že komponenty majú vlastnú:
Ako teda funkcia môže mať svoj vlastný stav, ktorý sa uchováva aj mimo času jej behu?
* zjednodušene povedané
Stav sa môže rozbiť, ak...
Reactovský mechanizmus, ktorým funkčné komponenty majú prístup k stavu
Prístup k stavu zároveň umožňuje riešiť side effecty a nahradzovať lifecycle metódy
Hook je obyčajná funkcia, ktorú môžeme volať v komponente
import { useState } from 'react';
const initialState = 0;
function CounterButton() {
const [state, setState] = useState(initialState);
function clickHandler() {
setState(state + 1);
};
function clickHandler2() {
setState((prevState) => prevState + 1);
};
return (
<button onClick={clickHandler}>{state}</button>
);
}
export default CounterButton;
const [state, setState] = useState(initialState);
const [state, setState] = useState(initialState);
Funkcia setState umožňuje dva spôsoby nastavenia nového stavu:
import { useState } from 'react';
const initialState = 0;
function CounterButton() {
const [state, setState] = useState(initialState);
function clickHandler() {
setState(state + 1);
setState(state + 1);
};
function clickHandler2() {
setState((prevState) => prevState + 1);
setState((prevState) => prevState + 1);
};
return (
<button onClick={clickHandler}>{state}</button>
);
}
import { useState } from 'react';
function App({ cvList }) {
const [title, setTitle] = useState('My App');
function inputChangeHandler(event) {
setTitle(event.target.value);
};
return (
<div>
<header>
<nav>
<Link to="/">Homepage</Link>
<Link to="/listing">Listing</Link>
</nav>
</header>
<input onChange={inputChangeHandler} />
<Homepage />
</div>
);
}
# PRESENTING CODE
import { useState } from 'react';
function App({ cvList }) {
const [title, setTitle] = useState('My App');
const inputChangeHandler = (event) => {
setTitle(event.target.value);
};
return (
<div>
<header>
<nav>
<Link to="/">Homepage</Link>
<Link to="/listing">Listing</Link>
</nav>
</header>
<input onChange={inputChangeHandler} value={title} />
<Homepage />
</div>
);
}
# PRESENTING CODE
Ak React kontroluje hodnotu formulárových komponent, tak sa im hovorí "controlled components"
Tento hook slúži na vykonávanie side effectov v reakcii na zmenu v daných závislostiach.
Vieme tak reagovať na:
useEffect(
() => {
/* funkcia vykonávajúca side effect */
},
[/* závislosti */]
);
Funkcia sa vykoná pri prvom volaní useEffect a potom vždy, keď sa zmení nejaká závislosť.
useEffect(
() => {
/* funkcia vykonávajúca side effect */
return () => { /* cleanup funkcia */};
},
[/* závislosti */]
);
Ak funkcia vráti funkciu, tak React predpokladá, že je v nej nejaký upratovací kód a zavolá ju pred každým ďalším volaním tohto useEffect a aj pred zrušením komponentu.
import { useEffect } from 'react';
// ...
function Detail({ title }) {
useEffect(() => {
document.querySelector('title').innerText = title;
}, [title])
return <h2>{title}</h2>;
}
# PRESENTING CODE
import Detail from '@local/pages/Detail';
// ...
function App() {
// ...
return (
<div>
{/* ... */}
<Detail title={'ahoj'} />
</div>
);
}
# PRESENTING CODE
import { useEffect, useState } from 'react';
function Listing() {
const [cvs, setCvs] = useState([]);
useEffect(() => {
fetch('/data/data.json', {
method: 'POST',
})
.then((response) => {
return response.json();
})
.then((data) => {
setCvs(data?.cvList ?? []);
});
}, []);
return (
<>
<h1>Listing</h1>
</>
);
};
export default Listing;
# PRESENTING CODE
return (
<>
<h1>Listing</h1>
<ul>
{cvs.map((cv) => (
<li key={cv.id}>{cv.title}</li>
))}
</ul>
</>
);
# PRESENTING CODE
import { useState } from 'react';
function Homepage({ cvList, changePage }) {
const [isTimerVisible, setIsTimerVisible] = useState(false);
return (
<>
<h1>Homepage</h1>
<button onClick={() => setIsTimerVisible((prevState) => !prevState)}>
Toggle Timer
</button>
{isTimerVisible ? <Timer /> : null}
{/* ... */}
</>
);
};
# PRESENTING CODE
import { useState, useEffect } from 'react';
function Timer() {
useEffect(() => {
const intervalId = setInterval(() => {
console.log(Date.now());
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <b>Timer</b>;
};
// ...
# PRESENTING CODE
* Okrem tých globálnych a importovaných, pretože tie sa väčšinou nemenia.
Hook určený pre prácu s komplexnejším stavom (napr. s celým objektom s viacerými property)
Hook určený pre sprístupnenie kontextu určeného na komunikáciu medzi rodičom a potomkami (aj naprieč viacerými úrovniami)
Hook, ktorý ukladá funkciu medzi vykonaniami komponentu.
Dá sa použiť ako výkonnostná optimalizácia zabraňujúca zbytočným rerendrom, ak napríklad komponent vo svojom tele vytvára funkciu a túto posiela cez propsy potomkom
KCD na blogu píše, ako to neprehnať.
Hook, ktorý ukladá hodnotu medzi vykonaniami komponentu
Dá sa použiť ako výkonnostná optimalizácia zabraňujúca zbytočnému náročnému výpočtu hodnoty medzi rerendrami.
KCD na blogu píše, ako to neprehnať.
Hook, ktorý sa dá použiť na získanie priameho prístupu k DOM reprezentácii nejakého HTML komponentu.
Alebo vytváranie stavových premenných, ktorých zmena nespôsobí re-render.
Context je mechanizmus, ako môže komponent "komunikovať" s iným komponentom na vyššej úrovni a to bez použitia propsov a bez ohľadu na to, koľko úrovní je medzi nimi.
// src/sandbox/MyContext.jsx
import { createContext } from 'react';
const defaultValue = 'foo';
const defaultValue = {
id: 'abcd-efgh',
title: 'abeceda zjedla deda',
foo: () => {},
};
const MyContext = createContext(defaultValue);
export default MyContext;
# PRESENTING CODE
import { useContext } from 'react';
import MyContext from '@local/sandbox/MyContext';
function ChildComponent() {
const value = useContext(MyContext);
return (
<div>
Hodnota z kontextu je {JSON.stringify(value)}
</div>
);
};
# PRESENTING CODE
import MyContext from '@local/sandbox/MyContext';
function ParentComponent({ children }) {
const valueForChildren = 'Lorem Ipsum';
return (
<div>
<h3>Nejaký bežný obsah</h3>
<MyContext.Provider value={valueForChildren}>
{children}
</MyContext.Provider>
</div>
);
};
# PRESENTING CODE
// ...
//
const MyContext = createContext(defaultValue);
export function MyContextProvider({ children}) {
const [id, setId] = useState(defaultValue.id);
const [title, setTitle] = useState(defaultValue.title);
const contextValue = {
id,
title,
foo: (id, title) => {
setId(id);
setTitle(title);
},
};
return (
<MyContext.Provider value={contextValue}>{children}</MyContext.Provider>
);
};
// ...
# PRESENTING CODE
import { MyContextProvider } from '@local/sandbox/MyContext';
// ...
//
return (
<MyContextProvider>
<div>
<header>
<nav>
<Link to="/">Homepage</Link>
<Link to="/listing">Listing</Link>
</nav>
</header>
<input onChange={inputChangeHandler} />
<Listing />
<Detail title={title} />
</div>
</MyContextProvider>
);
# PRESENTING CODE
import { useContext, useEffect } from 'react';
import MyContext from '@local/sandbox/MyContext';
function ChildComponent() {
const myCtx = useContext(MyContext);
return (
<div>
Hodnota v kontexte {JSON.stringify(myCtx)}{' '}
<button onClick={
() => myCtx.foo('42', 'ahoj')
}>Zmeň hodnotu</button>
</div>
);
};
function Detail({ title }) {
return (
<>
<h1>Detail {title}</h1>
<ChildComponent />
</>
);
};
# PRESENTING CODE
import { useContext } from 'react';
import Link from '@local/component/router/Link';
import AuthContext from '@local/security/AuthContext';
function Navigation() {
const authCtx = useContext(AuthContext);
return (
<nav>
<Link to="/">Homepage</Link> <Link to="/listing">Zoznam CV</Link>{' '}
{authCtx.isLoggedIn ? (
<>
<span>{authCtx.username}</span>{' '}
<button onClick={authCtx.logout}>Odhlásiť</button>
</>
) : (
<button onClick={() => authCtx.login('fero')}>Prihlásiť</button>
)}
</nav>
);
};
export default Navigation;
# PRESENTING CODE
import { createContext, useState } from 'react';
const defaultValue = {
isLoggedIn: false,
username: '',
login: () => {},
logout: () => {},
};
const AuthContext = createContext(defaultValue);
export function AuthContextProvider({ children }) {
const [isLoggedIn, setIsLoggedIn] = useState(defaultValue.isLoggedIn);
const [username, setUsername] = useState(defaultValue.username);
const contextValue = {
isLoggedIn,
username,
login: (username) => {
setIsLoggedIn(true);
setUsername(username);
},
logout: () => {
setIsLoggedIn(false);
setUsername('');
},
};
return <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>;
};
export default AuthContext;
# PRESENTING CODE
import { AuthContextProvider } from '@local/security/AuthContext';
import Navigation from '@local/components/Navigation';
// ...
return (
<AuthContextProvider>
<MyContextProvider>
<header><Navigation /></header>
<input onChange={inputChangeHandler} />
<Listing />
<Detail title={title} />
</MyContextProvider>
</AuthContextProvider>
);
# PRESENTING CODE
function App() {
const authCtx = useContext(AuthContext);
// ...
useEffect(() => {
fetch('/data/appUser.json', { method: 'POST' })
.then((response) => response.json())
.then((data) => {
if (data.id) {
authCtx.login(data.username);
}
});
}, []);
// ...
};
# PRESENTING CODE
Takto nám to nefunguje preto, lebo App dostane defaultné hodnoty z kontextu, pretože nemá nikde v úrovni nad sebou AuthContextProvider.
Presunieme AuthContextProvider ešte vyššie (tj. do main.jsx)
function App() {
// ...
// odstránime AuthContextProvider z App.jsx
return (
<MyContextProvider>
<div>
<header>
<Navigation />
</header>
<input onChange={inputChangeHandler} />
<Listing />
<Detail title={title} />
</div>
</MyContextProvider>
);
};
# PRESENTING CODE
import { AuthContextProvider } from '@local/security/AuthContext';
// ...
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<AuthContextProvider>
<App />
</AuthContextProvider>
</React.StrictMode>
);
# PRESENTING CODE
URL | Komponent |
---|---|
/ | Homepage |
/listing | Listing |
/detail | Detail |
function Link({ to, children }) {
function handleClick(event) {
event.preventDefault();
history.pushState({}, '', to);
dispatchEvent(new Event('locationChange'));
};
return (
<a href={to} onClick={handleClick}>
{children}
</a>
);
};
export default Link;
# PRESENTING CODE
addEventListener('popstate', () => {
dispatchEvent(new Event('locationChange'));
});
# PRESENTING CODE
useEffect(() => {
function listener() {
console.log(window.location.pathname);
};
addEventListener('locationChange', listener);
return () => removeEventListener('locationChange', listener);
}, []);
# PRESENTING CODE
return (
{/* ... */}
<h4>{window.location.pathname}</h4>
{/* ... */}
);
# PRESENTING CODE
Prečo sa zobrazuje stále rovnaká hodnota bez ohľadu na to, ako klikáme na linky a tlačidlá?
Lebo listener nijako Reactu neoznámi, že potrebujeme urobiť render.
const [pathname, setPathname] = useState(window.location.pathname);
useEffect(() => {
function listener() {
setPathname(window.location.pathname);
};
addEventListener('locationChange', listener);
return () => removeEventListener('locationChange', listener);
}, []);
// ...
return (
{/* ... */}
<h4>{pathname} {window.location.pathname}</h4>
{/* ... */}
);
# PRESENTING CODE
Takže teraz v každom komponente, kde chcem reagovať na zmenu URL musím:
import { useEffect, useState } from 'react';
const usePathname = () => {
const [pathname, setPathname] = useState(window.location.pathname);
useEffect(() => {
function listener() {
setPathname(window.location.pathname);
};
addEventListener('locationChange', listener);
return () => removeEventListener('locationChange', listener);
}, []);
return pathname;
};
export default usePathname;
# PRESENTING CODE
import usePathname from '@local/router/usePathname';
// ...
const pathname = usePathname();
// zmazaný useEffect
// ...
return (
{/* ... */}
<h4>{pathname} {window.location.pathname}</h4>
{/* ... */}
);
# PRESENTING CODE
Takže teraz v každom komponente, kde chcem reagovať na zmenu URL musím:
import { createContext, useEffect, useState } from 'react';
const defaultValue = {
pathname: window.location.pathname,
};
const RouterContext = createContext(defaultValue);
export const RouterContextProvider = ({ children }) => {
const [pathname, setPathname] = useState(defaultValue.pathname);
const contextValue = {
pathname,
};
useEffect(() => {
function listener() {
setPathname(window.location.pathname);
};
addEventListener('locationChange', listener);
return () => removeEventListener('locationChange', listener);
}, []);
return <RouterContext.Provider value={contextValue}>{children}</RouterContext.Provider>;
};
export default RouterContext;
# PRESENTING CODE
import { useContext } from 'react';
import RouterContext from '@local/router/RouterContext';
const usePathname = () => {
const routerCtx = useContext(RouterContext);
return routerCtx.pathname;
};
export default usePathname;
# PRESENTING CODE
import { RouterContextProvider } from '@local/router/RouterContext';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<AuthContextProvider>
<RouterContextProvider>
<App />
</RouterContextProvider>
</AuthContextProvider>
</React.StrictMode>
);
# PRESENTING CODE
import { createContext, useEffect, useState } from 'react';
const defaultValue = {
pathname: window.location.pathname,
};
const RouterContext = createContext(defaultValue);
// presunuté z main.jsx
addEventListener('popstate', () => {
dispatchEvent(new Event('locationChange'));
});
export const RouterContextProvider = ({ children }) => {
// ...
};
export default RouterContext;
# PRESENTING CODE
import usePathname from '@local/router/usePathname';
const Route = ({ path, component }) => {
const currentPath = usePathname();
if (currentPath === path) {
// lebo custom komponenty musia začínať veľkým písmenom
const Component = component;
return <Component />;
}
return null;
};
export default Route;
# PRESENTING CODE
URL | Komponent |
---|---|
/ | Homepage |
/listing | Listing |
/detail | Detail |
return (
{/* ... */}
<Route path="/" component={Homepage} />
<Route path="/listing" component={Listing} />
<Route path="/detail" component={Detail} />
{/* ... */}
);
# PRESENTING CODE
URL | Komponent |
---|---|
/ | Homepage |
/listing | Listing |
/detail | Detail |
yarn add react-router-dom@6
# PRESENTING CODE
import { BrowserRouter } from 'react-router-dom';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<AuthContextProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</AuthContextProvider>
</React.StrictMode>
);
# PRESENTING CODE
import { Routes, Route } from 'react-router-dom';
// ...
return (
<div>
<header>
<Navigation />
</header>
<h4>
{pathname} {window.location.pathname}
</h4>
<Routes>
<Route path="/" element={<Homepage />} />
<Route path="/listing" element={<Listing />} />
<Route path="/detail/:id" element={<Detail />} />
</Routes>
</div>
);
# PRESENTING CODE
import { Link } from 'react-router-dom';
const Navigation = () => {
// ...
return (
<nav>
<Link to="/">Homepage</Link>{' '}
<Link to="/listing">Zoznam CV</Link>{' '}
<Link to="/detail/cv-1">Detail</Link>
</nav>
);
};
# PRESENTING CODE
Hook, ktorý vieme použiť na automatické postavenie Routes a Route komponentov z konfigurácie
import { useRoutes } from 'react-router-dom';
const pages = [
{
path: '/',
element: <Homepage />,
},
{
path: '/detail/:id',
element: <Detail />,
},
{
path: '/listing',
element: <Listing />,
},
];
const Routes = () => {
const element = useRoutes(pages);
return element;
}
// ...
const App = () => {
return (
<div>
{/* ... */}
<Routes />
</div>
);
};
# PRESENTING CODE
Hook, ktorý vráti location objekt pre aktuálnu URL
Hook, ktorý vráti hodnoty parametrov z aktuálnej URL. Parametre sú definované v ceste pomocou dvojbodiek.
Hook, ktorý vráti parametre z query (search) časti URL, tj. za otáznikom. Vráti ich ako objekt typu URLSearchParams
Hook, ktorý vráti funkciu, pomocou ktorej vieme programátorsky robiť navigáciu na inú URL. Napr. ako "redirect", keď sa nenájdu dáta, chýbajú práva a pod.
A to je všetko
Ďakujem za pozornosť