• Főoldal
  • Blog
  • A React state kezelés előnyei, problémái és megoldásai

A React state kezelés előnyei, problémái és megoldásai

Létrehozás dátuma:2023-03-24 17:22:15
Utolsó módosítás dátuma:2023-03-24 17:22:15
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.
A React state kezelés előnyei, problémái és megoldásai

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 ugye csak 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!