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

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

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:

  1. príjme nový stav ako argument (využívame, ak nový stav nemá väzbu na predchádzajúci)
  2. 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ť listener
  • vyrobiť 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ť