Fiszki Online Redux (Preview)
Darmowy podgląd 15 z 45 dostępnych pytań
Podstawy Redux
Jakie są trzy fundamentalne zasady Redux (Three Principles)?
Odpowiedź w 30 sekund: Trzy zasady Redux to: (1) jedno źródło prawdy — cały stan aplikacji w jednym store, (2) stan jest tylko do odczytu — można go zmienić wyłącznie przez dispatchowanie akcji, (3) zmiany są dokonywane przez czyste funkcje (reduktory), które przyjmują poprzedni stan i akcję, a zwracają nowy stan.
Odpowiedź w 2 minuty: Pierwsza zasada — Single Source of Truth (Jedno źródło prawdy) — mówi, że cały stan aplikacji jest przechowywany w drzewie obiektów wewnątrz jednego store. To radykalnie upraszcza debugowanie i pozwala na implementację funkcji takich jak undo/redo, persystencja stanu czy serwerowe renderowanie. Wcześniej w aplikacjach typu MVC stan był rozproszony między wieloma kontrolerami i widokami, co utrudniało śledzenie zmian.
Druga zasada — State is Read-Only (Stan jest tylko do odczytu) — oznacza, że jedynym sposobem zmiany stanu jest wyemitowanie akcji, czyli obiektu opisującego co się stało. Komponenty nie mogą bezpośrednio modyfikować stanu — muszą wywołać dispatch z odpowiednią akcją. Dzięki temu mamy pełną kontrolę nad tym, kto, kiedy i dlaczego zmienia stan. Wszystkie zmiany są scentralizowane i wykonywane w ścisłej kolejności, więc nie musimy się martwić o race conditions.
Trzecia zasada — Changes are Made with Pure Functions (Zmiany są dokonywane przez czyste funkcje) — definiuje, że reduktory to czyste funkcje, które przyjmują poprzedni stan i akcję, a zwracają nowy stan. Czyste znaczy: brak efektów ubocznych, brak mutacji argumentów, brak wywołań API, brak Math.random() czy Date.now(). Ta sama akcja na tym samym stanie zawsze daje ten sam wynik. To kluczowe dla przewidywalności, testowalności i możliwości time-travel debuggingu.
Te trzy zasady razem tworzą fundament, na którym opiera się przewidywalność Redux. Mark Erikson często podkreśla, że ich naruszenie (np. mutacja stanu w reduktorze) jest najczęstszym źródłem trudnych do wykrycia błędów w aplikacjach Redux.
Przykład kodu:
// Zasada 1: Jedno źródło prawdy - cały stan w jednym obiekcie
const initialState = {
user: { name: 'Anna', loggedIn: true },
todos: [
{ id: 1, text: 'Nauczyć się Redux', completed: false }
],
filter: 'all'
};
// Zasada 2: Stan tylko do odczytu - zmieniamy go przez akcje
const addTodoAction = {
type: 'todos/added',
payload: { id: 2, text: 'Napisać testy', completed: false }
};
// Zasada 3: Czyste funkcje - reduktor nie mutuje, zwraca nowy obiekt
function todosReducer(state = initialState, action) {
switch (action.type) {
case 'todos/added':
// ŹLE: state.todos.push(action.payload) - to mutacja!
// DOBRZE: zwracamy nowy obiekt z nową tablicą
return {
...state,
todos: [...state.todos, action.payload]
};
case 'todos/toggled':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
default:
return state;
}
}
Materiały
↑ Powrót na góręCzym jest Redux i jaki problem rozwiązuje w aplikacjach JavaScript?
Odpowiedź w 30 sekund: Redux to przewidywalny kontener stanu dla aplikacji JavaScript, który centralizuje stan aplikacji w jednym miejscu (store). Rozwiązuje problem zarządzania stanem współdzielonym między wieloma komponentami, eliminując chaos związany z przekazywaniem propsów przez wiele poziomów (prop drilling) i nieprzewidywalnymi mutacjami stanu.
Odpowiedź w 2 minuty: Redux został stworzony przez Dana Abramova w 2015 roku jako odpowiedź na rosnącą złożoność zarządzania stanem w dużych aplikacjach SPA, szczególnie tych zbudowanych w React. W aplikacjach tego typu stan jest często rozproszony między wieloma komponentami, co prowadzi do trudnych do debugowania błędów — szczególnie gdy ten sam fragment danych musi być dostępny w odległych częściach drzewa komponentów.
Redux rozwiązuje kilka konkretnych problemów. Po pierwsze, eliminuje prop drilling, czyli konieczność przekazywania danych przez kolejne poziomy komponentów, które same z tych danych nie korzystają. Po drugie, wprowadza jasny i przewidywalny przepływ danych — każda zmiana stanu musi przejść przez akcję (action) i czysty reduktor (reducer), co eliminuje ukryte mutacje. Po trzecie, ułatwia debugowanie dzięki narzędziom takim jak Redux DevTools, które pozwalają na time-travel debugging — cofanie się i przewijanie kolejnych stanów aplikacji.
Warto pamiętać, że Redux to nie tylko biblioteka, ale przede wszystkim wzorzec architektoniczny inspirowany Fluxem od Facebooka oraz elementami funkcyjnymi z języka Elm. Współcześnie zaleca się używanie Redux Toolkit (RTK), który znacznie redukuje boilerplate i jest oficjalnie rekomendowanym sposobem pisania logiki Redux.
Redux nie jest jednak rozwiązaniem dla każdego — w mniejszych aplikacjach często wystarczy useState, useReducer lub Context API. Mark Erikson, główny maintainer Redux, sam wielokrotnie powtarzał, że Redux powinien być używany tylko wtedy, gdy faktycznie rozwiązuje konkretny problem.
Przykład kodu:
import { createStore } from 'redux';
// Reduktor - czysta funkcja opisująca jak stan zmienia się w odpowiedzi na akcję
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case 'counter/incremented':
// Zwracamy nowy obiekt stanu, nigdy nie mutujemy istniejącego
return { value: state.value + 1 };
case 'counter/decremented':
return { value: state.value - 1 };
default:
return state;
}
}
// Tworzymy store - centralne miejsce przechowywania stanu
const store = createStore(counterReducer);
// Subskrybujemy zmiany - funkcja zostanie wywołana po każdej akcji
store.subscribe(() => console.log('Nowy stan:', store.getState()));
// Wywołujemy akcje - jedyny sposób na zmianę stanu w Redux
store.dispatch({ type: 'counter/incremented' }); // { value: 1 }
store.dispatch({ type: 'counter/incremented' }); // { value: 2 }
store.dispatch({ type: 'counter/decremented' }); // { value: 1 }
Materiały
- Redux - Getting Started
- Motivation - Redux Docs
- Mark Erikson - Idiomatic Redux: The History and Implementation
Czym jest jednokierunkowy przepływ danych (unidirectional data flow) w Redux?
Odpowiedź w 30 sekund: Unidirectional data flow oznacza, że dane w Redux płyną zawsze w jednym kierunku: View dispatchuje Action, Action trafia do Reducera, Reducer tworzy nowy State w Store, a Store powiadamia View o zmianie. Ten ścisły cykl gwarantuje przewidywalność i eliminuje bidirectional binding znany z frameworków takich jak Angular 1.x.
Odpowiedź w 2 minuty: Jednokierunkowy przepływ danych to architektoniczny wzorzec, w którym dane w aplikacji poruszają się tylko w jednym kierunku przez wyraźnie zdefiniowane etapy. W Redux ten cykl wygląda następująco: użytkownik wchodzi w interakcję z UI (np. klika przycisk), co powoduje dispatchowanie akcji. Akcja to zwykły obiekt JavaScript opisujący "co się stało". Następnie store przekazuje aktualny stan i akcję do reduktora, który zwraca nowy stan. Po aktualizacji store powiadamia wszystkich subskrybentów (np. komponenty React poprzez react-redux), które re-renderują się z nowymi danymi.
Kluczową cechą tego wzorca jest brak skrótów. Nie można "po cichu" zmienić stanu z poziomu komponentu — każda zmiana musi przejść przez pełny cykl. To rozwiązuje klasyczny problem dwukierunkowego bindingu, gdzie zmiana w jednym widoku mogła kaskadowo wpływać na inne widoki w nieprzewidywalny sposób, co było bolączką AngularJS 1.x.
Praktyczne korzyści unidirectional data flow to: łatwiejsze debugowanie (zawsze wiadomo, skąd wzięła się zmiana stanu), możliwość time-travel debuggingu (każdą zmianę można odtworzyć), prostsze testowanie (reduktory to czyste funkcje) oraz lepsza wydajność (możliwość selektywnego renderowania na podstawie zmian w store).
Warto zauważyć, że Redux nie wymyślił tego wzorca — wywodzi się on z architektury Flux od Facebooka oraz języka Elm. Jednak Redux znacząco uprościł implementację, redukując ją do jednego store i czystych reduktorów.
flowchart LR
A[View<br/>Komponent UI] -->|1. dispatch action| B[Action<br/>type: 'todo/added']
B -->|2. przekazana do| C[Reducer<br/>czysta funkcja]
C -->|3. zwraca nowy state| D[Store<br/>centralny stan]
D -->|4. notify subscribers| A
style A fill:#61DAFB,stroke:#000,color:#000
style B fill:#FFD93D,stroke:#000,color:#000
style C fill:#6BCB77,stroke:#000,color:#000
style D fill:#764ABC,stroke:#000,color:#fff
Przykład kodu:
import { createStore } from 'redux';
// 1. Reducer - czysta funkcja zmieniająca stan w odpowiedzi na akcje
function todosReducer(state = [], action) {
switch (action.type) {
case 'todo/added':
// Zwracamy nową tablicę zamiast mutować istniejącą
return [...state, { id: Date.now(), text: action.payload }];
default:
return state;
}
}
// 2. Store - przechowuje stan i koordynuje przepływ
const store = createStore(todosReducer);
// 3. View - subskrybuje zmiany stanu (symulacja komponentu)
store.subscribe(() => {
console.log('View renderuje się z nowym stanem:', store.getState());
});
// 4. User interaction - dispatch akcji uruchamia cały cykl
// View -> Action -> Reducer -> Store -> View
store.dispatch({ type: 'todo/added', payload: 'Nauczyć się jednokierunkowego przepływu' });
// Output: View renderuje się z nowym stanem: [{ id: ..., text: '...' }]
store.dispatch({ type: 'todo/added', payload: 'Zrozumieć dlaczego to przewidywalne' });
// Cykl powtarza się - znowu pełna droga przez wszystkie etapy
Materiały
- Data Flow - Redux Docs
- Redux Fundamentals - One-Way Data Flow
- Mark Erikson - The Tao of Redux, Part 2: Practice and Philosophy
Kiedy warto użyć Redux, a kiedy wystarczy lokalny state komponentu?
Odpowiedź w 30 sekund: Redux warto użyć, gdy stan jest współdzielony między wieloma niezwiązanymi komponentami, gdy logika aktualizacji stanu jest złożona, gdy potrzebujesz pełnej historii zmian (debugowanie, undo/redo) lub gdy aplikacja jest duża. Dla prostych formularzy, lokalnych stanów UI czy małych aplikacji wystarczy useState/useReducer w komponencie.
Odpowiedź w 2 minuty: Wybór między Redux a lokalnym stanem to klasyczny przykład decyzji architektonicznej, którą należy podejmować świadomie. Sam Dan Abramov w słynnym artykule "You Might Not Need Redux" przestrzegał przed dodawaniem Redux "na wszelki wypadek". Z kolei Mark Erikson w "When (and when not) to Reach for Redux" przedstawił konkretne kryteria, kiedy Redux faktycznie się opłaca.
Użyj Redux, gdy:
- masz znaczne ilości stanu aplikacji potrzebnego w wielu miejscach aplikacji,
- stan jest często aktualizowany,
- logika aktualizacji stanu może być skomplikowana,
- aplikacja ma średnią lub dużą bazę kodu i pracuje nad nią wielu programistów,
- chcesz mieć podgląd jak stan jest aktualizowany w czasie (time-travel debugging),
- musisz synchronizować stan między wieloma drzewami komponentów (np. modal, sidebar, lista),
- chcesz cache'ować dane z API w sposób przewidywalny (chociaż tu dziś polecane jest RTK Query).
Wystarczy lokalny stan, gdy:
- stan dotyczy tylko jednego komponentu lub jego dzieci (np. open/closed dropdown, wartość inputa),
- aplikacja jest mała i prosta,
- nie potrzebujesz historii zmian ani zaawansowanego debugowania,
- nie współdzielisz stanu między odległymi częściami drzewa komponentów.
Warto pamiętać, że to nie jest decyzja "wszystko albo nic". W praktyce większość aplikacji używa kombinacji: globalny stan w Redux (dane użytkownika, autoryzacja, cache API), a lokalny stan w komponentach (formularze, stany UI, hover'y). Również Context API może być dobrym rozwiązaniem dla rzeczy zmieniających się rzadko (np. theme, język).
Współcześnie, dzięki Redux Toolkit (RTK) i RTK Query, próg wejścia w Redux jest znacznie niższy niż kiedyś — boilerplate jest minimalny, więc argument "Redux to za dużo kodu" stracił na sile.
Przykład kodu:
// === PRZYKŁAD 1: Lokalny state - WYSTARCZAJĄCY ===
// Stan dotyczy tylko tego komponentu - useState w zupełności wystarczy
function DropdownMenu() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Menu</button>
{isOpen && <ul><li>Opcja 1</li><li>Opcja 2</li></ul>}
</div>
);
}
// === PRZYKŁAD 2: Redux - UZASADNIONY ===
// Dane użytkownika potrzebne w wielu miejscach aplikacji
// (Header, Sidebar, Profile, Settings, Checkout...)
import { createSlice } from '@reduxjs/toolkit';
const userSlice = createSlice({
name: 'user',
initialState: { name: null, email: null, isLoggedIn: false },
reducers: {
loggedIn: (state, action) => {
// RTK używa Immer pod spodem - możemy "mutować" stan
state.name = action.payload.name;
state.email = action.payload.email;
state.isLoggedIn = true;
},
loggedOut: (state) => {
state.name = null;
state.email = null;
state.isLoggedIn = false;
}
}
});
// Header czyta z Redux
function Header() {
const user = useSelector(state => state.user);
return <div>Witaj, {user.name}!</div>;
}
// Profile też czyta z tego samego store - jeden źródło prawdy
function Profile() {
const user = useSelector(state => state.user);
return <div>Email: {user.email}</div>;
}
Materiały
- Mark Erikson - When (and when not) to Reach for Redux
- Dan Abramov - You Might Not Need Redux
- Redux Toolkit - Why Redux Toolkit is How To Use Redux Today
Czym różni się Redux od Context API w React?
Odpowiedź w 30 sekund: Context API to mechanizm Reacta do przekazywania danych przez drzewo komponentów bez prop drillingu — nie jest narzędziem do zarządzania stanem. Redux to pełnoprawne rozwiązanie do zarządzania stanem z reduktorami, middleware, DevTools, optymalizacją renderowania i przewidywalnym przepływem danych. Context świetnie sprawdza się przy rzadko zmieniających się danych (theme, język), Redux przy złożonej, dynamicznej logice biznesowej.
Odpowiedź w 2 minuty: To jedno z najczęstszych nieporozumień we współczesnym React. Mark Erikson napisał o tym osobny artykuł "Blogged Answers: Why React Context is Not a 'State Management' Tool (and Why It Doesn't Replace Redux)", w którym jasno tłumaczy różnicę.
Context API to mechanizm dependency injection w React. Pozwala udostępnić wartość (cokolwiek to jest) wszystkim komponentom poniżej w drzewie bez przekazywania jej przez propsy. To wszystko. Context sam w sobie nie zarządza stanem, nie wymusza wzorca aktualizacji, nie ma optymalizacji renderowania, nie ma DevTools. Często łączy się go z useReducer, żeby uzyskać namiastkę zarządzania stanem.
Redux to kompletna biblioteka zarządzania stanem oparta na czystych reduktorach, akcjach i scentralizowanym store. Oferuje middleware (np. thunk, saga), Redux DevTools z time-travel debuggingiem, selektory z memoizacją (reselect), optymalizację re-renderów poprzez react-redux, a w wersji RTK Query — także cache'owanie zapytań API.
Kluczowe różnice praktyczne:
- Re-renderowanie: gdy wartość w Context się zmieni, wszystkie konsumujące komponenty re-renderują się — nawet jeśli używają tylko fragmentu tej wartości. Redux z react-redux używa shallow comparison i renderuje tylko komponenty, których wybrany fragment stanu faktycznie się zmienił.
- Middleware: Context nie ma żadnego sposobu na przechwytywanie akcji. Redux ma rozbudowany ekosystem middleware do logowania, async operacji, persystencji itd.
- DevTools: Redux DevTools pozwala na cofanie czasu, eksport/import stanu, śledzenie akcji. Context tego nie ma.
- Boilerplate: Context jest bardziej zwięzły dla prostych przypadków. RTK znacznie zredukował boilerplate Redux.
Tabela porównawcza:
| Cecha | Redux | Context API |
|---|---|---|
| Typ narzędzia | Biblioteka zarządzania stanem | Mechanizm dependency injection |
| Przeznaczenie | Złożona logika biznesowa, globalny stan | Przekazywanie wartości bez prop drillingu |
| Wzorzec aktualizacji | Wymuszony: action → reducer → store | Brak — dowolna wartość |
| Optymalizacja re-renderów | Tak (shallow compare w useSelector) | Nie — re-render wszystkich konsumentów |
| Middleware | Tak (thunk, saga, RTK Query) | Brak |
| DevTools | Redux DevTools (time-travel) | Brak dedykowanych |
| Async logic | Wbudowane w RTK (createAsyncThunk, RTK Query) | Trzeba zaimplementować ręcznie |
| Memoizacja selektorów | Reselect, RTK createSelector | Brak |
| Idealne dla | Częste aktualizacje, złożona logika, duże aplikacje | Theme, język, zalogowany user (rzadko zmienne) |
| Krzywa uczenia | Wyższa (akcje, reduktory, middleware) | Niska (Provider + useContext) |
| Boilerplate (z RTK) | Mały | Bardzo mały |
W praktyce te narzędzia często się uzupełniają. Redux zarządza złożonym stanem aplikacji, a Context dostarcza rzeczy specyficzne dla poddrzewa (np. theme w obrębie sekcji, formularz w obrębie modala).
Przykład kodu:
// === CONTEXT API - dobre dla rzadko zmieniających się danych ===
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('light');
// Theme zmienia się rzadko - Context jest idealny
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
);
}
function ThemedButton() {
// Prosty hook konsumujący - bez boilerplate
const { theme, setTheme } = useContext(ThemeContext);
return <button className={theme}>Przełącz motyw</button>;
}
// === REDUX (z RTK) - dobre dla często zmieniających się danych ===
import { createSlice, configureStore } from '@reduxjs/toolkit';
import { useSelector, useDispatch, Provider } from 'react-redux';
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [], total: 0 },
reducers: {
itemAdded: (state, action) => {
// Koszyk zmienia się często - Redux daje nam optymalizację
state.items.push(action.payload);
state.total += action.payload.price;
}
}
});
const store = configureStore({ reducer: { cart: cartSlice.reducer } });
function CartCounter() {
// useSelector używa shallow compare - re-render tylko gdy items.length się zmieni
const itemCount = useSelector(state => state.cart.items.length);
return <span>Pozycje w koszyku: {itemCount}</span>;
}
function CartTotal() {
// Ten komponent re-renderuje się TYLKO gdy total się zmieni,
// niezależnie od zmian w items
const total = useSelector(state => state.cart.total);
return <span>Suma: {total} zł</span>;
}
Materiały
- Mark Erikson - Why React Context is Not a "State Management" Tool
- Redux FAQ - When should I use Redux?
- Redux Toolkit - Official Documentation
Actions i Action Creators
Czym jest Action w Redux i jaką ma strukturę?
Odpowiedź w 30 sekund:
Action to zwykły obiekt JavaScript opisujący, co się wydarzyło w aplikacji. Jest jedynym źródłem informacji dla store i musi zawierać pole type identyfikujące rodzaj zdarzenia. Opcjonalnie może przenosić dodatkowe dane potrzebne do aktualizacji stanu.
Odpowiedź w 2 minuty:
Action w Redux to plain object, który reprezentuje intencję zmiany stanu. Zgodnie z oficjalną dokumentacją Redux, akcje są "the only source of information for the store" — store nigdy nie jest modyfikowany bezpośrednio, zawsze za pośrednictwem dispatchowanych akcji. Każdy action musi mieć pole type (zwykle string), które jednoznacznie opisuje, co się stało, np. 'todos/todoAdded'.
Poza polem type action może zawierać dowolne inne pola przenoszące dane. Konwencja zaleca, aby dodatkowe informacje umieszczać w polu payload, co poprawia czytelność i ułatwia tworzenie generycznego middleware. Akcje powinny być serializowalne — nie powinny zawierać funkcji, klas, instancji Promise ani innych nieserializowalnych wartości, ponieważ uniemożliwia to działanie Redux DevTools (time-travel debugging) oraz persystencji stanu.
W praktyce akcje opisują "co się wydarzyło" (np. userLoggedIn), a nie "jak zmienić stan" — logika zmiany stanu należy do reducerów. Konwencja nazewnicza zalecana przez Redux Style Guide to feature/eventName w formie czasu przeszłego, np. cart/itemAdded.
Przykład kodu:
// Najprostszy action - tylko type
const incrementAction = { type: 'counter/incremented' };
// Action z payloadem
const addTodoAction = {
type: 'todos/todoAdded',
payload: {
id: 1,
text: 'Nauczyć się Redux',
completed: false
}
};
// Action z metadanymi i flagą błędu (zgodne z FSA)
const fetchUserFailed = {
type: 'users/fetchFailed',
payload: new Error('Network error'),
error: true,
meta: { timestamp: Date.now() }
};
// Dispatchowanie akcji
store.dispatch(incrementAction);
store.dispatch(addTodoAction);
Materiały
↑ Powrót na góręRedux Toolkit (RTK)
Czym jest Redux Toolkit i dlaczego jest rekomendowany?
Odpowiedź w 30 sekund:
Redux Toolkit (RTK) to oficjalna, rekomendowana biblioteka do pisania logiki Redux. Eliminuje boilerplate, zawiera wbudowany Immer (mutowalne aktualizacje), Redux Thunk oraz narzędzia takie jak createSlice, configureStore i createAsyncThunk. Jest oficjalnie rekomendowany przez zespół Redux od 2019 roku.
Odpowiedź w 2 minuty: Klasyczny Redux słusznie krytykowano za nadmiar kodu szablonowego: ręcznie definiowane stałe akcji, action creators, switch-case w reducerach, konfiguracja middleware oraz DevTools. Redux Toolkit powstał, żeby ten ból wyeliminować, zachowując jednocześnie wszystkie zalety Reduxa - przewidywalność, możliwość debugowania w czasie podróży i podejście single source of truth.
RTK dostarcza zestaw narzędzi: configureStore (konfiguruje store z dobrymi domyślnymi ustawieniami, w tym Redux DevTools i thunk middleware), createSlice (generuje reducer i action creators z jednej definicji), createAsyncThunk (obsługa operacji asynchronicznych), createEntityAdapter (normalizacja kolekcji danych) oraz createSelector (memoizowane selektory z Reselect).
Dodatkowo, RTK zawiera RTK Query - rozszerzenie do data fetching i cachowania, które jest alternatywą dla bibliotek takich jak React Query czy SWR. RTK Query automatycznie generuje hooki React (np. useGetPostsQuery), zarządza cachowaniem, deduplikacją zapytań i invalidacją.
Dzięki użyciu Immer pod spodem, reducery mogą "mutować" stan bezpośrednio - co znacząco upraszcza kod, zwłaszcza przy zagnieżdżonych strukturach. RTK jest napisany w TypeScript i oferuje świetne wsparcie typów.
Przykład kodu:
// Klasyczny Redux - dużo boilerplate
const INCREMENT = 'counter/increment';
const increment = () => ({ type: INCREMENT });
const counterReducer = (state = { value: 0 }, action) => {
switch (action.type) {
case INCREMENT:
return { ...state, value: state.value + 1 };
default:
return state;
}
};
// Redux Toolkit - znacznie krócej
import { createSlice, configureStore } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
// Można "mutować" stan dzięki Immer
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; }
}
});
// Akcje są generowane automatycznie
export const { increment, decrement } = counterSlice.actions;
// Konfiguracja store w jednej linii
export const store = configureStore({
reducer: { counter: counterSlice.reducer }
});
Materiały
↑ Powrót na góręReact-Redux
Jak unikać niepotrzebnych renderowań przy użyciu useSelector?
Odpowiedź w 30 sekund:
useSelector rerenderuje komponent gdy zwrócona wartość zmieni się referencyjnie (===). Najczęstsze pułapki: tworzenie nowych obiektów/tablic w selektorze (każdy render = nowa referencja), brak memoizacji drogich obliczeń. Rozwiązania: selekcja prymitywów, shallowEqual, memoizowane selektory (createSelector).
Odpowiedź w 2 minuty:
Mechanizm rerenderów useSelector: Po każdej akcji hook wywołuje selektor i porównuje wynik z poprzednim za pomocą === (strict equality). Jeśli wartości są różne - komponent się rerenderuje. To prosty mechanizm, ale łatwo go nieświadomie złamać.
Antywzorzec 1: Tworzenie nowych obiektów/tablic w selektorze
// ZLE - każde wywołanie zwraca nowy obiekt, rerender przy KAZDEJ akcji
const user = useSelector(state => ({
name: state.user.name,
email: state.user.email,
}));
// ZLE - .filter() / .map() tworzą nową tablicę
const activeUsers = useSelector(state =>
state.users.filter(u => u.isActive)
);
Antywzorzec 2: Brak memoizacji drogich obliczeń
// ZLE - sortowanie przy każdym wywołaniu selektora (po każdej akcji!)
const sorted = useSelector(state =>
[...state.items].sort((a, b) => b.price - a.price)
);
Antywzorzec 3: Selekcja zbyt dużego wycinka
// ZLE - rerender przy zmianie czegokolwiek w state.user
const user = useSelector(state => state.user);
return <div>{user.name}</div>; // używamy tylko name!
Rozwiązania:
1. Selekcja prymitywów (najprostsze)
// Prymitywy mają stabilną tożsamość - ten sam string === ten sam string
const name = useSelector(state => state.user.name);
const email = useSelector(state => state.user.email);
2. shallowEqual jako equalityFn
import { shallowEqual } from 'react-redux';
// Porównanie płytkie - obiekt jest "ten sam" jeśli wszystkie wartości === poprzednim
const user = useSelector(
state => ({ name: state.user.name, email: state.user.email }),
shallowEqual
);
3. Memoizowane selektory z createSelector (dla drogich obliczeń)
import { createSelector } from '@reduxjs/toolkit';
// Przelicza tylko gdy zmieni się state.items LUB state.filter
const selectFilteredSorted = createSelector(
[(state: RootState) => state.items, (state: RootState) => state.filter],
(items, filter) => items
.filter(i => i.category === filter)
.sort((a, b) => b.price - a.price)
);
4. Wiele małych useSelector zamiast jednego dużego - każdy ma własną subskrypcję, niezależne rerendery.
5. React.memo dla komponentów konsumujących propsy - jeśli komponent dostaje wartość z Redux i przekazuje ją dalej.
Debugowanie: Użyj React DevTools Profiler lub why-did-you-render, by zidentyfikować komponenty rerenderujące się bez potrzeby. Częstym znakiem problemu jest komponent rerenderujący się przy AKAZDEJ akcji - to klasyczna oznaka tworzenia nowej referencji w selektorze.
Przykład kodu:
import { shallowEqual } from 'react-redux';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from './hooks';
// === ZLE - antywzorce ===
function BadUserCard() {
// Antywzorzec 1: nowy obiekt przy każdym renderze
const user = useAppSelector(state => ({
name: state.user.name,
age: state.user.age,
}));
// Rerender przy każdej akcji w aplikacji!
return <div>{user.name}, {user.age}</div>;
}
function BadProductList() {
// Antywzorzec 2: filter tworzy nową tablicę
const products = useAppSelector(state =>
state.products.filter(p => p.inStock)
);
// Rerender nawet gdy products w ogóle się nie zmienił
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
// === DOBRZE - rozwiązania ===
// 1. Prymitywy - najprostsze, najbezpieczniejsze
function GoodUserCard() {
const name = useAppSelector(state => state.user.name);
const age = useAppSelector(state => state.user.age);
return <div>{name}, {age}</div>;
}
// 2. shallowEqual gdy potrzebujemy obiektu
function GoodUserCardObject() {
const user = useAppSelector(
state => ({ name: state.user.name, age: state.user.age }),
shallowEqual // porównanie płytkie - tylko klucze pierwszego poziomu
);
return <div>{user.name}, {user.age}</div>;
}
// 3. Memoizowany selektor dla list/transformacji
const selectInStockProducts = createSelector(
[(state: RootState) => state.products],
(products) => products.filter(p => p.inStock)
// Zwraca tę samą referencję dopóki state.products się nie zmieni
);
function GoodProductList() {
const products = useAppSelector(selectInStockProducts);
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
Materiały
↑ Powrót na góręSelektory i Reselect
Czym jest memoizacja selektorów i jakie daje korzyści wydajnościowe?
Odpowiedź w 30 sekund: Memoizacja to technika cache'owania wyniku funkcji na podstawie jej argumentów - jeśli argumenty się nie zmieniły, zwracany jest poprzedni wynik bez ponownych obliczeń. W selektorach Redux memoizacja zapobiega kosztownym przeliczeniom (np. filtrowaniu, sortowaniu, mapowaniu) oraz - co ważniejsze - zapobiega niepotrzebnym re-renderom komponentów, ponieważ zwracana jest ta sama referencja.
Odpowiedź w 2 minuty:
W React-Redux useSelector po każdej akcji wywołuje przekazany selektor i porównuje wynik z poprzednim za pomocą === (referencyjne porównanie). Jeśli wynik jest "nowy" (inna referencja), komponent się przerenderuje. Problem pojawia się przy selektorach zwracających nowe obiekty/tablice na każde wywołanie:
useSelector(state => state.todos.filter(t => !t.done)) // zła praktyka!
filter zawsze tworzy nową tablicę, więc nawet gdy state.todos się nie zmienia, komponent będzie się renderował po każdej akcji w aplikacji. Memoizacja rozwiązuje ten problem - memoizowany selektor zwraca tę samą referencję dopóki dane wejściowe nie ulegną zmianie.
Drugą korzyścią jest oszczędność obliczeń. Jeśli selektor wykonuje kosztowne operacje (sortowanie tysięcy elementów, agregacje, transformacje), memoizacja sprawia że obliczenie wykona się tylko raz, a kolejne odczyty są O(1). Selektor "z pamięcią" porównuje input selectory operatorem === (domyślnie) - jeśli wszystkie są takie same jak poprzednio, zwracany jest cache'owany wynik.
Warto pamiętać, że nie wszystko warto memoizować. Selektor typu state => state.user.name to po prostu odczyt - referencja jest stabilna z definicji niemutowalnego stanu. Memoizacja ma sens przy wyliczeniach (filter, map, reduce, sort) lub tworzeniu nowych struktur danych.
Przykład kodu:
import { createSelector } from '@reduxjs/toolkit';
// PROBLEM - bez memoizacji
const selectExpensiveData = (state) => {
console.log('Drogie obliczenie!');
return state.items
.filter(i => i.active)
.sort((a, b) => b.score - a.score)
.map(i => ({ ...i, displayName: `${i.name} (${i.score})` }));
};
// Każdy useSelector(selectExpensiveData) wywoła się po każdej akcji,
// nawet niezwiązanej z items - i zwróci NOWĄ tablicę → re-render!
// ROZWIĄZANIE - memoizacja przez Reselect
const selectItems = (state) => state.items;
const selectActiveSortedItems = createSelector(
[selectItems],
(items) => {
console.log('Drogie obliczenie - tylko gdy items się zmienią');
return items
.filter(i => i.active)
.sort((a, b) => b.score - a.score)
.map(i => ({ ...i, displayName: `${i.name} (${i.score})` }));
}
);
// Komponent
const items = useSelector(selectActiveSortedItems);
// Bez re-renderów dopóki state.items się nie zmieni!
// Sprawdzanie statystyk cache (przydatne w debugowaniu)
console.log(selectActiveSortedItems.recomputations()); // ile razy wyliczono
console.log(selectActiveSortedItems.dependencyRecomputations()); // ile razy zmieniły się inputy
Materiały
↑ Powrót na góręStore
Czym jest Store w Redux i jaką pełni rolę?
Odpowiedź w 30 sekund:
Store to centralny obiekt w Redux, który przechowuje cały stan aplikacji w jednym miejscu (single source of truth). Pełni rolę kontenera stanu, umożliwia jego odczyt przez getState(), modyfikację przez dispatch(action) oraz subskrypcję zmian przez subscribe(listener).
Odpowiedź w 2 minuty: Store w Redux to obiekt łączący w sobie trzy kluczowe elementy architektury: aktualny stan aplikacji, reduktor (lub korzeń drzewa reduktorów) odpowiedzialny za jego aktualizację oraz mechanizm powiadamiania subskrybentów o zmianach. Stanowi serce wzorca Flux/Redux i jest jedynym źródłem prawdy o stanie aplikacji.
Główną rolą Store jest centralizacja zarządzania stanem. Zamiast rozsianych po komponentach kawałków stanu, mamy jeden obiekt, do którego można uzyskać dostęp z dowolnego miejsca aplikacji. Dzięki temu łatwiej debugować aplikację (znamy każdą zmianę stanu), implementować funkcje typu undo/redo, a także persystować stan między sesjami.
Store wymusza też jednokierunkowy przepływ danych: stan jest tylko do odczytu, a jego modyfikacja możliwa jest wyłącznie poprzez wysłanie akcji (dispatch). To eliminuje wiele problemów typowych dla aplikacji z dwukierunkowym wiązaniem danych - łatwiej przewidzieć skutki zmian i śledzić, kto i kiedy zmodyfikował stan.
W praktyce z Store komunikujemy się głównie pośrednio - przez bibliotekę React Redux (hooki useSelector, useDispatch), która abstrahuje bezpośrednie wywołania metod Store.
Przykład kodu:
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';
// Tworzymy Store - centralny kontener stanu aplikacji
const store = configureStore({
reducer: {
counter: counterReducer,
},
});
// Store przechowuje stan i udostępnia metody do pracy z nim
console.log(store.getState()); // { counter: { value: 0 } }
// Wysłanie akcji - jedyny sposób na zmianę stanu
store.dispatch({ type: 'counter/increment' });
// Subskrypcja zmian - reaguje na każdą aktualizację stanu
const unsubscribe = store.subscribe(() => {
console.log('Stan się zmienił:', store.getState());
});
// Anulowanie subskrypcji, gdy już jej nie potrzebujemy
unsubscribe();
Materiały
↑ Powrót na góręJak utworzyć Store za pomocą createStore i configureStore?
Odpowiedź w 30 sekund:
createStore to historyczna funkcja Redux Core (obecnie deprecated od Redux Toolkit 1.x), tworząca Store ze zwykłego reduktora. configureStore z Redux Toolkit to zalecany sposób - automatycznie konfiguruje middleware (thunk, immutability check), DevTools i obsługuje combineReducers w polu reducer.
Odpowiedź w 2 minuty:
W klasycznym Redux Store tworzyło się funkcją createStore(reducer, preloadedState, enhancer) z pakietu redux. Wymagało to ręcznej konfiguracji middleware (przez applyMiddleware), integracji z Redux DevTools (przez composeWithDevTools) oraz ręcznego łączenia reduktorów (combineReducers). To podejście, choć działa, prowadzi do dużej ilości boilerplate'u i łatwo o pomyłki konfiguracyjne.
Od czasu wprowadzenia Redux Toolkit (RTK), oficjalnie zalecanym sposobem jest funkcja configureStore z pakietu @reduxjs/toolkit. Ona robi wszystko za nas: dodaje domyślny middleware (Redux Thunk dla operacji asynchronicznych, sprawdzanie niezmienialności stanu, wykrywanie nieserializowalnych wartości w stanie/akcjach), włącza integrację z Redux DevTools w trybie deweloperskim, automatycznie wywołuje combineReducers, gdy reducer jest obiektem.
Porównanie obu podejść:
| Cecha | createStore (legacy) | configureStore (RTK) |
|---|---|---|
| Pakiet | redux |
@reduxjs/toolkit |
| Status | Deprecated od RTK 1.x | Zalecany |
| Middleware | Ręcznie (applyMiddleware) | Automatycznie (thunk + checks) |
| DevTools | Ręcznie (composeWithDevTools) | Automatycznie |
| combineReducers | Ręcznie | Automatycznie z obiektu |
| Wykrywanie błędów | Brak | Immutability/serializability checks |
| Typowanie TS | Wymaga ręcznej pracy | Lepsze inferowanie typów |
| Boilerplate | Dużo | Minimalny |
W nowych projektach zawsze należy używać configureStore. Funkcja createStore została oznaczona jako deprecated nie dlatego, że przestaje działać, ale po to, by zachęcić deweloperów do migracji na RTK.
Przykład kodu:
// PODEJŚCIE LEGACY (createStore) - nie zalecane
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';
const rootReducer = combineReducers({
counter: counterReducer,
user: userReducer,
});
// Ręczna konfiguracja - dużo kodu szablonowego
const legacyStore = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunk))
);
// PODEJŚCIE NOWOCZESNE (configureStore z RTK) - zalecane
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: {
counter: counterReducer, // combineReducers wywołane automatycznie
user: userReducer,
},
// Thunk middleware, immutability check, serializability check
// oraz DevTools zostały dodane automatycznie
});
// Można rozszerzyć domyślny middleware o własne
const storeWithExtra = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(logger),
});
Materiały
- Redux Toolkit - configureStore
- Redux - createStore (deprecated)
- Why Redux Toolkit is How To Use Redux Today
Jakie metody udostępnia obiekt Store (getState, dispatch, subscribe)?
Odpowiedź w 30 sekund:
Store udostępnia trzy główne metody: getState() zwraca aktualny stan aplikacji, dispatch(action) wysyła akcję do reduktorów (jedyny sposób na zmianę stanu), subscribe(listener) rejestruje funkcję wywoływaną przy każdej zmianie stanu. Dodatkowo dostępne są replaceReducer() i [Symbol.observable]().
Odpowiedź w 2 minuty: Obiekt Store w Redux ma minimalne, lecz wystarczające API składające się głównie z trzech metod, które realizują wzorzec jednokierunkowego przepływu danych.
getState() to metoda zwracająca aktualny stan aplikacji - cały obiekt drzewa stanu. Jest synchroniczna i zwraca referencję do bieżącego stanu (nie kopię), dlatego nie wolno modyfikować zwróconego obiektu. Najczęściej używana wewnątrz reduktorów (do odczytu poprzedniego stanu) lub w kodzie middleware. W komponentach React zamiast getState używa się useSelector z biblioteki React Redux.
dispatch(action) to jedyny sposób na wywołanie zmiany stanu. Przyjmuje obiekt akcji (zawierający przynajmniej pole type) i przekazuje go przez stos middleware do reduktora głównego. Reduktor wylicza nowy stan, a Store zastępuje stary stan nowym. Po aktualizacji wszyscy subskrybenci są synchronicznie powiadamiani. Z middlewarem typu Thunk można dispatchować również funkcje.
subscribe(listener) rejestruje funkcję zwrotną wywoływaną po każdej aktualizacji stanu. Zwraca funkcję anulującą subskrypcję, którą należy wywołać, gdy listener nie jest już potrzebny - inaczej powstanie wyciek pamięci. Listener nie otrzymuje stanu jako argumentu - musi sam wywołać getState(). W praktyce subscribe jest używany przez biblioteki integracyjne (React Redux), a nie bezpośrednio w komponentach.
Dwie dodatkowe metody: replaceReducer(nextReducer) używana do code splittingu i hot reloadingu reduktorów, oraz [Symbol.observable]() umożliwiająca interop z bibliotekami reaktywnymi typu RxJS.
Przykład kodu:
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
addBy: (state, action) => { state.value += action.payload; },
},
});
const store = configureStore({ reducer: { counter: counterSlice.reducer } });
const { increment, addBy } = counterSlice.actions;
// 1. getState() - odczyt aktualnego stanu
console.log(store.getState()); // { counter: { value: 0 } }
// 2. subscribe() - rejestracja listenera zmian stanu
const unsubscribe = store.subscribe(() => {
// Listener nie otrzymuje stanu - musimy sami go pobrać
const currentState = store.getState();
console.log('Nowa wartość licznika:', currentState.counter.value);
});
// 3. dispatch() - wysłanie akcji zmieniającej stan
store.dispatch(increment()); // Listener loguje: 1
store.dispatch(addBy(5)); // Listener loguje: 6
// Pamiętaj o anulowaniu subskrypcji - inaczej wyciek pamięci!
unsubscribe();
store.dispatch(increment()); // Listener już nie reaguje
Materiały
↑ Powrót na góręReducers
Czym jest Reducer i jakie są zasady jego tworzenia?
Odpowiedź w 30 sekund:
Reducer to czysta funkcja o sygnaturze (state, action) => newState, która opisuje, jak stan aplikacji zmienia się w odpowiedzi na akcje. Musi być deterministyczna, nie może mutować stanu ani wykonywać efektów ubocznych, a w przypadku nieznanej akcji powinna zwrócić aktualny stan bez zmian.
Odpowiedź w 2 minuty:
Reducer w Redux jest sercem zarządzania stanem. Otrzymuje dwa argumenty: bieżący stan oraz akcję opisującą, co się wydarzyło, a zwraca nowy stan. Nazwa "reducer" pochodzi od funkcji Array.prototype.reduce - tak samo jak ona, redukuje sekwencję akcji do pojedynczej wartości stanu.
Podstawowe zasady tworzenia reducerów są ścisłe: muszą być czystymi funkcjami (te same wejścia zawsze dają te same wyjścia), nie mogą mutować przekazanego stanu, nie mogą wykonywać efektów ubocznych (zapytania API, losowanie wartości, odczyt daty), oraz muszą zwrócić nowy obiekt stanu, jeśli coś się zmieniło, lub ten sam stan, jeśli akcja ich nie dotyczy.
Reducer powinien też mieć zdefiniowany stan początkowy - najczęściej przez parametr domyślny state = initialState. Dzięki temu Redux może zainicjalizować store przez wywołanie reducera z akcją typu @@INIT. Dobrym wzorcem jest również użycie instrukcji switch po action.type lub mapy obiektowej, aby obsłużyć różne rodzaje akcji.
W nowoczesnym Redux Toolkit (RTK) używamy createSlice, które generuje reducery automatycznie i pozwala pisać kod, który "wygląda jak mutacja", ale pod spodem używa biblioteki Immer do tworzenia niezmiennych kopii.
Przykład kodu:
// Klasyczny reducer Redux
const initialState = { count: 0, items: [] };
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'counter/incremented':
// Zwracamy nowy obiekt, nie mutujemy istniejącego
return { ...state, count: state.count + 1 };
case 'counter/decremented':
return { ...state, count: state.count - 1 };
case 'items/added':
// Nowa tablica zamiast push()
return { ...state, items: [...state.items, action.payload] };
default:
// Nieznana akcja - zwracamy stan bez zmian
return state;
}
}
// Nowoczesny wariant z Redux Toolkit (createSlice + Immer)
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
incremented(state) {
// "Wygląda jak mutacja" - Immer tworzy nową kopię pod spodem
state.count += 1;
},
itemAdded(state, action) {
state.items.push(action.payload); // Bezpieczne dzięki Immer
},
},
});
Materiały
↑ Powrót na góręMiddleware
Czym jest Middleware w Redux i jak działa?
Odpowiedź w 30 sekund:
Middleware w Redux to warstwa pośrednia między wywołaniem dispatch(action) a dotarciem akcji do reducera. Pozwala przechwytywać akcje, modyfikować je, logować, opóźniać lub całkowicie blokować — to mechanizm rozszerzania store o efekty uboczne (asynchroniczność, logowanie, autoryzacja).
Odpowiedź w 2 minuty:
Middleware to funkcja, która "owija" oryginalną metodę dispatch store'a. Kiedy w aplikacji wywołasz store.dispatch(action), akcja nie trafia bezpośrednio do reducera — najpierw przechodzi przez łańcuch middleware'ów dodanych przy tworzeniu store. Każdy z nich może wykonać dowolną logikę przed przekazaniem akcji dalej za pomocą next(action).
Dzięki temu mechanizmowi Redux, który sam w sobie jest synchroniczny i czysty, zyskuje wsparcie dla efektów ubocznych: zapytań HTTP (Thunk, Saga, RTK Query), logowania (redux-logger), persistencji (redux-persist), routingu, autoryzacji itp. Middleware to klasyczny przykład wzorca chain of responsibility — kolejne ogniwa decydują, czy przepuszczają zdarzenie dalej.
Standardowa kolejność: dispatch(action) → middleware 1 → middleware 2 → ... → middleware N → reducer(state, action) → nowy stan. Middleware widzi akcję dwukrotnie: gdy "schodzi w dół" przez łańcuch i (opcjonalnie po next()) gdy "wraca w górę" po przejściu przez reducer.
W nowoczesnych projektach z Redux Toolkit middleware konfigurujesz przez configureStore, które automatycznie dołącza thunk oraz developerskie middleware (np. sprawdzanie serializowalności). Wiele projektów w ogóle nie potrzebuje własnego middleware — wystarczy RTK z createAsyncThunk i RTK Query.
Przykład kodu:
// Diagram przepływu akcji przez middleware:
// action --> mw1 --> mw2 --> mw3 --> reducer --> nowy stan
import { configureStore } from '@reduxjs/toolkit';
import logger from 'redux-logger';
import counterReducer from './counterSlice';
// configureStore automatycznie dodaje thunk + dev middleware
const store = configureStore({
reducer: { counter: counterReducer },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(logger), // dokładamy logger
});
// dispatch przechodzi przez: thunk -> serializableCheck -> logger -> reducer
store.dispatch({ type: 'counter/increment' });
sequenceDiagram
participant C as Komponent
participant D as dispatch()
participant M1 as Middleware 1 (thunk)
participant M2 as Middleware 2 (logger)
participant R as Reducer
participant S as Store
C->>D: dispatch(action)
D->>M1: action
M1->>M2: next(action)
M2->>R: next(action)
R->>S: nowy stan
S-->>M2: po reducerze
M2-->>M1: log "after"
M1-->>D: zwrot
D-->>C: kontrola wraca
Materiały
↑ Powrót na góręTestowanie Redux
Jak testować Reducery w izolacji?
Odpowiedź w 30 sekund:
Reducery to czyste funkcje (state, action) => newState, więc testuje się je niezależnie od store'a, komponentów czy middleware. Wystarczy wywołać reducer z konkretnym stanem początkowym i akcją, a następnie sprawdzić zwrócony stan przez toEqual. Nie potrzeba żadnych mocków - testy są szybkie, deterministyczne i pokrywają każdą gałąź switch/builder.addCase.
Odpowiedź w 2 minuty:
Reducer w Reduksie to czysta funkcja - nie ma efektów ubocznych, nie modyfikuje argumentów i dla tych samych danych wejściowych zwraca zawsze ten sam wynik. To czyni go idealnym kandydatem do testów jednostkowych. W teście nie tworzymy store'a, nie dispatchujemy akcji - po prostu wołamy reducer(state, action) i asercjujemy wynik. Pozwala to izolować logikę aktualizacji stanu od reszty aplikacji.
Standardowo testuje się: (1) stan początkowy - wywołanie z undefined i akcją @@INIT powinno zwrócić initialState; (2) obsługę nieznanej akcji - reducer powinien zwrócić stan bez zmian; (3) każdy obsługiwany typ akcji - sprawdzając, czy stan zmienia się zgodnie z oczekiwaniem; (4) niemutowalność - referencja do nowego stanu powinna być inna niż do starego (expect(newState).not.toBe(oldState)).
Przy użyciu Redux Toolkit i createSlice testowanie jest jeszcze prostsze - slice eksportuje reducer i actions, więc test sprowadza się do slice.reducer(state, slice.actions.someAction(payload)). Immer pozwala pisać "mutujący" kod w reducerze, ale wynik jest niezmienny - testy mogą bezpiecznie porównywać struktury przez toEqual.
Dobre testy reducera są szybkie (brak I/O), deterministyczne i służą jako żywa dokumentacja zachowania - czytając test widać dokładnie, jak każda akcja wpływa na kształt stanu.
Przykład kodu:
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0, history: [] },
reducers: {
incremented: (state, action) => {
// Immer pozwala "mutować" - pod spodem tworzy nowy obiekt
state.value += action.payload ?? 1;
state.history.push(state.value);
},
reset: () => ({ value: 0, history: [] }),
},
});
export const { incremented, reset } = counterSlice.actions;
export default counterSlice.reducer;
// counterSlice.test.js
import counterReducer, { incremented, reset } from './counterSlice';
describe('counter reducer', () => {
const initialState = { value: 0, history: [] };
it('powinien zwrócić stan początkowy dla nieznanej akcji', () => {
// Wołamy reducer bez argumentów - powinien dać initialState
expect(counterReducer(undefined, { type: 'unknown/action' }))
.toEqual(initialState);
});
it('powinien zwiększyć licznik o domyślną wartość 1', () => {
const newState = counterReducer(initialState, incremented());
expect(newState.value).toBe(1);
expect(newState.history).toEqual([1]);
});
it('powinien zwiększyć licznik o przekazaną wartość', () => {
const previousState = { value: 5, history: [1, 3, 5] };
const newState = counterReducer(previousState, incremented(10));
expect(newState.value).toBe(15);
expect(newState.history).toEqual([1, 3, 5, 15]);
});
it('nie powinien mutować poprzedniego stanu (niemutowalność)', () => {
const previousState = { value: 5, history: [5] };
const newState = counterReducer(previousState, incremented());
// Sprawdzamy że referencje są różne - Immer stworzył nowy obiekt
expect(newState).not.toBe(previousState);
expect(previousState.value).toBe(5); // oryginał nietknięty
expect(previousState.history).toEqual([5]);
});
it('powinien zresetować stan do wartości początkowych', () => {
const previousState = { value: 42, history: [1, 2, 42] };
expect(counterReducer(previousState, reset())).toEqual(initialState);
});
});
Materiały
↑ Powrót na górę