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
* Ehm... Minimizing bundle size
- 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
React - rýchlokurz - časť 2
By Milan Herda
React - rýchlokurz - časť 2
- 568