React

Intenzívny rýchlokurz o základoch práce v Reacte

Milan Herda / máj 2022

Redux, MUI

React

  • Deklaratívny prístup k tvorbe rozhraní
  • Zobrazený UI závisí od stavu aplikácie
import { useState } from 'react';

const CounterButton = () => {
    const [state, setState] = useState(initialState);

    const clickHandler = () => {
        setState((prevState) => prevState + 1);
    };
  
    return (
        <button onClick={clickHandler}>{state}</button>
    );
};
# PRESENTING CODE

Stav komponentu

  • Čo ak viacero komponent potrebuje prístup k tomu istému stavu?
  • Čo v prípadoch, keď je náš stav zložitý?
  • Čo keď je aplikácia rozsiahla?
  • Čo ak máme takto viacero nezávislých stavov?

Kontext vs. stav mimo komponent?

Flux

Flux

Architektonický pattern pre správu jednosmerného toku dát*

* Neskôr aj knižnica od Facebooku implementujúca tento pattern.

Dnes už deprecated.

Redux

Redux

Asi najpopulárnejšia knižnica implementujúca Flux

  • manažuje globálny stav aplikácie
  • jednosmerný tok dát
  • predvídateľný
  • dá sa použiť "time-travelling"
  • podpora v pluginoch pre prehliadače
  • nezávislý od React-u

Redux

  • jednoduchý na pochopenie
  • veľa písania na rozbehanie

RTK - Redux Toolkit

Knižnica pre zjednodušenie písania logiky pre Redux

Pochádza od autorov Reduxu a je odporúčaná ako nový a správny spôsob práce s Reduxom

Redux

Ako sme v Reduxe robili po starom

Prečo potrebujeme prebrať starý spôsob, keď existuje nový?

  • RTK je len syntax sugar pre starý spôsob
  • v starom spôsobe je pekne vidieť princípy
  • šedivá je teória, zelený strom života
    • radšej si to raz naprogramujeme, ako budeme dlho počúvať teóriu, ktorú si nezapamätáme
yarn add redux react-redux

# alebo lepšie:

yarn add @reduxjs/toolkit react-redux
# PRESENTING CODE

Inštalácia

import { createStore } from 'redux';
import { createStore } from '@reduxjs/toolkit';

const initialState = {
    cvCount: 0,
};

const rootReducer = (state = initialState, action) => {
    if (action?.type === 'INCREMENT') {
        return {
            ...state,
            cvCount: state.cvCount + 1,
        };
    }

    return state;
};

const store = createStore(rootReducer, initialState);

export default store;
# PRESENTING CODE

src/store/index.js

import { Provider } from 'react-redux';
import store from '@local/store';

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <Provider store={store}>
            <AuthContextProvider>
                <BrowserRouter>
                    <App {...initialData} />
                </BrowserRouter>
            </AuthContextProvider>
        </Provider>
    </React.StrictMode>
);
# PRESENTING CODE

src/main.js

import { useSelector } from 'react-redux';

const Navigation = () => {
    // ...
    const cvCount = useSelector((state) => state.cvCount);

    return (
        <nav>
            <Link to="/">Homepage</Link>
            <Link to="/listing">Zoznam CV ({cvCount})</Link>{' '}
            {/* ... */}
        </nav>
    );
};

export default Navigation;
# PRESENTING CODE

src/component/Navigation.jsx

import { useDispatch, useSelector } from 'react-redux';

const Homepage = () => {
    const cvCount = useSelector((state) => state.cvCount);
    const dispatch = useDispatch();

    const handleIncrement = (e) => {
        e.preventDefault();
        dispatch({ type: 'INCREMENT' });
    };

    return (
        <>
            <div>Aktuálny počet CV: {cvCount}</div>

            <button onClick={handleIncrement}>Pridaj CV</button>
        </>
    );
};

export default Homepage;
# PRESENTING CODE

src/pages/Homepage.jsx

Názvoslovie

store

Objekt, v ktorom je uložený stav a ktorý poskytuje metódy na manipuláciu so stavom (dispatch, getState)

// store/index.js
import { createStore } from 'redux';

const store = createStore(rootReducer, initialState);

export default store;

// src/main.jsx
import { Provider } from 'react-redux';
import store from '@local/store';

<Provider store={store}>
  <App />
</Provider>

state

Objekt predstavujúci stav aplikácie, ktorý chceme držať v Reduxe.

const initialState = {
    appUser: { /* ... */ },
    cvList: { /* ... */},
    // ...
};

const store = createStore(rootReducer, initialState);
  • čistý dátový JS objekt, nesmie obsahovať funkcie
  • je generovaný pure funkciou, ktorej sa hovorí reducer (root reducer)
  • je immutable
  • v reálnych aplikáciách je rozdelený na časti, o ktoré sa starajú vlastné reducer funkcie
  • je vhodné mať úvodný stav

akcia

Objekt predstavujúci príkaz na zmenu stavu / udalosť, ktorá nastala a na jej základe treba zmeniť stav.

const dispatch = useDispatch();

dispatch({ type: 'INCREMENT' });
  • iba dátový objekt, žiadne funkcie
  • podľa konvencie obsahuje položku type, ktorej hodnotou je string v CAMEL_CASE
  • vo väčších aplikáciách sa unikátnosť typu zabezpečuje pridaním prefixu pre modul a prípadne aj vendora a ich oddelením znakom `/`. Napr. cvList/INCREMENT, profesia/cvList/INCREMENT
  • ak sú potrebné ďalšie údaje, konvenciou je vkladať ich do položky nazvanej payload

reducer

Pure funkcia, ktorá vypočíta nový stav. Vychádza pritom z predchádzajúceho stavu a akcie.

  • pure: nemá žiadne side effekty, výsledok funkcie je závislý iba na argumentoch
  • názov "reducer" je kvôli podobnosti s Array.reduce
  • reducer musí k predchádzajúcemu stavu pristupovať ako k immutable objektu
    • nesmie ho modifikovať
    • musí vytvoriť nový objekt predstavujúci nový stav

reducer

const rootReducer = (state = initialState, action) => {
    if (action?.type === 'INCREMENT') {
        return {
            ...state,
            cvCount: state.cvCount + 1,
        };
    }

    return state;
};

reducer

Prečo je state immutable a prečo je reducer pure function?

Predvídateľnosť, testovateľnosť a opakovateľnosť.

selector

Funkcia, ktorá ako parameter dostane celý stav a vráti jeho časť alebo nejakú z neho vypočítanú hodnotu.

import { useSelector } from 'react-redux';

const cvCount = useSelector((state) => state.cvCount);

Pro Tip

useSelector sa vykonáva po každej(!) dispatchnutej akcii.

Avšak, useSelector si sleduje referencie vrátených hodnôt a spôsobí re-render iba vtedy, ak sa zmení referencia.

  • v komponentách Homepage a Navigation je použitý rovnaký selektor
  • extrahujte ho do samostatnej funkcie
  • funkciu uložte na vhodné miesto a vhodne pomenujte

Úloha:

export const getCvCount = (state) => state.cvCount;
# PRESENTING CODE

src/store/selectors.js

import { getCvCount } from '@local/state/selectors';

const Homepage = () => {
    const cvCount = useSelector(getCvCount);
    // ...
};
# PRESENTING CODE

src/pages/Homepage.jsx

import { getCvCount } from '@local/state/selectors';

const Navigation = () => {
    const cvCount = useSelector(getCvCount);
    // ...
};
# PRESENTING CODE

src/component/Navigation.jsx

  • Pridajte tlačidlo pre "pridanie" CV aj na stránku so zoznamom CV
  • Urobte tam preklep v slove INCREMENT
  • Ako by sme riziku preklepov zabránili?
  • Vytvorte funkciu vytvárajúcu objekt reprezentujúci akciu a vhodne ju umiestnite

Úloha:

export const incrementCvCount = () => {
    return {
        type: 'INCREMENT',
    };
};
# PRESENTING CODE

src/store/actions.js

import { incrementCvCount } from '@local/store/actions';

const handleIncrement = (e) => {
    e.preventDefault();
    dispatch(incrementCvCount());
};
# PRESENTING CODE

src/pages/Listing.js, Homepage.js

Action Creator

Funkcia, ktorá vytvorí a vráti objekt reprezentujúci akciu.

Môže mať parametre, tie potom najčastejšie posúva do payload-u

export const incrementCvCount = () => {
    return {
        type: 'INCREMENT',
    };
};
  • Upravte kód tak, aby bolo možné určiť o koľko sa má zvýšiť počet CV
  • Na Homepage sa po kliknutí na tlačidlo zvýši počet o 5, na Listingu o 1
  • Bude to chcieť úpravy v:
    • action creator
    • action
    • reducer
    • dispatchovaní akcie

Úloha:

dispatch(incrementCvCount(5));
# PRESENTING CODE

src/pages/Homepage.jsx

export const incrementCvCount = (amount = 1) => {
    return {
        type: 'INCREMENT',
        payload: {
            amount,
        },
    };
};
# PRESENTING CODE

src/store/actions.js

const rootReducer = (state = initialState, action) => {
    if (action?.type === 'INCREMENT') {
        return {
            ...state,
            cvCount: state.cvCount + action.payload.amount,
        };
    }

    return state;
};
# PRESENTING CODE

src/store/index.js

Pro Tip

Ak sa rozhodnete používať starý spôsob práce s Reduxom, tak to nerobte.

 

Pro Tip

Ak sa predsa rozhodnete používať starý spôsob práce s Reduxom, tak budete potrebovať nejaký systém, ako organizovať rôzne "moduly".

Ja odporúčam re-ducks

RTK

Redux Toolkit

yarn add @reduxjs/toolkit react-redux
# PRESENTING CODE

Inštalácia

import { configureStore } from '@reduxjs/toolkit';
import cvListReducer from '@local/store/slice/cvList';

const store = configureStore({
    reducer: {
        cvList: cvListReducer,
    },
});

export default store;
# PRESENTING CODE

src/store/index.js

import { createSlice } from '@reduxjs/toolkit';

const initialState = {
    cvCount: 0,
};

export const cvListSlice = createSlice({
    name: 'cvList', // použije sa v action.type
    initialState,
    reducers: {
        // meno funkcie sa pouzije v action.type
        increment: (state, action) => {
            state.cvCount += action.payload ?? 1;
        },
    },
});

// RTK za nás sama vytvorí action creator funkcie.
// Hocijaký parameter pre action creator sa do reducera
// dostane ako hodnota action.payload
export const { increment } = cvListSlice.actions;

export default cvListSlice.reducer;
# PRESENTING CODE

src/store/slice/cvList.js

import { increment } from '@local/store/slice/cvList';

const Homepage = () => {
    const dispatch = useDispatch();

    const handleIncrement = (e) => {
        e.preventDefault();
        dispatch(increment(5));
    };

    // ...
};
# PRESENTING CODE

src/pages/Homepage.jsx, Listing.jsx

configureStore

Oproti createStore priamo predpokladá, že stav je zložený z viacerých modulov.

Modulu sa v RTK hovorí slice

RTK za nás rieši kombináciu reducerov, ale aj napríklad doplnenie enhanceru pre dev prostredie a uzitočných middlewarov

immer

Knižnica, ktorú RTK používa na sledovanie zmien nad stavom, ktorý odovzdáva reducerom.

Reducer v RTK tak môže stav mutovať a immer sa postará o to, že v Reduxe bude stav immutable a po zmene sa vytvorí nový objekt.

Prečo?

Ja náročné napísať logiku zmeny immutable stavu bez chyby.

Pozor: immer je súčasťou iba createSlice reducerov. Mimo nich musíte robiť immutable updaty sami.

  • Vytvorte slice appUser, ktorý bude držať info o prihlásenom používateľovi
  • Nahraďte ním funkcionalitu, ktorú doteraz obstarával AuthContext
  • AuthContext sa následne môže zmazať

Úloha:

import { createSlice } from '@reduxjs/toolkit';

const initialState = {
    isLoggedIn: false,
    username: null,
};

export const appUserSlice = createSlice({
    name: 'appUser',
    initialState,
    reducers: {
        login: (state, action) => {
            state.isLoggedIn = true;
            state.username = action.payload;
        },
        logout: (state) => {
            state.isLoggedIn = false;
            state.username = initialState.username;
        },
    },
});

export const { login, logout } = appUserSlice.actions;

export default appUserSlice.reducer;
# PRESENTING CODE

src/store/slice/appUser.js

import { configureStore } from '@reduxjs/toolkit';
import cvListReducer from '@local/store/slice/cvList';
import appUserReducer from '@local/store/slice/appUser';

const store = configureStore({
    reducer: {
        cvList: cvListReducer,
        appUser: appUserReducer,
    },
});

export default store;
# PRESENTING CODE

src/store/index.js

import { useDispatch } from 'react-redux';
import { login } from '@local/store/slice/appUser';

function App() {
    const pathname = usePathname();
    const dispatch = useDispatch();

    useEffect(() => {
        fetch('/data/appUser.json', { method: 'POST' })
            .then((response) => response.json())
            .then((data) => {
                if (data.id) {
                    dispatch(login(data.username));
                }
            });
    }, []);
  
    // ...
}
# PRESENTING CODE

src/App.jsx

import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { getCvCount } from '@local/store/selectors';
import { login, logout } from '@local/store/slice/appUser';

const Navigation = () => {
    const cvCount = useSelector(getCvCount);
    const isLoggedIn = useSelector((state) => state.appUser.isLoggedIn);
    const username = useSelector((state) => state.appUser.username);
    const dispatch = useDispatch();

    return (
        <nav>
            <Link to="/">Homepage</Link> <Link to="/listing">Zoznam CV ({cvCount})</Link>{' '}
            <Link to="/detail/cv-1">Detail</Link>{' '}
            {isLoggedIn ? (
                <>
                    <span>{username}</span>{' '}
                    <button onClick={() => dispatch(logout())}>Odhlásiť</button>
                </>
            ) : (
                <button onClick={() => dispatch(login('fero'))}>Prihlásiť</button>
            )}
        </nav>
    );
};

export default Navigation;
# PRESENTING CODE

src/component/Navigation.jsx

ukážka Redux DevTools

  • Je ešte vhodné mať getCvCount selector v samostatnom súbore?
  • Kam by sa mal presunúť?
  • Presuňte ho tam
  • Pôvodný súbor zmažte
  • Navrhnite spôsob, ako odlíšiť akcie a selectory exportované zo slice súboru

Úloha:

src/store/slice/cvList.js

import { createSlice } from '@reduxjs/toolkit';

// ...

export const { increment } = cvListSlice.actions;

export const selectCvCount = (state) => state.cvList.cvCount;

export default cvListSlice.reducer;
# PRESENTING CODE

src/component/Navigation.jsx

import { selectCvCount } from '@local/store/slice/cvList';

const Navigation = () => {
    const cvCount = useSelector(selectCvCount);

    // ...
};
# PRESENTING CODE

src/pages/Homepage.jsx

import { increment, selectCvCount } from '@local/store/slice/cvList';

const Homepage = () => {
    const cvCount = useSelector(selectCvCount);
    // ...
};
# PRESENTING CODE

thunk

thunk

Thunk je funkcia, ktorú môžeme používať ako action creator, tj. môžeme ju dispatchovať.

Avšak thunk nevracia objekt predstavujúci akciu, ale namiesto toho vracia funkciu, v ktorej sa môžu dispatchovať ďalšie akcie

thunk

Thunk je mechanizmus pre prácu s asynchrónnymi operáciami*

*thunk je iba jeden zo spôsobov (najpopulárnejší a preto defaultne v RTK)

// komponent

dispatch(loadUserData(42));

// slice súbor

export loadUserData = (userId) => (dispatch, getState) => {
    dispatch(startLoading());
  
    loadData(`/get-data/${userId}`).then((userData) => {
        dispatch(setUserData(userData));
    }).finally(() => {
        dispatch(stopLoading());
    });
};
# PRESENTING CODE

Príklad

// komponent

dispatch(loadUserData(42));

// slice súbor

export loadUserData = (userId) => {
    return (dispatch, getState) => {
        dispatch(startLoading());
  
        loadData(`/get-data/${userId}`).then((userData) => {
            dispatch(setUserData(userData));
        }).finally(() => {
            dispatch(stopLoading());
        });
    };
};
# PRESENTING CODE

Príklad

  • V App.jsx sa z backendu získavajú informácie o používateľovi priamo v komponente
  • Použite namiesto toho thunk funkciu vytvorenú v appUser slice
  • Zamyslite sa, ako by bolo vhodné upraviť appUser state

Úloha:

src/store/slice/appUser.js

// ...

export const loadUserData = () => (dispatch) => {
    dispatch(appUserSlice.actions.startFetching());

    fetch('/data/appUser.json', { method: 'POST' })
        .then((response) => response.json())
        .then((data) => {
            if (data.id) {
                dispatch(login(data.username));
            }
        })
        .finally(() => {
            dispatch(appUserSlice.actions.stopFetching());
        });
};
# PRESENTING CODE

src/store/slice/appUser.js

const initialState = {
    isFetching: false,
    isLoggedIn: false,
    username: null,
};

export const appUserSlice = createSlice({
    // ...
    reducers: {
        // ...
        startFetching: (state) => {
            state.isFetching = true;
        },
        stopFetching: (state) => {
            state.isFetching = false;
        },
    },
});
# PRESENTING CODE

src/App.jsx

import { loadUserData } from '@local/store/slice/appUser';

function App() {
    const pathname = usePathname();
    const dispatch = useDispatch();

    useEffect(() => {
        dispatch(loadUserData());
    }, []);

    // ...
}
# PRESENTING CODE

Upravte slice cvList tak, aby:

  • obsahoval zoznam životopisov načítaných z backendu (endpoint /data/data.json)
  • selectCvCount vracal počet CV v zozname
  • zrušte tlačidlá, reducer funkcie upravujúce počet
  • zoznam CV načítajte pri štarte aplikácie
  • Listing bude mať aktívne prekliky
  • Detail bude zobrazovať údaje k CV

Úloha:

src/store/slice/cvList.js

import { createSlice } from '@reduxjs/toolkit';

const initialState = {
    isFetching: false,
    items: [],
};

export const cvListSlice = createSlice({
    name: 'cvList',
    initialState,
    reducers: {
        startFetching: (state) => {
            state.isFetching = true;
        },
        stopFetching: (state) => {
            state.isFetching = false;
        },
        setData: (state, action) => {
            state.items = action.payload;
        },
    },
});

export const { setData } = cvListSlice.actions;
# PRESENTING CODE

src/store/slice/cvList.js

export const loadCvList = () => (dispatch) => {
    dispatch(cvListSlice.actions.startFetching());

    fetch('/data/data.json')
        .then((response) => response.json())
        .then((data) => {
            if (data?.cvList) {
                dispatch(setData(data.cvList));
            }
        })
        .finally(() => {
            dispatch(cvListSlice.actions.stopFetching());
        });
};

export const selectCvCount = (state) => state.cvList.items.length;

export default cvListSlice.reducer;
# PRESENTING CODE

src/App.jsx

import { loadCvList } from '@local/store/slice/cvList';

function App() {
    const dispatch = useDispatch();

    useEffect(() => {
        dispatch(loadUserData());
        dispatch(loadCvList());
    }, []);

    // ...
}
# PRESENTING CODE

Naozaj je dobrý nápad mať zrovna tieto dve akcie na tomto mieste?

Možno je vhodnejšie umiestnenie v main.jsx

src/pages/Homepage.jsx

import { useSelector } from 'react-redux';
import { selectCvCount } from '@local/store/slice/cvList';

const Homepage = () => {
    const cvCount = useSelector(selectCvCount);

    return (
        <>
            <h1>Homepage</h1>

            <div>Aktuálny počet CV: {cvCount}</div>
        </>
    );
};

export default Homepage;
# PRESENTING CODE

src/pages/Listing.jsx

import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';

const Listing = () => {
    const cvs = useSelector((state) => state.cvList.items);

    return (
        <>
            <h1>Listing</h1>
            <ul>
                {cvs?.map((cv) => (
                    <li key={cv.id}>
                        <Link to={`/detail/${cv.id}`}>{cv.title}</Link>
                    </li>
                ))}
            </ul>
        </>
    );
};

export default Listing;
# PRESENTING CODE

src/pages/Detail.jsx

import { useParams } from 'react-router-dom';
import { useSelector } from 'react-redux';

const Detail = () => {
    const params = useParams();
    const cv = useSelector((state) => state.cvList.items?.find((cv) => cv.id === params.id));

    if (!cv) {
        return <div>CV sa nenašlo</div>;
    }

    return (
        <>
            <h1>{cv.title}</h1>
            <dl>
                <dt>Meno</dt>
                <dd>{cv.name}</dd>
                <dt>Priezvisko</dt>
                <dd>{cv.lastname}</dd>
                <dt>Jazyk CV</dt>
                <dd>{cv.language}</dd>
            </dl>
        </>
    );
};

export default Detail;
# PRESENTING CODE

Všimli ste si, ako sa nám opakuje logika pre načítanie dát z backendu?

  • potreba flagov v stave (isFetching, isError...)
  • akcia pre začiatok, akcia pre načítanie, akcia pre ukončenie

Zamyslite sa, ako by ste túto duplicitu ostránili

Pro Tip

Redux väčšinou nemá zmysel použiť pre dáta, ktoré sú len v jednej komponente.

V reduceri nesmiem generovať ID, náhodné hodnoty, ani nič iné.

 

Čo mám teda robiť v situácii, keď potrebujem na viacerých miestach dispatchovať akciu, ktorá potrebuje napr. vygenerované ID?

A čo v situácii, keď si chcem toto generovanie centralizovať?

  • vytvoríme vlastnú obaľovaciu funkciu
  • thunk
  • "prepare" funkcia pre reducer
const postsSlice = createSlice({
    name: 'cvList',
    initialState,
    reducers: {
        createCv: {
            reducer(state, action) {
                state.items.push(action.payload);
            },
            prepare(title, name, surname, language) {
                return {
                    payload: {
                        id: nanoid(),
                        title,
                        name,
                        surname,
                        language,
                    },
                };
            }
        }
    }
});

// ...

dispatch(createCv(title, name, surname, language));

Čo ďalej?

Veľmi, veľmi, naozaj veľmi odporúčam si prečítať kompletný

oficiálny Redux Essentials tutoriál.

  • fantasticky napísaný
  • dozviete sa ako správne pracovať s RTK
  • dozviete sa veci, ktoré sme nepreberali
    • memoizácia (createSelector)
    • automatické vytváranie thunkov pre prácu s API (createAsyncThunk)
    • ako normalizovať dáta v state (createEntityAdapter)
    • tipy pre zlepšenie výkonu
    • ako v jednom slice reagovať na akciu z iného

Zoznam CV je stav našej FE aplikácie alebo databázy na BE?

Ak je to stav databázy, tak je Redux správny nástroj pre ich pamätanie?

Nie je tak Redux použitý iba ako akási cache a zoznam stavových flagov (isFetching...) pre BE stav?

Redux je nástroj pre udržiavanie stavu našej FE aplikácie.

RTK Query

Nástroj z RTK na manažovanie a zjednodušenie načítavania a cachovania dát z backendu.

Dáta, flagy a cachovacie kľúče sú uložené v Redux-e v samostatnom module (api slice).

Zabudovaná podpora REST API, ale je možné použiť aj GraphQL

React Query

Samostatná knižnica na riešenie toho istého problému.

  • nepoužíva Redux
  • nezávislá od spôsobu vytvárania dotazov na BE (REST, GraphQL, čokoľvek iné)
  • podľa mňa jednoduchšia na použitie
  • podporuje iba React (RTKQ je nezávislá na UI)

MUI

Komponentová knižnica založená na Material Design-e

Inštalácia MUI

yarn add @mui/material @emotion/react @emotion/styled
# PRESENTING CODE

Inštalácia fontov

yarn add @fontsource/roboto
# PRESENTING CODE

Nemôžem fonty len tak nalinkovať napr. priamo z fonts.googleapi.com?

Nie. Lebo GDPR.

Fonty, src/main/jsx

// ...

import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';

// ...
# PRESENTING CODE

CSS reset, src/main/jsx

// ...

import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import CssBaseline from '@mui/material/CssBaseline';

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <CssBaseline />
        <Provider store={store}>
            <BrowserRouter>
                <App />
            </BrowserRouter>
        </Provider>
    </React.StrictMode>
);
# PRESENTING CODE

Pro Tip

Keď sa postupne migruje aplikácia na MUI, tak môže byť vhodnejšie použiť ScopedCssBaseline iba na jej časti.

  • Nahradíme všetky nadpisy za pouzitie Typography

Spoločná úloha:

src/pages/Homepage.jsx

// ...
import Typography from '@mui/material/Typography';

const Homepage = () => {
    // ...

    return (
        <>
            <Typography variant="h1">Homepage</Typography>
            {/* ... */}
        </>
    );
};
# PRESENTING CODE
  • Nahradíme všetky nadpisy za pouzitie Typography
  • H1 je sémanticky ok, ale dizajnovo by sme radšej H3

Spoločná úloha:

src/pages/Homepage.jsx

// ...
import Typography from '@mui/material/Typography';

const Homepage = () => {
    // ...

    return (
        <>
            <Typography variant="h3" component="h1">
                Homepage
            </Typography>
            {/* ... */}
        </>
    );
};
# PRESENTING CODE
  • Nahradíme všetky nadpisy za pouzitie Typography
  • H1 je sémanticky ok, ale dizajnovo by sme radšej H3
  • Nadpisu iba na Homepage chcem dať modré (navy) pozadie a italic štýl

Spoločná úloha:

Dokumentácia k styled

src/pages/Homepage.jsx

// ...
import Typography from '@mui/material/Typography';
import { styled } from '@mui/material/styles';

const Homepage = () => {
    // ...

    return (
        <>
           <Title variant="h3" component="h1">
                Homepage
            </Title>
            {/* ... */}
        </>
    );
};

const Title = styled(Typography)(() => ({
    backgroundColor: 'navy',
    fontStyle: 'italic',
}));

# PRESENTING CODE
  • nadpis => Typography
  • H1 => H3
  • Nadpisu iba na Homepage chcem dať modré pozadie a italic štýl
  • Nadpis na Homepage bude mať paddin vo veľkosti 0.5 násobku štandardnej základnej veľkosti fontu a top a bottom margin vo veľkosti dvojnásobku

Spoločná úloha:

Spacing

  • MUI na určovanie rozmerov a veľkostí používa kroky vo veľkosti 8px.
  • Vychádza z predpokladu, že defaultná hodnota pre font size v prehliadačoch (1 REM) je 16px
  • Veľkosť kroku vieme zmeniť v špecifikácii témy
  • Téma poskytuje metódu spacing, ktorá ako argument očakáva násobok kroku a vráti vypočítaný počet pixelov
    • theme.spacing(1) => '8px'
    • theme.spacing(2.25) => '18px'

src/pages/Homepage.jsx

// ...
import { styled } from '@mui/material/styles';

// ...

const Title = styled(Typography)(({ theme }) => ({
    backgroundColor: 'steelblue',
    fontStyle: 'italic',
    padding: theme.spacing(1),
    marginTop: theme.spacing(4),
    marginBottom: theme.spacing(4),
}));

# PRESENTING CODE
  • nadpis => Typography
  • H1 => H3
  • custom štýl pre nadpis
  • margin, padding
  • Chcem, nadpis na Homepage mal farbu textu nastavenú na primárnu farbu témy

Spoločná úloha:

src/pages/Homepage.jsx

// ...
import { styled } from '@mui/material/styles';

// ...

const Title = styled(Typography)(({ theme }) => ({
    backgroundColor: 'steelblue',
    fontStyle: 'italic',
    padding: theme.spacing(1),
    marginTop: theme.spacing(4),
    marginBottom: theme.spacing(4),
    color: theme.palette.primary.main,
}));

# PRESENTING CODE
  • nadpis => Typography
  • H1 => H3
  • custom štýl pre nadpis
  • margin, padding
  • text => primárna farba
  • Chcem, aby primárna farba témy bola oranžová

Spoločná úloha:

src/styles/theme.js

import { createTheme } from '@mui/material/styles';

const theme = createTheme({
    palette: {
        primary: {
            main: '#ffa500',
        },
    },
});

export default theme;
# PRESENTING CODE

src/main.jsx

// ...
import theme from '@local/styles/theme';
import { ThemeProvider } from '@mui/material/styles';

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <CssBaseline />
        <Provider store={store}>
            <ThemeProvider theme={theme}>
                <BrowserRouter>
                    <App />
                </BrowserRouter>
            </ThemeProvider>
        </Provider>
    </React.StrictMode>
);
# PRESENTING CODE

Prepojenie MUI a react-router-dom

MUI má vlastný komponent Link, ktorý je vhodný používať namiesto <a>.

Ale taký istý element má aj react-router-dom

Potrebujeme skombinovat oba, lebo:

  • MUI Link má štýly, tému
  • Router Link má funkcionalitu

Začneme tým, že si urobíme vlastný Link.

Takže už budú tri!

src/components/router/Link.jsx

import { forwardRef } from 'react';
import { Link as RouterLink } from 'react-router-dom';

const Link = forwardRef(
    (props, ref) => {
        const { href, ...otherProps } = props;

        return <RouterLink ref={ref} to={href} {...otherProps} />;
    }
);

export default Link;
# PRESENTING CODE

src/components/router/Link.tsx

import { forwardRef } from 'react';
import { Link as RouterLink, LinkProps as RouterLinkProps } from 'react-router-dom';

const Link = forwardRef<any, Omit<RouterLinkProps, 'to'> & { href: RouterLinkProps['to'] }>(
    (props, ref) => {
        const { href, ...otherProps } = props;

        return <RouterLink ref={ref} to={href} {...otherProps} />;
    }
);

export default Link;
# PRESENTING CODE

Potom upravíme tému, kde povieme, že MUI Link a MUI Button Link komponent sa má rendrovať cez náš Link

src/styles/theme.js

import { createTheme } from '@mui/material';
import Link from '@local/components/router/Link';

const theme = createTheme({
    palette: {
        // ...
    },
    components: {
        MuiLink: {
            defaultProps: {
                component: Link,
            },
        },
        MuiButtonBase: {
            defaultProps: {
                LinkComponent: Link,
            },
        },
    },
});

export default theme;
# PRESENTING CODE

src/styles/theme.ts

import { createTheme } from '@mui/material';
import { LinkProps } from '@mui/material/Link';
import Link from '@local/components/router/Link';

const theme = createTheme({
    palette: {
        // ...
    },
    components: {
        MuiLink: {
            defaultProps: {
                component: Link,
            } as LinkProps,
        },
        MuiButtonBase: {
            defaultProps: {
                LinkComponent: Link,
            },
        },
    },
});

export default theme;
# PRESENTING CODE

Pro Tip

MUI komponenty importujeme z

@mui/material/NazovKomponentu

Tak vložíme do aplikácie iba žiadané časti a nie komplet celé MUI aj s naším galaktickým okolím

  • Vymeňte linky na Listingu za Link komponent z MUI
  • obaľte celý obsah stránky (okrem navigácie) do komponentu Container s 64px top marginom, ktorý nastavíte pomocou spacingu z témy

Úloha:

src/pages/Listing.jsx

// ...
import Link from '@mui/material/Link';

const Listing = () => {
    const cvs = useSelector((state) => state.cvList.items);

    return (
        <>
            <Typography variant="h3" component="h1">
                Listing
            </Typography>
            <ul>
                {cvs?.map((cv) => (
                    <li key={cv.id}>
                        <Link to={`/detail/${cv.id}`}>{cv.title}</Link>
                    </li>
                ))}
            </ul>
        </>
    );
};
# PRESENTING CODE

src/App.jsx

// ...
import Container from '@mui/material/Container';
import { styled } from '@mui/system';

// ...

return (
    <MyContextProvider>
        <div>
            <Navigation />

            <ContentContainer>
                <MyRoutes />
            </ContentContainer>
        </div>
    </MyContextProvider>
);

const ContentContainer = styled(Container)(({ theme }) => ({
    marginTop: theme.spacing(8),
}));
# PRESENTING CODE
  • Vyskúšajte pomocou AppBar vytvoriť krajšie navigačné menu
  • Tip: Odkazy budete musieť vykresliť cez Button

Úloha:

src/component/Navigation.jsx

import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { styled } from '@mui/material/styles';

const Navigation = () => {
    // ...

    return (
        <AppBar>
            <Toolbar>
                // ...
            </Toolbar>
        </AppBar>
    );
};

const MenuLinks = styled(Box)(() => ({
    flexGrow: 1,
}));
# PRESENTING CODE

src/component/Navigation.jsx

const Navigation = () => {
    // ...
    return (
        <AppBar>
            <Toolbar>
                <Button variant="secondary" to="/">
                    B2C
                </Button>
                <MenuLinks>
                    <Button variant="secondary" to="/listing">
                        Zoznam CV ({cvCount})
                    </Button>
                </MenuLinks>
                {isLoggedIn ? (
                    <>
                        <Typography component="span">{username}</Typography>
                        <Button variant="secondary" onClick={() => dispatch(logout())}>
                            Odhlásiť
                        </Button>
                    </>
                ) : (
                    <Button variant="secondary" onClick={() => dispatch(login('fero'))}>
                        Prihlásiť
                    </Button>
                )}
            </Toolbar>
        </AppBar>
    );
};
# PRESENTING CODE

Vyskúšali sme si len pár vecí, ale MUI toho ponúka oveľa viac.

  • môžeme pridávať vlastné farby do definície
  • komponenty si vieme prispôsobiť viacerými spôsobmi
  • podpora pre jednoduchú prácu s breakpointami a media queries:
    • theme.breakpoints
    • useMediaQuery
  • množstvo hotových komponent
  • dokumentácia k použitiu + prípadné hlbšie vysvetlenie a filozofia v dokumentácii k Material Design