React
Intenzívny rýchlokurz o základoch práce v React-e
Milan Herda / máj 2022
- poznáte nový JavaScript
- máte nainštalovaný Node
- máte aspoň základné skúsenosti s Vue/Angular/Svelte
Prerekvizity
- JS knižnica pre stavbu používateľských rozhraní
- deklaratívny prístup
- založená na komponentoch
Čo je React?
- hovoríme, čo sa má v danej situácii zobraziť
- nehovoríme ako
Deklaratívny prístup
Komponent
Časť používateľského rozhrania so svojím vlastným:
- stavom
- logikou
- "šablónou"
Komponenty vieme navzájom kombinovať a tak vytvárať komplexnejšie rozhrania
Založenie a spustenie projektu
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
Profesia Code Style
yarn add -D prettier
# PRESENTING CODE
// .prettierrc.json
{
"trailingComma": "es5",
"semi": true,
"tabWidth": 4,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"printWidth": 100,
"singleQuote": true
}
Zaujímavé súbory
- index.html - základná HTML kostra
- main.jsx - vstupný bod pre JS
- App.jsx - hlavný komponent
Zaujímavé súbory (Vite)
- package.json - skripty
- vite.config.js - konfigurácia
vite.config.js
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'),
},
},
});
Komponent
- funkcia
- vracia JSX element alebo null
- názov začína veľkým písmenom (a tak sa musí používať aj v JSX)
- prvý parameter sú props
JSX
JSX
- XML-like syntax namontovaná do JavaScriptu
- HTML tagy začínajú malým písmenom
- v skutočnosti to nie sú HTML tagy, ale preddefinované React komponenty
- vlastné komponenty sa píšu ako tagy a začínajú veľkým písmenom
- atribúty (=propsy) sa nepíšu s pomlčkou, ale v camelCase
JSX
- po obalení do { } je možné vo vnútri používať JS kód
- obsah { } sa automaticky escapuje (ochrana proti XSS)
- výsledok výrazu vo vnútri zátvoriek môže byť hocijaký platný JS výraz
- pokiaľ ho však chceme vykresliť, tak
- ReactNode (sem patrí aj string a null)
- alebo pole ReactNode položiek
- pokiaľ ho však chceme vykresliť, tak
JSX
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>;
};
JSX
Ak nejakému "HTML" elementu potrebujeme zadať CSS triedu, tak používame className
Dôvod: class je kľúčové slovo v JS
JSX
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
App.jsx
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
Fragment
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
- viaceré elementy na rovnakej úrovni vždy potrebujú rodiča
- ak nemajú prirodzeného, použijeme Fragment
Pro Tip
Nepoužívajte && operátor na podmienečné vykresľovanie
{condition && <Hello />}
{condition ? <Hello /> : null}
Úloha:
- nainštalujte si React Developer Tools
- Vytvorte komponent Homepage
- umiestnite ho do adresára src/pages
- bude mať nadpis("Homepage") a obsah ("Lorem Ipsum")
- importujte a použite ho v App.jsx
Úloha:
src/pages/Homepage.jsx
function Homepage() {
return (
<>
<h1>Homepage</h1>
<div>
Lorem Ipsum...
</div>
</>
);
};
export default Homepage;
# PRESENTING CODE
src/App.jsx
import Homepage from '@local/pages/Homepage';
function App() {
return (
<div>
<header>
<nav>B2C</nav>
</header>
<Homepage />
</div>
);
};
export default App;
# PRESENTING CODE
Eventy
Eventy
React umožňuje nastaviť vlastné event handlery na všetky interaktívne udalosti podporované prehliadačom (kliknutie, hover, odoslanie formuláru, zmena inputu, skrolovanie...)
- používajú sa na to propsy v natívnych elementoch nazvané podľa vzoru onNázovUdalosti
- event handlerom je funkcia, ktorú sami definujeme
- pri vykonaní eventu sa funkcia zavolá a ako prvý parameter dostane objekt typu SyntheticEvent, čo je wrapper nad všetkými typmi eventov, ktorý poskytuje jednotné API
- na zrušenie predvolenej akcie používame metódu preventDefault()
Eventy
function handleClick() {
alert('klikol som');
};
// ...
<button onClick={handleClick}>Klikni na mňa</button>
# PRESENTING CODE
Eventy
- podporované eventy: https://reactjs.org/docs/events.html#supported-events
- wrapper pre natívny event: https://reactjs.org/docs/events.html
- nová dokumentácia: https://react.dev/learn/responding-to-events
Pro Tip
Pozor na rozdiel oproti Vue:
Ak sa za definíciou handlera v propse hneď použijú funkčné zátvorky, tak sa handler okamžite vykoná.
<button onClick={handleClick()}>
<button onClick={handleClick}>
Pro Tip
Je dobrým zvykom funkcie obsluhujúce eventy nazývať s prefixom handle
Pro Tip
Môžeme používať aj anonymné funkcie
<button
onClick={() => {
alert('klikol som');
}}
>
Klikni na mňa
</button>
- Vytvorte komponent Link
- umiestnite ho do adresára src/components/router
- bude obsahovať jeden "a" element
- bude potrebovať propsu nazvanú "to", ktorú použije ako hodnotu pre a[href]
- použijeme v navigácii v App.jsx a "to" nastavíme na "/listing"
Úloha:
src/component/router/Link.jsx
function Link({ to, children }) {
return <a href={to}>{children}</a>;
};
export default Link;
# PRESENTING CODE
src/App.jsx
import Link from '@local/components/router/Link';
// ...
<header>
<nav>
<Link to="/listing">
Listing
</Link>
</nav>
</header>
# PRESENTING CODE
- odchytíme onClick na "a" element a zabránime default akcii
- ručne zmeníme URL na hodnotu v "to"
Úloha:
src/component/router/Link.jsx
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ú:
- logiku
- šablónu
- stav
- logika = kód funkcie
- šablóna = JSX v kóde funkcie
- stav = ???
- React počas rendrovania funkciu vykonáva nanovo
- funkcia po svojom vykonaní končí
- ak sa komponent nachádza v kóde viackrát, aj funkcia sa volá viackrát
Ako teda funkcia môže mať svoj vlastný stav, ktorý sa uchováva aj mimo času jej behu?
- stav musí byť uchovávaný mimo komponentovej funkcie
- ale prístup k nemu musí mať iba jedno konkrétne volanie funkcie
- nesmie dôjsť k zámene s iným komponentom alebo iným volaním toho istého komponentu
Riešenie
- stav môžeme uchovávať vo vlastnom module, ktorý poskytuje metódy pre prístup
- každý komponent je jednoznačne identifikovateľný
- identita komponentu je uchovávaná medzi volaniami
- React si "identitu" komponentu vytvára počas volania React.createElement*
* zjednodušene povedané
Dôsledok
Stav sa môže rozbiť, ak...
- sa k nemu pristúpi mimo komponent
- sa k nemu pristúpi v inom poradí
Hooks
Hooks
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
Rules of hooks:
- hook je možné používať iba v komponente alebo inom hooku
- názov hooku musí začínať na use
- hook sa nesmie zavolať vo vnútri podmienky (pozor aj na early return), cyklu, ani vo vnorenej funkcii
- poradie hookov je dôležité pre React a keď sa počas behu zmení, výsledok nie je garantovaný (to si treba pamätať pri používaní HMR)
- useState
- useEffect
- useContext
- useReducer
- useRef
- useCallback
- useMemo
Najčastejšie používané hooky
useState
useState
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;
useState
const [state, setState] = useState(initialState);
- initialState sa použije pri prvom vykonaní useState
- v premennej state je uložený aktuálny stav
- funkcia setState umožňuje stav meniť
- zavolanie setState informuje React, že komponent treba vykresliť nanovo
- názov state a setState si volíme sami
useState
const [state, setState] = useState(initialState);
Funkcia setState umožňuje dva spôsoby nastavenia nového stavu:
- príjme nový stav ako argument (využívame, ak nový stav nemá väzbu na predchádzajúci)
- ak je argument funkcia, tak tejto sa odovzdá aktuálny stav a návratová hodnota sa použije ako nový stav
useState
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>
);
}
- Pridajte do App.jsx formulárový input (<input>)
- Vytvorte stavovú premennú nazvanú "title" s prvotnou hodnotou "My App"
- Po zmene inputu (onChange) uložte jeho aktuálnu hodnotu do stavu title
Úloha:
src/App.jsx
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
- Skúste zabezpečiť, aby mal input na začiatku rovnakú hodnotu ako stavová premenná title
Malá odbočka
src/App.jsx
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"
Pro Tip
Stav sa nemení v čase zavolania setState funkcie, ale až vo chvíli, keď React robí re-render komponentu.
useEffect
useEffect
Tento hook slúži na vykonávanie side effectov v reakcii na zmenu v daných závislostiach.
Vieme tak reagovať na:
- zmenu stavu
- prvé spustenie "inštancie" komponentu (aka primontovanie)
- zrušenie (unmount) komponentu
useEffect
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
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.
Pro Tip
Ak je pole závislostí prázdne, tak sa useEffect vykoná iba pri namontovaní komponentu. To je užitočné napr. pre získavanie dát z backendu.
Pro Tip
Ak pole závislostí nie je vôbec definované (vynecháme argument), tak efekt sa spustí pri každom rendri komponentu.
- Vytvorte komponent Detail
- umiestnite ho do adresára src/pages
- bude očakávať propsu title
- bude mať nadpis "Detail {title}"
- importujte a použite ho v App.jsx
- pomocou useEffect zabezpečte, že HTML tag <title> bude nastavený podľa hodnoty propsy title a bude reflektovať prípadnú zmenu
Úloha:
src/pages/Detail.jsx
import { useEffect } from 'react';
// ...
function Detail({ title }) {
useEffect(() => {
document.querySelector('title').innerText = title;
}, [title])
return <h2>{title}</h2>;
}
# PRESENTING CODE
src/App.jsx
import Detail from '@local/pages/Detail';
// ...
function App() {
// ...
return (
<div>
{/* ... */}
<Detail title={'ahoj'} />
</div>
);
}
# PRESENTING CODE
- Vytvorte komponent Listing
- umiestnite ho do adresára src/pages
- bude mať nadpis "Zoznam"
- importujte a použite ho v App.jsx
- pomocou useEffect zabezpečte, aby okamžite po svojom namontovaní načítal pomocou ajaxu dáta z URL /data/data.json
- načítané údaje z kľúča cvList vložte do stavovej premennej cvs
Úloha:
src/pages/Listing.jsx
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
- Vypíšte zoznam životopisov pomocou ul>li
- Stačí, ak v zozname bude iba title
- Každý element vypisovaný v cykle musí mať unikátnu hodnotu pre propsu key
Úloha:
src/pages/Listing.jsx
return (
<>
<h1>Listing</h1>
<ul>
{cvs.map((cv) => (
<li key={cv.id}>{cv.title}</li>
))}
</ul>
</>
);
# PRESENTING CODE
- Vytvorte komponent Timer, obsahujúci text "Timer"
- bude zobrazený na Homepage
- na Homepage bude aj tlačidlo, ktoré bude prepínať príznak toho, či má alebo nemá byť Timer zobrazený
- zodpovednosťou Timeru bude, že ak bude zahrnutý v DOMe, tak každú sekundu do konzoly vypíše aktuálny unixový čas
- keď nebude, nebude vypisovať
Úloha:
src/pages/Homepage.jsx
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
src/pages/Homepage.jsx
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
Pro Tip
Závislosťou pre useEffect je každá premenná (a funkcia), ktorá sa používa vo vnútri efektu a pristupuje k propsom alebo stavu.
Mali by sme vymenovať všetky naše závislosti*.
* Okrem tých globálnych a importovaných, pretože tie sa väčšinou nemenia.
Pro Tip
Nepoužívajte jeden useEffect pre správu viacerých side effectov. Nebojte sa mať v komponente viacero volaní useEffect
useReducer
Hook určený pre prácu s komplexnejším stavom (napr. s celým objektom s viacerými property)
useContext
Hook určený pre sprístupnenie kontextu určeného na komunikáciu medzi rodičom a potomkami (aj naprieč viacerými úrovniami)
useCallback
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ť.
useMemo
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ť.
useRef
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
Context
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.
Vytvorenie contextu
// 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
Spýtanie sa na context (použitie)
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
Poskytnutie contextu (v rodičovi)
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
Pro Tip
Našou prvou voľbou by malo byť použitie props a po contexte siahnuť až vtedy, keď nám hrozí props drilling a nevieme sa mu vyhnúť.
Dôvod: context je menej prehľadný a náročnejší na výkon
Pro Tip
Ak si vyrobíme vlastný komponentový wrapper nad Context.Provider, tak v ňom vieme použiť celú škálu React nástrojov.
Napríklad useState/useReducer a spravovať tak stav aplikácie.
src/sandbox/MyContext.jsx
// ...
//
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
src/App.jsx
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
src/pages/Detail.jsx
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
- Vytvorte komponent Navigation
- umiestnite ho do adresára src/components
- bude obsahovať odkaz na "/"
- okrem toho bude zobrazovať meno prihláseného používateľa a tlačidlo na odhlásenie
- alebo tlačidlo na prihlásenie, ak používateľ nie je prihlásený
- informáciu o používateľovi získate z kontextu AuthContext, ktorý si vytvorte sami
Úloha:
src/component/Navigation.jsx
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
src/security/AuthContext.jsx
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
src/App.jsx
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
- Skúste zariadiť, aby sa App komponent pri svojom prvom vykonaní spýtal backendu na to, či je nejaký používateľ prihlásený
- URL je "/data/appUser.json"
- ak sa nám vrátia info o userovi, tak ho aplikácia prihlási
Úloha:
src/App.jsx
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
Prečo nás neprihlasuje?
Takto nám to nefunguje preto, lebo App dostane defaultné hodnoty z kontextu, pretože nemá nikde v úrovni nad sebou AuthContextProvider.
Riešenie
Presunieme AuthContextProvider ešte vyššie (tj. do main.jsx)
src/App.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
src/main.jsx
import { AuthContextProvider } from '@local/security/AuthContext';
// ...
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<AuthContextProvider>
<App />
</AuthContextProvider>
</React.StrictMode>
);
# PRESENTING CODE
Vlastný router
Cieľ:
- Podľa URL sa v App.jsx zobrazí obsah komponentu Homepage, Listing alebo Detail
- Zmena URL sa okamžite prejaví zobrazením a skrytím patričných komponentov (bez reloadu stránky)
- Hociktorý komponent si môže zistiť aktuálnu pathname a keď sa zmení, tak sa komponent vyrendruje nanovo (tj. pathname bude reaktívna)
URL | Komponent |
---|---|
/ | Homepage |
/listing | Listing |
/detail | Detail |
Otázky:
-
Odkiaľ zistíme, aká je aktuálna URL (pathname)?
- window.location.pathname
-
Ako dochádza k zmene URL bez reloadu?
- tlačidlami (dopredu, dozadu) v prehliadači, ak stránka používa history objekt (používame)
- programátorskou zmenou history objektu
-
Vieme v JS sledovať zmenu v adresnom riadku?
- vieme počúvať na udalosť popstate (tlačidlá v prehliadači)
- vieme dispatchovať vlastné eventy a počúvať na ne (naše programátorské zmeny URL)
- Zabezpečte, aby aplikácia pri zmene URL vyvolala event locationChange, to znamená:
- Upravte komponent Link tak, aby vyvolal tento event
- Na globálnej úrovni (main.jsx) zabezpečte, že vždy po vyvolaní eventu popstate, sa vyvolá aj náš locationChange
- V App.jsx zaregistrujte listener na locationChange, ktorý len do konzoly vypíše pathname
Úloha:
src/component/router/Link.jsx
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
src/main.jsx
addEventListener('popstate', () => {
dispatchEvent(new Event('locationChange'));
});
# PRESENTING CODE
src/App.jsx
useEffect(() => {
function listener() {
console.log(window.location.pathname);
};
addEventListener('locationChange', listener);
return () => removeEventListener('locationChange', listener);
}, []);
# PRESENTING CODE
- Pridajte do App.jsx nadpis, v ktorom vypíšete aktuálny window.location.pathname
Úloha:
src/App.jsx
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.
- Vyskúšajme v listeneri v App.jsx, namiesto vypísania aktuálnej hodnoty pathname do konzoly, ju zapísať do novej stavovej premennej nazvanej napr. url alebo pathname
Skúška:
src/App.jsx
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:
- v useEffect-e zaregistrovať listener
- vyrobiť stavovú premennú
- zapísať zmenu do premennej
- duplikácia
- veľa listenerov
- porušenie Single Responsibility Principle
- Kód, ktorý by sa opakoval vo viacerých komponentoch vieme vytiahnuť do samostatnej funkcie
- Bol to tzv. skrytý kolaborant
- Funkciu vieme vytiahnuť do samostatného modulu (súboru)
- Keďže však funkcia používa hooky, tak sama tiež musí byť hookom
- Dajme jej názov usePathname a umiestnime ju do src/router/usePathname.js
- čo bude návratovou hodnotou?
Skúška:
src/router/usePathname.js
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
src/App.jsx
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:
v useEffect-e zaregistrovať listenervyrobiť stavovú premennúzapísať zmenu do premennej- použiť usePathname hook ✅
duplikácia- veľa listenerov ❌
porušenie Single Responsibility Principle
- Čo keby sme mali iba jeden listener, jeden useEffect a jeden useState a použili na to context?
- Provider by sme zaregistrovali niekde vysoko (main.jsx)
- usePathname by si len prečítal pathname z contextu
- Vyrobte taký context a provider v src/router/RouterContext.jsx
Skúška:
src/router/RouterContext.jsx
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
src/router/usePathname.js
import { useContext } from 'react';
import RouterContext from '@local/router/RouterContext';
const usePathname = () => {
const routerCtx = useContext(RouterContext);
return routerCtx.pathname;
};
export default usePathname;
# PRESENTING CODE
src/main.jsx
import { RouterContextProvider } from '@local/router/RouterContext';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<AuthContextProvider>
<RouterContextProvider>
<App />
</RouterContextProvider>
</AuthContextProvider>
</React.StrictMode>
);
# PRESENTING CODE
src/router/RouterContext.jsx
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
- Vytvorte komponent Route
- Umiestnite ho do src/components/router/Route.jsx
- Prijíma dve propsy:
- path
- component
- Pomocou usePathname si porovná aktuálnu URL s propsou path
- Ak sú rovnaké, tak vykreslí component
- Ak nie, vráti null
Úloha:
src/components/router/Route.jsx
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
- Použite Route vo vnútri App.jsx na to, aby ste zobrazili iba stránku pre aktuálnu URL
Úloha:
URL | Komponent |
---|---|
/ | Homepage |
/listing | Listing |
/detail | Detail |
src/App.jsx
return (
{/* ... */}
<Route path="/" component={Homepage} />
<Route path="/listing" component={Listing} />
<Route path="/detail" component={Detail} />
{/* ... */}
);
# PRESENTING CODE
Cieľ:
- Podľa URL sa v App.jsx zobrazí obsah komponentu Homepage, Listing alebo Detail ✅
- Zmena URL sa okamžite prejaví zobrazením a skrytím patričných komponentov (bez reloadu stránky) ✅
- Hociktorý komponent si môže zistiť aktuálnu pathname a keď sa zmení, tak sa komponent vyrendruje nanovo (tj. pathname bude reaktívna) ✅
URL | Komponent |
---|---|
/ | Homepage |
/listing | Listing |
/detail | Detail |
✅
React Router
Inštalácia React Router
yarn add react-router-dom@6
# PRESENTING CODE
src/main.jsx
import { BrowserRouter } from 'react-router-dom';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<AuthContextProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</AuthContextProvider>
</React.StrictMode>
);
# PRESENTING CODE
src/App.jsx
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
src/components/Navigation.jsx
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
useRoutes
Hook, ktorý vieme použiť na automatické postavenie Routes a Route komponentov z konfigurácie
src/App.jsx
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
useLocation
Hook, ktorý vráti location objekt pre aktuálnu URL
useParams
Hook, ktorý vráti hodnoty parametrov z aktuálnej URL. Parametre sú definované v ceste pomocou dvojbodiek.
- path: /detail/:id
- URL: /detail/cv-1
- params: { id: 'cv-1' }
useSearchParams
Hook, ktorý vráti parametre z query (search) časti URL, tj. za otáznikom. Vráti ich ako objekt typu URLSearchParams
useNavigate
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.
- štandardné riešenie
- podpora parametrov
- podpora vnorených rout
- užitočné hooky
- návody na prepojenie s inými (najmä komponentovými) knižnicami
Prečo React Router?
A to je všetko
Ďakujem za pozornosť
React - rýchlokurz
By Milan Herda
React - rýchlokurz
- 693