Dubinski uvid u Reactov useReducer hook za učinkovito upravljanje složenim stanjima aplikacije, čime se poboljšavaju performanse i održivost globalnih React projekata.
React useReducer uzorak: Ovladavanje upravljanjem složenim stanjem
U svijetu front-end razvoja koji se neprestano razvija, React se etablirao kao vodeći framework za izradu korisničkih sučelja. Kako aplikacije postaju složenije, upravljanje stanjem postaje sve veći izazov. Hook useState
pruža jednostavan način za upravljanje stanjem unutar komponente, ali za složenije scenarije, React nudi moćnu alternativu: hook useReducer
. Ovaj blog post duboko ulazi u useReducer
uzorak, istražujući njegove prednosti, praktične implementacije i kako može značajno poboljšati vaše React aplikacije na globalnoj razini.
Razumijevanje potrebe za upravljanjem složenim stanjem
Pri izradi React aplikacija često se susrećemo sa situacijama u kojima stanje komponente nije samo jednostavna vrijednost, već skup međusobno povezanih podataka ili stanje koje ovisi o prethodnim vrijednostima stanja. Razmotrite ove primjere:
- Autentifikacija korisnika: Upravljanje statusom prijave, podacima o korisniku i autentifikacijskim tokenima.
- Obrada obrazaca: Praćenje vrijednosti više polja za unos, pogrešaka pri validaciji i statusa podnošenja.
- Košarica u e-trgovini: Upravljanje artiklima, količinama, cijenama i informacijama o naplati.
- Chat aplikacije u stvarnom vremenu: Upravljanje porukama, prisutnošću korisnika i statusom veze.
U ovim scenarijima, korištenje samo useState
hooka može dovesti do složenog koda kojim je teško upravljati. Može postati nezgrapno ažurirati više varijabli stanja kao odgovor na jedan događaj, a logika za upravljanje tim ažuriranjima može postati raspršena po komponenti, što otežava razumijevanje i održavanje. Ovdje useReducer
dolazi do izražaja.
Predstavljanje useReducer
hooka
Hook useReducer
je alternativa useState
hooku za upravljanje složenom logikom stanja. Temelji se na principima Redux uzorka, ali je implementiran unutar same React komponente, čime se u mnogim slučajevima eliminira potreba za zasebnom vanjskom bibliotekom. Omogućuje vam centraliziranje logike ažuriranja stanja u jednoj funkciji koja se naziva reducer.
Hook useReducer
prima dva argumenta:
- Reducer funkcija: Ovo je čista funkcija koja kao ulazne parametre prima trenutno stanje i akciju te vraća novo stanje.
- Početno stanje: Ovo je početna vrijednost stanja.
Hook vraća niz koji sadrži dva elementa:
- Trenutno stanje: Ovo je trenutna vrijednost stanja.
- Dispatch funkcija: Ova se funkcija koristi za pokretanje ažuriranja stanja slanjem akcija (dispatching actions) reduceru.
Reducer funkcija
Reducer funkcija je srce useReducer
uzorka. To je čista funkcija, što znači da ne bi trebala imati nikakve nuspojave (poput API poziva ili modificiranja globalnih varijabli) i uvijek bi trebala vraćati isti izlaz za isti ulaz. Reducer funkcija prima dva argumenta:
state
: Trenutno stanje.action
: Objekt koji opisuje što bi se trebalo dogoditi sa stanjem. Akcije obično imaju svojstvotype
koje označava vrstu akcije i svojstvopayload
koje sadrži podatke povezane s akcijom.
Unutar reducer funkcije koristite switch
naredbu ili if/else if
naredbe za obradu različitih vrsta akcija i odgovarajuće ažuriranje stanja. To centralizira vašu logiku ažuriranja stanja i olakšava razumijevanje kako se stanje mijenja kao odgovor na različite događaje.
Dispatch funkcija
Dispatch funkcija je metoda koju koristite za pokretanje ažuriranja stanja. Kada pozovete dispatch(action)
, akcija se prosljeđuje reducer funkciji, koja zatim ažurira stanje na temelju vrste i payload-a akcije.
Praktičan primjer: Implementacija brojača
Krenimo s jednostavnim primjerom: komponenta brojača. Ovo ilustrira osnovne koncepte prije nego što prijeđemo na složenije primjere. Napravit ćemo brojač koji se može povećavati, smanjivati i resetirati:
import React, { useReducer } from 'react';
// Definiranje vrsta akcija
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';
// Definiranje reducer funkcije
function counterReducer(state, action) {
switch (action.type) {
case INCREMENT:
return { count: state.count + 1 };
case DECREMENT:
return { count: state.count - 1 };
case RESET:
return { count: 0 };
default:
return state;
}
}
function Counter() {
// Inicijalizacija useReducer-a
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>Broj: {state.count}</p>
<button onClick={() => dispatch({ type: INCREMENT })}>Povećaj</button>
<button onClick={() => dispatch({ type: DECREMENT })}>Smanji</button>
<button onClick={() => dispatch({ type: RESET })}>Resetiraj</button>
</div>
);
}
export default Counter;
U ovom primjeru:
- Definiramo vrste akcija kao konstante radi bolje održivosti (
INCREMENT
,DECREMENT
,RESET
). - Funkcija
counterReducer
prima trenutno stanje i akciju. Koristiswitch
naredbu kako bi odredila kako ažurirati stanje na temelju vrste akcije. - Početno stanje je
{ count: 0 }
. - Funkcija
dispatch
koristi se u rukovateljima događaja klika na gumbe za pokretanje ažuriranja stanja. Na primjer,dispatch({ type: INCREMENT })
šalje akciju tipaINCREMENT
reduceru.
Proširenje primjera brojača: Dodavanje payload-a
Izmijenimo brojač kako bismo omogućili povećanje za određenu vrijednost. Ovo uvodi koncept payload-a u akciji:
import React, { useReducer } from 'react';
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';
const SET_VALUE = 'SET_VALUE';
function counterReducer(state, action) {
switch (action.type) {
case INCREMENT:
return { count: state.count + action.payload };
case DECREMENT:
return { count: state.count - action.payload };
case RESET:
return { count: 0 };
case SET_VALUE:
return { count: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
const [inputValue, setInputValue] = React.useState(1);
return (
<div>
<p>Broj: {state.count}</p>
<button onClick={() => dispatch({ type: INCREMENT, payload: parseInt(inputValue) || 1 })}>Povećaj za {inputValue}</button>
<button onClick={() => dispatch({ type: DECREMENT, payload: parseInt(inputValue) || 1 })}>Smanji za {inputValue}</button>
<button onClick={() => dispatch({ type: RESET })}>Resetiraj</button>
<input
type="number"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
</div>
);
}
export default Counter;
U ovom proširenom primjeru:
- Dodali smo vrstu akcije
SET_VALUE
. - Akcije
INCREMENT
iDECREMENT
sada prihvaćajupayload
, koji predstavlja iznos za povećanje ili smanjenje.parseInt(inputValue) || 1
osigurava da je vrijednost cijeli broj i postavlja zadanu vrijednost na 1 ako je unos nevažeći. - Dodali smo polje za unos koje korisnicima omogućuje postavljanje vrijednosti za povećanje/smanjenje.
Prednosti korištenja useReducer
-a
Uzorak useReducer
nudi nekoliko prednosti u odnosu na izravno korištenje useState
-a za upravljanje složenim stanjem:
- Centralizirana logika stanja: Sva ažuriranja stanja obrađuju se unutar reducer funkcije, što olakšava razumijevanje i ispravljanje pogrešaka u promjenama stanja.
- Poboljšana organizacija koda: Odvajanjem logike ažuriranja stanja od logike renderiranja komponente, vaš kod postaje organiziraniji i čitljiviji, što promiče bolju održivost koda.
- Predvidljiva ažuriranja stanja: Budući da su reduceri čiste funkcije, možete lako predvidjeti kako će se stanje promijeniti s obzirom na određenu akciju i početno stanje. To znatno olakšava ispravljanje pogrešaka i testiranje.
- Optimizacija performansi:
useReducer
može pomoći u optimizaciji performansi, posebno kada su ažuriranja stanja računski zahtjevna. React može učinkovitije optimizirati ponovno renderiranje kada je logika ažuriranja stanja sadržana u reduceru. - Mogućnost testiranja: Reduceri su čiste funkcije, što ih čini lakima za testiranje. Možete pisati jedinične testove kako biste osigurali da vaš reducer ispravno obrađuje različite akcije i početna stanja.
- Alternative za Redux: Za mnoge aplikacije,
useReducer
pruža pojednostavljenu alternativu Reduxu, eliminirajući potrebu za zasebnom bibliotekom i troškovima njezine konfiguracije i upravljanja. To može pojednostaviti vaš razvojni tijek rada, posebno za manje do srednje velike projekte.
Kada koristiti useReducer
Iako useReducer
nudi značajne prednosti, nije uvijek pravi izbor. Razmislite o korištenju useReducer
-a kada:
- Imate složenu logiku stanja koja uključuje više varijabli stanja.
- Ažuriranja stanja ovise o prethodnom stanju (npr. izračunavanje tekućeg zbroja).
- Trebate centralizirati i organizirati svoju logiku ažuriranja stanja radi bolje održivosti.
- Želite poboljšati mogućnost testiranja i predvidljivost ažuriranja stanja.
- Tražite uzorak sličan Reduxu bez uvođenja zasebne biblioteke.
Za jednostavna ažuriranja stanja, useState
je često dovoljan i jednostavniji za korištenje. Prilikom donošenja odluke uzmite u obzir složenost vašeg stanja i potencijal za rast.
Napredni koncepti i tehnike
Kombiniranje useReducer
-a s Context API-jem
Za upravljanje globalnim stanjem ili dijeljenje stanja između više komponenti, možete kombinirati useReducer
s Reactovim Context API-jem. Ovaj pristup se često preferira u odnosu na Redux za manje do srednje velike projekte gdje ne želite uvoditi dodatne ovisnosti.
import React, { createContext, useReducer, useContext } from 'react';
// Definiranje vrsta akcija i reducera (kao i prije)
const INCREMENT = 'INCREMENT';
// ... (druge vrste akcija i counterReducer funkcija)
const CounterContext = createContext();
function CounterProvider({ children }) {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
}
function useCounter() {
return useContext(CounterContext);
}
function Counter() {
const { state, dispatch } = useCounter();
return (
<div>
<p>Broj: {state.count}</p>
<button onClick={() => dispatch({ type: INCREMENT })}>Povećaj</button>
</div>
);
}
function App() {
return (
<CounterProvider>
<Counter />
</CounterProvider>
);
}
export default App;
U ovom primjeru:
- Stvaramo
CounterContext
pomoćucreateContext
. CounterProvider
omotava aplikaciju (ili dijelove koji trebaju pristup stanju brojača) i pružastate
idispatch
izuseReducer
-a.- Hook
useCounter
pojednostavljuje pristup kontekstu unutar podređenih komponenti. - Komponente poput
Counter
sada mogu pristupiti i mijenjati stanje brojača globalno. To eliminira potrebu za prosljeđivanjem stanja i dispatch funkcije kroz više razina komponenti, pojednostavljujući upravljanje propsima.
Testiranje useReducer
-a
Testiranje reducera je jednostavno jer su to čiste funkcije. Možete lako testirati reducer funkciju izolirano pomoću okvira za jedinično testiranje kao što su Jest ili Mocha. Evo primjera korištenja Jesta:
import { counterReducer } from './counterReducer'; // Pretpostavljajući da je counterReducer u zasebnoj datoteci
const INCREMENT = 'INCREMENT';
describe('counterReducer', () => {
it('trebao bi povećati brojač', () => {
const state = { count: 0 };
const action = { type: INCREMENT };
const newState = counterReducer(state, action);
expect(newState.count).toBe(1);
});
it('trebao bi vratiti isto stanje za nepoznate vrste akcija', () => {
const state = { count: 10 };
const action = { type: 'UNKNOWN_ACTION' };
const newState = counterReducer(state, action);
expect(newState).toBe(state); // Provjera da se stanje nije promijenilo
});
});
Testiranje vaših reducera osigurava da se ponašaju kako se očekuje i olakšava refaktoriranje logike stanja. Ovo je ključan korak u izgradnji robusnih i održivih aplikacija.
Optimizacija performansi pomoću memoizacije
Kada radite sa složenim stanjima i čestim ažuriranjima, razmislite o korištenju useMemo
za optimizaciju performansi vaših komponenti, posebno ako imate izvedene vrijednosti izračunate na temelju stanja. Na primjer:
import React, { useReducer, useMemo } from 'react';
function reducer(state, action) {
// ... (logika reducera)
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, initialState);
// Izračunaj izvedenu vrijednost, memoizirajući je s useMemo
const derivedValue = useMemo(() => {
// Računski zahtjevan izračun temeljen na stanju
return state.value1 + state.value2;
}, [state.value1, state.value2]); // Ovisnosti: ponovno izračunaj samo kada se ove vrijednosti promijene
return (
<div>
<p>Izvedena vrijednost: {derivedValue}</p>
<button onClick={() => dispatch({ type: 'UPDATE_VALUE1', payload: 10 })}>Ažuriraj vrijednost 1</button>
<button onClick={() => dispatch({ type: 'UPDATE_VALUE2', payload: 20 })}>Ažuriraj vrijednost 2</button>
</div>
);
}
U ovom primjeru, derivedValue
se izračunava samo kada se state.value1
ili state.value2
promijene, sprječavajući nepotrebne izračune pri svakom ponovnom renderiranju. Ovaj pristup je uobičajena praksa za osiguravanje optimalnih performansi renderiranja.
Primjeri iz stvarnog svijeta i slučajevi upotrebe
Istražimo nekoliko praktičnih primjera gdje je useReducer
vrijedan alat u izradi React aplikacija za globalnu publiku. Imajte na umu da su ovi primjeri pojednostavljeni kako bi ilustrirali osnovne koncepte. Stvarne implementacije mogu uključivati složeniju logiku i ovisnosti.
1. Filteri proizvoda u e-trgovini
Zamislite web stranicu za e-trgovinu (poput popularnih platformi kao što su Amazon ili AliExpress, dostupnih globalno) s velikim katalogom proizvoda. Korisnici trebaju filtrirati proizvode po različitim kriterijima (raspon cijena, marka, veličina, boja, zemlja podrijetla itd.). useReducer
je idealan za upravljanje stanjem filtera.
import React, { useReducer } from 'react';
const initialState = {
priceRange: { min: 0, max: 1000 },
brand: [], // Niz odabranih marki
color: [], // Niz odabranih boja
//... ostali kriteriji filtriranja
};
function filterReducer(state, action) {
switch (action.type) {
case 'UPDATE_PRICE_RANGE':
return { ...state, priceRange: action.payload };
case 'TOGGLE_BRAND':
const brand = action.payload;
return { ...state, brand: state.brand.includes(brand) ? state.brand.filter(b => b !== brand) : [...state.brand, brand] };
case 'TOGGLE_COLOR':
// Slična logika za filtriranje boja
return { ...state, color: state.color.includes(action.payload) ? state.color.filter(c => c !== action.payload) : [...state.color, action.payload] };
// ... ostale akcije filtriranja
default:
return state;
}
}
function ProductFilter() {
const [state, dispatch] = useReducer(filterReducer, initialState);
// UI komponente za odabir kriterija filtriranja i pokretanje dispatch akcija
// Na primjer: klizač za cijenu, potvrdni okviri za marke, itd.
return (
<div>
<!-- UI elementi filtera -->
</div>
);
}
Ovaj primjer pokazuje kako upravljati s više kriterija filtriranja na kontroliran način. Kada korisnik izmijeni bilo koju postavku filtera (cijenu, marku itd.), reducer ažurira stanje filtera u skladu s tim. Komponenta odgovorna za prikaz proizvoda zatim koristi ažurirano stanje za filtriranje prikazanih proizvoda. Ovaj uzorak podržava izgradnju složenih sustava filtriranja uobičajenih na globalnim platformama za e-trgovinu.
2. Višekoračni obrasci (npr. obrasci za međunarodnu dostavu)
Mnoge aplikacije uključuju višekoračne obrasce, poput onih koji se koriste za međunarodnu dostavu ili stvaranje korisničkih računa sa složenim zahtjevima. useReducer
se ističe u upravljanju stanjem takvih obrazaca.
import React, { useReducer } from 'react';
const initialState = {
step: 1, // Trenutni korak u obrascu
formData: {
firstName: '',
lastName: '',
address: '',
city: '',
country: '',
// ... ostala polja obrasca
},
errors: {},
};
function formReducer(state, action) {
switch (action.type) {
case 'NEXT_STEP':
return { ...state, step: state.step + 1 };
case 'PREV_STEP':
return { ...state, step: state.step - 1 };
case 'UPDATE_FIELD':
return { ...state, formData: { ...state.formData, [action.payload.field]: action.payload.value } };
case 'SET_ERRORS':
return { ...state, errors: action.payload };
case 'SUBMIT_FORM':
// Ovdje obradite logiku podnošenja obrasca, npr. API pozive
return state;
default:
return state;
}
}
function MultiStepForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
// Logika renderiranja za svaki korak obrasca
// Na temelju trenutnog koraka u stanju
const renderStep = () => {
switch (state.step) {
case 1:
return <Step1 formData={state.formData} dispatch={dispatch} />;
case 2:
return <Step2 formData={state.formData} dispatch={dispatch} />;
// ... ostali koraci
default:
return <p>Nevažeći korak</p>;
}
};
return (
<div>
{renderStep()}
<!-- Navigacijski gumbi (Dalje, Prethodno, Pošalji) na temelju trenutnog koraka -->
</div>
);
}
Ovo ilustrira kako upravljati različitim poljima obrasca, koracima i potencijalnim pogreškama pri validaciji na strukturiran i održiv način. Ključno je za izgradnju korisnički prilagođenih procesa registracije ili naplate, posebno za međunarodne korisnike koji mogu imati različita očekivanja na temelju svojih lokalnih običaja i iskustva s raznim platformama poput Facebooka ili WeChata.
3. Aplikacije u stvarnom vremenu (Chat, alati za suradnju)
useReducer
je koristan za aplikacije u stvarnom vremenu, kao što su alati za suradnju poput Google Docsa ili aplikacije za razmjenu poruka. Obrađuje događaje poput primanja poruka, pridruživanja/odlaska korisnika i statusa veze, osiguravajući da se korisničko sučelje ažurira prema potrebi.
import React, { useReducer, useEffect } from 'react';
const initialState = {
messages: [],
users: [],
connectionStatus: 'connecting',
};
function chatReducer(state, action) {
switch (action.type) {
case 'RECEIVE_MESSAGE':
return { ...state, messages: [...state.messages, action.payload] };
case 'USER_JOINED':
return { ...state, users: [...state.users, action.payload] };
case 'USER_LEFT':
return { ...state, users: state.users.filter(user => user.id !== action.payload.id) };
case 'SET_CONNECTION_STATUS':
return { ...state, connectionStatus: action.payload };
default:
return state;
}
}
function ChatRoom() {
const [state, dispatch] = useReducer(chatReducer, initialState);
useEffect(() => {
// Uspostavljanje WebSocket veze (primjer):
const socket = new WebSocket('wss://your-websocket-server.com');
socket.onopen = () => dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'connected' });
socket.onmessage = (event) => dispatch({ type: 'RECEIVE_MESSAGE', payload: JSON.parse(event.data) });
socket.onclose = () => dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'disconnected' });
return () => socket.close(); // Čišćenje prilikom demontiranja komponente
}, []);
// Renderiranje poruka, popisa korisnika i statusa veze na temelju stanja
return (
<div>
<p>Status veze: {state.connectionStatus}</p>
<!-- UI za prikazivanje poruka, popisa korisnika i slanje poruka -->
</div>
);
}
Ovaj primjer pruža osnovu za upravljanje chatom u stvarnom vremenu. Stanje upravlja pohranom poruka, korisnicima koji su trenutno u chatu i statusom veze. Hook useEffect
odgovoran je za uspostavljanje WebSocket veze i obradu dolaznih poruka. Ovakav pristup stvara responzivno i dinamično korisničko sučelje koje je prilagođeno korisnicima diljem svijeta.
Najbolje prakse za korištenje useReducer
-a
Kako biste učinkovito koristili useReducer
i stvarali održive aplikacije, razmotrite ove najbolje prakse:
- Definirajte vrste akcija: Koristite konstante za svoje vrste akcija (npr.
const INCREMENT = 'INCREMENT';
). To olakšava izbjegavanje tipfelera i poboljšava čitljivost koda. - Održavajte reducere čistima: Reduceri bi trebali biti čiste funkcije. Ne bi smjeli imati nuspojave, poput modificiranja globalnih varijabli ili upućivanja API poziva. Reducer bi trebao samo izračunati i vratiti novo stanje na temelju trenutnog stanja i akcije.
- Nepromjenjiva ažuriranja stanja: Uvijek ažurirajte stanje na nepromjenjiv način. Nemojte izravno mijenjati objekt stanja. Umjesto toga, stvorite novi objekt sa željenim promjenama koristeći spread sintaksu (
...
) iliObject.assign()
. To sprječava neočekivano ponašanje i omogućuje lakše ispravljanje pogrešaka. - Strukturirajte akcije s payloadima: Koristite svojstvo
payload
u svojim akcijama za prosljeđivanje podataka reduceru. To čini vaše akcije fleksibilnijima i omogućuje vam obradu šireg raspona ažuriranja stanja. - Koristite Context API za globalno stanje: Ako se vaše stanje treba dijeliti između više komponenti, kombinirajte
useReducer
s Context API-jem. To pruža čist i učinkovit način upravljanja globalnim stanjem bez uvođenja vanjskih ovisnosti poput Reduxa. - Razdvojite reducere za složenu logiku: Za složenu logiku stanja, razmislite o razdvajanju vašeg reducera na manje, lakše upravljive funkcije. To poboljšava čitljivost i održivost. Također možete grupirati povezane akcije unutar određenog dijela reducer funkcije.
- Testirajte svoje reducere: Napišite jedinične testove za svoje reducere kako biste osigurali da ispravno obrađuju različite akcije i početna stanja. To je ključno za osiguravanje kvalitete koda i sprječavanje regresija. Testovi bi trebali pokriti sve moguće scenarije promjena stanja.
- Razmotrite optimizaciju performansi: Ako su vaša ažuriranja stanja računski zahtjevna ili pokreću česta ponovna renderiranja, koristite tehnike memoizacije poput
useMemo
za optimizaciju performansi vaših komponenti. - Dokumentacija: Pružite jasnu dokumentaciju o stanju, akcijama i svrsi vašeg reducera. To pomaže drugim programerima da razumiju i održavaju vaš kod.
Zaključak
Hook useReducer
je moćan i svestran alat za upravljanje složenim stanjem u React aplikacijama. Nudi brojne prednosti, uključujući centraliziranu logiku stanja, poboljšanu organizaciju koda i poboljšanu mogućnost testiranja. Slijedeći najbolje prakse i razumijevajući njegove osnovne koncepte, možete iskoristiti useReducer
za izgradnju robusnijih, održivijih i učinkovitijih React aplikacija. Ovaj uzorak vas osnažuje da se učinkovito nosite s izazovima upravljanja složenim stanjem, omogućujući vam izgradnju aplikacija spremnih za globalno tržište koje pružaju besprijekorno korisničko iskustvo diljem svijeta.
Kako dublje ulazite u razvoj s Reactom, uključivanje useReducer
uzorka u vaš set alata nedvojbeno će dovesti do čišćih, skalabilnijih i lakše održivih kodnih baza. Zapamtite da uvijek uzimate u obzir specifične potrebe vaše aplikacije i odabirete najbolji pristup upravljanju stanjem za svaku situaciju. Sretno kodiranje!