Ha valaki foglalkozik legalább kezdő szinten a React bármilyen formájával (tehát ide tartozik a React Native is), akkor biztosra vehető, hogy találkozott state kezeléssel. A vanilla JS-hez képest egy nagyon kényelmes megoldást nyújtanak a komponensek futásidejű változtatására, frissítésére.
Ha valaki foglalkozik legalább kezdő szinten a React bármilyen formájával (tehát ide tartozik a React Native is), akkor biztosra vehető, hogy találkozott state kezeléssel. A vanilla JS-hez képest egy nagyon kényelmes megoldást nyújtanak a komponensek futásidejű változtatására, frissítésére.

Egyszerű komponensek szintjén általában problémamentesen működik a beépített state megoldás.
Hogy is néz ki a beépített state kezelés Reactben?
Nézzük a legegyszerűbb példát a dokumentációból:
import React, { useState } from 'react';
function CounterButton() {
const [count, setCount] = useState(0);
const name = "John";
const add = () => {
setCount(prev => prev+1);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={add}>
Click me {name}
</button>
</div>
);
}Itt annyi történik, hogy a gomb onClick eventje frissíti a count állapotát, az értékéhez hozzáad 1-et. Magát az állapotot láthatóan egy tagben olvassuk ki, így az az elem biztosan újra fog tölteni a változásra. Ami elsőre nem logikus, hogy itt igazából a teljes CounterButton komponens újra fog renderelni, tehát a benne lévő div is és a p-vel egy szinten lévő button is, továbbá a “name” nevű változónál is újra megtörténik az értékadás, hiába definiáltuk konstansként. Ez azért van, mivel a CounterButton egy funkcionális komponens (függvény alapú). Felfoghatjuk úgy is, mintha a statek változása esetén a CounterButton, mint függvény meg lenne hívva. Értelemszerűen egy függvény meghívása esetén az az elejétől kezdve újra fog futni. Ez egy ilyen egyszerű esetben nem okoz problémát, viszont komplexebb komponenseknél már a felhasználó által is látható lenne a statek változása és az ebből fakadó újra renderelés, ha ezt külön nem kezeljük le.
Bonyolultabb appoknál gyakran felmerül a state-ek továbbadása belső komponensek számára. Ennek a legegyszerűbb formája a propként átadás.
Az előző példához közel maradva tehát ha egy töltés animációt szeretnénk egy gombra rakni, akkor valahogy így fog kinézni:
const Parent = () => {
const [loading, setLoading] = useState(false);
const deleteSomething = async () => {
setLoading(true);
await fetch("...");
setLoading(false);
}
return (
<Button loading={loading} onClick={deleteSomething}>Törlés</Button>
)
}
const Button = ({children, loading, onClick}) => {
return(
<button onClick={onClick}>{loading ? <Loader/> : children}</button>
)
}Egy törlési kérelem elküldésekor a loader megjelenik a gombon a state alapján és egészen addig ott is marad, amíg a fetch-re nem érkezik válasz a backendtől.
Ha ezt gyakran változó adatokhoz használjuk, ráadásul nem csak 1-2 komponensen keresztül passolunk egy ilyen propot és nem csak 1 helyen változtatjuk (hiszen éppen ezért passoltuk egy magasabb szintről), akkor elég hamar átláthatatlan lesz a működés, mert minden amin keresztül átadtuk ezt, újra fog renderelni.
React Context API
Az átláthatóság javítására használhatunk Context API-t, aminek köszönhetően nem kell átadni az azonos kontextuson belül lévő komponensek között az állapotokat.
Maga a Context API része a Reactnek, amelynek köszönhetően csomagtelepítés és a bundle size növelése nélkül tudjuk használni.
const App = () => {
return (
<AppContextProvider>
<Frame>
<div>tartalom</div>
</Frame>
</AppContextProvider>
);
};
const AppContext = createContext();
const AppContextProvider = ({ children }) => {
const [app, setApp] = useState({
loading: false,
});
const value = useMemo(
() => ({
app,
setApp,
}),
[app],
);
return <AppContext.Provider value={value}>{children};
};
const useAppContext = () => {
const { app, setApp } = useContext(AppContext);
return { app, setApp };
};
const Frame = ({ children }) => {
const {
app: { loading },
} = useAppContext();
if (loading) {
return <Loader />;
}
return <div>{children}</div>;
};A következő példa nyilván az állatorvosi ló példája, de a működés bemutatására elegendő. Egy ilyen egyszerű funkció megvalósítására a való életben természetesen nem használnánk a Context API-t, látható, hogy aránylag sok kódra van szükségünk a megvalósításhoz.
A Context API használatához először létre kell hoznunk magát a contextet, majd egy providert, ami biztosítani fogja a hozzáférést a contexthez azokon a komponenseken belül, amelyek “wrappelve” vannak a providerbe.
const AppContext = createContext();
const AppContextProvider = ({ children }) => {
const [app, setApp] = useState({
loading: true,
});
const value = useMemo(
() => ({
app,
setApp,
}),
[app],
);
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
const useAppContext = () => {
const { app, setApp } = useContext(AppContext);
return { app, setApp };
};Az app staten belül létrehoztunk egy loading mezőt, ami a példában azt határozza meg, hogy éppen tölt-e az alkalmazás. Ha több adat lekérése szükséges minden friss indításnál, akkor érdemes a kezdeti állapotot “true”-ként definiálni.
Mivel a Context API használata akkor ajánlott, ha több helyen hozzá kell férnünk egy adott statehez, ezért érdemes egy egyedi hookot készíteni, ami biztosítani fogja a hozzáférést a contexthez. Így a használat helyén nem kell majd importálnunk az AppContextet, és a useContextet, elég a saját useAppContext hookunk, ezzel is növelve az átláthatóságot.
const App = () => {
return (
<AppContextProvider>
<Frame>
<div>tartalom</div>
</Frame>
</AppContextProvider>
);
};Miután elkészült a context providerünk, wrappeljük bele azokat a komponenseket, amikben szeretnénk, hogy elérhető legyen a context. A példában az egész app hozzá fog férni, kivéve természetesen maga az App komponens (Mivel abban történik a wrappelés).
const Frame = ({ children }) => {
const {
app: { loading },
} = useAppContext();
if (loading) {
return;
}
return <div>{children}</div>;
};A Frame komponenst felfoghatjuk az egész alkalmazás kereteként. Látható, hogy a useAppContext egyedi hookunk által könnyen hozzáférünk a loading statehez. Ennek köszönhetően mondhatjuk azt, hogy ha a loading state true, akkor egy Loader komponenst jelenítünk meg, minden más esetben pedig magát a tartalmat.
Mivel a statek sajátossága az, hogy változtatásuk esetén újrarenderel a hozzájuk tartozó komponens, így a Context API használatával is ugyanez történik. Tehát ha a provideren belül megváltozik a loading stateünk, akkor minden, a provideren belül lévő komponens újra fog renderelni. Ha ezt a működést szeretnénk elkerülni, abban az esetben még tovább kell mennünk a Reactes state kezelésben, itt jön a képbe a Redux.
React Redux
Az újra renderelés problémáját azonban a Context API nem oldja meg sajnos. Az egyik legismertebb megoldás lehet a React Redux könyvtár használata.
A Reduxot nagyjából úgy képzeljük el, mint egy okos kliensoldali adatbázist, amelybe bárhonnan írhatunk és bárhonnan olvashatjuk, ezzel felváltva a state-ek használatát.
A gyakorlatban ez úgy néz ki, hogy létrehozunk egy storet (ebből egyébként több is lehet egyszerre), amelyben tárolni fogjuk az adatokat.
const rootReducer = combineReducers({
auth: authReducer,
});
export const store = createStore(rootReducer);
const makeStore = () => store;
export const wrapper = createWrapper(makeStore);Példánkban NextJS-t használunk. Az Appunkat beletesszük egy Providerbe, amely megkapja a storet propként, ami a useWrappedStore hook segítségével olvasható ki. Ennek köszönhetően minden ami az Appban van, eléri majd a storet.
function MyApp({ Component, pageProps, ...rest }) {
const { store, props } = wrapper.useWrappedStore({
...pageProps,
...rest
});
return (
<Provider store={store}>
<AppWithRedux Component={Component} pageProps={pageProps} />
</Provider>
);
}Adat beírást a dispatch függvény használatával tudunk elindítani, amellyel elindul egy action, amelynek köszönhetően a reducerben lefut az adott actionnek megfelelő rész, bekerül az adat a storeba.
Ez lesz a reducerünk:
const defaultState = {
isAuthenticated: undefined,
};
export default (state = defaultState, action) => {
switch (action.type) {
case 'SET_IS_AUTHENTICATED':
return {
...state,
isAuthenticated: action.data.isAuthenticated,
};
default:
return state;
}
};Ha meghívódik egy action akkor a reducer function lefut és az action típusától függően módosítja a storeban a state-et. Esetünkben az isAuthenticated fog átállni majd, amikor lefut az alábbi action:
export const setIsAuthenticated = isAuthenticated => {
return {
type: 'SET_IS_AUTHENTICATED',
data: {
isAuthenticated: isAuthenticated,
},
};
};Itt nincs semmi varázslat, megadjuk hogy a reducer milyen típust kapjon meg, illetve beletesszük a konkrét adatot amit szeretnénk beírni. Ezután pedig már csak dispatchelni kell:
dispatch(setIsAuthenticated(isAuthenticated))És az adatunk be is került a storeba. Ha kiolvasni szeretnénk, akkor komponensekben való használathoz a useSelector hookot tudjuk használni, melyet az adott elem kulcsával címzünk.
const auth = useSelector(state => state.auth)Ezután a selectorból kijövő adatot már kezelhetjük úgy, mintha egy sima state lenne, lokálisan egy komponensen belül. Ennek az egésznek az a nagy előnye, hogy így csak az a komponens fog újrarenderelni amibe a selectort írtuk, a parent elemek nem. Tekinthetünk rá úgy, mintha ez egy lokális state lenne, de globális scopeban is elérjük.
A hátránya ezek után egyértelműen az, hogy egyszerű appoknál túl sok plusz kódot kell gyártani, így nincs értelme élből mindenre Reduxot használni.
Vedd fel velünk a kapcsolatot!
Ha elakadtál a fejlesztéssel vagy valamilyen kérdésed van, keresd bizalommal alkalmazás fejlesztő kollégáinkat!
Kapcsolódó cikkek
A mobile-first szemlélet előnyei és hátrányai
A modern webdesign már nem a monitoroknál indul. A mobile-first megközelítés a telefonokra tervez először, csak ezután jön a már megszokott asztali kinézet.
