Kodėl Redux vis dar aktualus 2024-aisiais
Kai pirmą kartą susidūriau su Redux, atvirai pasakysiu – norėjosi viską mesti po velnių. Tiek daug boilerplate kodo, tiek daug failų, tiek daug koncepcijų, kurias reikia suprasti. Bet praėjus metams supratau, kad Redux nėra tik dar viena biblioteka – tai filosofija, kaip valdyti sudėtingą būseną didelėse aplikacijose.
Šiandien, kai turime Context API, Zustand, Jotai ir daugybę kitų sprendimų, vis dar matau Redux projektus, kurie veikia kaip šveicariškas laikrodis. Ir ne be reikalo. Redux suteikia tai, ko daugelis alternatyvų negali – absoliučią prognozuojamą būsenos valdymo sistemą su laiko kelionėmis (time-travel debugging), puikia ekosistema ir įrankiais, kurie realiai palengvina gyvenimą.
Pagrindinės Redux koncepcijos be akademinio žargono
Įsivaizduokite, kad jūsų aplikacija yra bankas. Redux store – tai centrinis seifas, kur saugomi visi pinigai (duomenys). Negalite tiesiog įeiti ir paimti pinigų – reikia užpildyti kvitą (dispatch action). Banko darbuotojas (reducer) gauna jūsų kvitą, patikrina ar viskas tvarkoje, ir tik tada atlieka operaciją, grąžindamas naują būseną.
Store – vienas objektas, kuriame gyvena visa jūsų aplikacijos būsena. Ne dalimis, ne fragmentais – viskas vienoje vietoje. Tai skamba bauginančiai, bet praktikoje tai didžiulis privalumas.
Actions – paprasti JavaScript objektai, kurie aprašo, kas įvyko. Pavyzdžiui:
„`javascript
{
type: ‘USER_LOGGED_IN’,
payload: {
userId: 123,
email: ‘[email protected]’
}
}
„`
Reducers – grynosios funkcijos, kurios gauna esamą būseną ir action, tada grąžina naują būseną. Svarbiausia taisyklė – niekada nekeisti esamos būsenos tiesiogiai, visada grąžinti naują objektą.
Dispatch – metodas, kuriuo siunčiate actions į store. Tai vienintelis būdas pakeisti būseną.
Praktinis Redux įdiegimas nuo nulio
Geriausia mokytis darydamas, todėl sukursime realią aplikaciją – užduočių valdymo sistemą. Pradėkime nuo instaliacijos:
„`bash
npm install @reduxjs/toolkit react-redux
„`
Taip, naudosime Redux Toolkit, ne seną gerą Redux. Kodėl? Nes Redux Toolkit sumažina boilerplate kodą maždaug 70% ir yra oficialiai rekomenduojamas būdas rašyti Redux logiką.
Sukurkime store struktūrą. Aš asmeniškai laikau Redux logiką `src/store` kataloge:
„`javascript
// src/store/store.js
import { configureStore } from ‘@reduxjs/toolkit’;
import tasksReducer from ‘./slices/tasksSlice’;
import userReducer from ‘./slices/userSlice’;
export const store = configureStore({
reducer: {
tasks: tasksReducer,
user: userReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [‘tasks/addTask’],
},
}),
});
„`
Dabar sukuriame slice – tai Redux Toolkit koncepcija, kuri sujungia actions ir reducers į vieną failą:
„`javascript
// src/store/slices/tasksSlice.js
import { createSlice } from ‘@reduxjs/toolkit’;
const tasksSlice = createSlice({
name: ‘tasks’,
initialState: {
items: [],
loading: false,
error: null,
filter: ‘all’, // all, active, completed
},
reducers: {
addTask: (state, action) => {
state.items.push({
id: Date.now(),
text: action.payload,
completed: false,
createdAt: new Date().toISOString(),
});
},
toggleTask: (state, action) => {
const task = state.items.find(t => t.id === action.payload);
if (task) {
task.completed = !task.completed;
}
},
deleteTask: (state, action) => {
state.items = state.items.filter(t => t.id !== action.payload);
},
setFilter: (state, action) => {
state.filter = action.payload;
},
},
});
export const { addTask, toggleTask, deleteTask, setFilter } = tasksSlice.actions;
export default tasksSlice.reducer;
„`
Atkreipkite dėmesį – atrodo, kad keičiame būseną tiesiogiai (`state.items.push`), bet Redux Toolkit naudoja Immer biblioteką, kuri už kulisų sukuria naują būseną. Tai viena iš priežasčių, kodėl Redux Toolkit yra toks patogus.
Komponentų prijungimas prie Redux
Pirmiausia reikia apgaubti aplikaciją Provider komponentu:
„`javascript
// src/index.js
import React from ‘react’;
import ReactDOM from ‘react-dom/client’;
import { Provider } from ‘react-redux’;
import { store } from ‘./store/store’;
import App from ‘./App’;
const root = ReactDOM.createRoot(document.getElementById(‘root’));
root.render(
);
„`
Dabar bet kuris komponentas gali pasiekti Redux būseną naudodamas `useSelector` ir dispatch actions su `useDispatch`:
„`javascript
// src/components/TaskList.jsx
import React from ‘react’;
import { useSelector, useDispatch } from ‘react-redux’;
import { toggleTask, deleteTask } from ‘../store/slices/tasksSlice’;
const TaskList = () => {
const dispatch = useDispatch();
const { items, filter } = useSelector(state => state.tasks);
const filteredTasks = items.filter(task => {
if (filter === ‘active’) return !task.completed;
if (filter === ‘completed’) return task.completed;
return true;
});
return (
/>
{task.text}
))}
);
};
export default TaskList;
„`
Asinchroninės operacijos su createAsyncThunk
Realybėje duomenys ateina iš API, ne iš lokalaus state. Redux Toolkit turi puikų sprendimą asinchroninėms operacijoms – `createAsyncThunk`. Štai kaip tai veikia:
„`javascript
// src/store/slices/tasksSlice.js
import { createSlice, createAsyncThunk } from ‘@reduxjs/toolkit’;
export const fetchTasks = createAsyncThunk(
‘tasks/fetchTasks’,
async (userId, { rejectWithValue }) => {
try {
const response = await fetch(`/api/tasks?userId=${userId}`);
if (!response.ok) throw new Error(‘Nepavyko gauti užduočių’);
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
export const createTask = createAsyncThunk(
‘tasks/createTask’,
async (taskData, { rejectWithValue }) => {
try {
const response = await fetch(‘/api/tasks’, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify(taskData),
});
if (!response.ok) throw new Error(‘Nepavyko sukurti užduoties’);
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const tasksSlice = createSlice({
name: ‘tasks’,
initialState: {
items: [],
loading: false,
error: null,
},
reducers: {
// … kiti reducers
},
extraReducers: (builder) => {
builder
.addCase(fetchTasks.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchTasks.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchTasks.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
})
.addCase(createTask.fulfilled, (state, action) => {
state.items.push(action.payload);
});
},
});
„`
Komponente naudojame taip:
„`javascript
import { useEffect } from ‘react’;
import { useDispatch, useSelector } from ‘react-redux’;
import { fetchTasks } from ‘../store/slices/tasksSlice’;
const TasksContainer = () => {
const dispatch = useDispatch();
const { items, loading, error } = useSelector(state => state.tasks);
const userId = useSelector(state => state.user.id);
useEffect(() => {
if (userId) {
dispatch(fetchTasks(userId));
}
}, [dispatch, userId]);
if (loading) return
;
if (error) return
;
return
};
„`
Selektoriai ir performance optimizacija
Viena didžiausių klaidų, kurią mačiau pradedančiųjų kode – naudoti `useSelector` su sudėtinga logika tiesiogiai komponente. Tai sukelia nereikalingus re-renderius ir lėtina aplikaciją.
Sprendimas – reselect biblioteka ir memoizuoti selektoriai:
„`javascript
// src/store/selectors/taskSelectors.js
import { createSelector } from ‘@reduxjs/toolkit’;
export const selectAllTasks = state => state.tasks.items;
export const selectTasksFilter = state => state.tasks.filter;
export const selectFilteredTasks = createSelector(
[selectAllTasks, selectTasksFilter],
(tasks, filter) => {
switch (filter) {
case ‘active’:
return tasks.filter(task => !task.completed);
case ‘completed’:
return tasks.filter(task => task.completed);
default:
return tasks;
}
}
);
export const selectTaskStats = createSelector(
[selectAllTasks],
(tasks) => ({
total: tasks.length,
completed: tasks.filter(t => t.completed).length,
active: tasks.filter(t => !t.completed).length,
})
);
export const selectTaskById = createSelector(
[selectAllTasks, (state, taskId) => taskId],
(tasks, taskId) => tasks.find(task => task.id === taskId)
);
„`
Dabar komponente:
„`javascript
import { useSelector } from ‘react-redux’;
import { selectFilteredTasks, selectTaskStats } from ‘../store/selectors/taskSelectors’;
const TasksDashboard = () => {
const filteredTasks = useSelector(selectFilteredTasks);
const stats = useSelector(selectTaskStats);
return (
Aktyvių: {stats.active}
Užbaigtų: {stats.completed}
);
};
„`
Selektoriai perskaičiuojami tik tada, kai pasikeičia jų priklausomybės. Tai drastiškai pagerina performance didelėse aplikacijose.
Redux DevTools ir debugging patarimai
Jei dar nenaudojate Redux DevTools – įsidiekite dabar. Tai Chrome/Firefox plėtinys, kuris leidžia matyti kiekvieną action, būsenos pokyčius, net keliauti laike atgal.
Keletas praktinių patarimų debugging’ui:
1. Naudokite aiškius action tipus
Vietoj `’UPDATE’` rašykite `’tasks/updateTaskTitle’` arba `’user/updateProfile’`. Kai DevTools rodys 50 action’ų, suprasite kodėl tai svarbu.
2. Įtraukite metadata į actions
„`javascript
const addTask = (text) => ({
type: ‘tasks/addTask’,
payload: text,
meta: {
timestamp: Date.now(),
source: ‘user_input’,
},
});
„`
3. Naudokite Redux middleware logging’ui
„`javascript
const loggerMiddleware = store => next => action => {
console.group(action.type);
console.info(‘dispatching’, action);
const result = next(action);
console.log(‘next state’, store.getState());
console.groupEnd();
return result;
};
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware),
});
„`
4. Testuokite reducers atskirai
Reducers yra grynosios funkcijos, todėl jas testuoti – malonumas:
„`javascript
import tasksReducer, { addTask, toggleTask } from ‘./tasksSlice’;
describe(‘tasksSlice’, () => {
it(‘should add new task’, () => {
const initialState = { items: [], loading: false, error: null };
const state = tasksReducer(initialState, addTask(‘Pirkti pieną’));
expect(state.items).toHaveLength(1);
expect(state.items[0].text).toBe(‘Pirkti pieną’);
expect(state.items[0].completed).toBe(false);
});
it(‘should toggle task completion’, () => {
const initialState = {
items: [{ id: 1, text: ‘Test’, completed: false }],
loading: false,
error: null,
};
const state = tasksReducer(initialState, toggleTask(1));
expect(state.items[0].completed).toBe(true);
});
});
„`
Kada Redux yra per daug ir kada per mažai
Dirbau projekte, kur Redux buvo naudojamas net formų būsenai valdyti. Tai buvo košmaras. Taip pat mačiau projektą, kur 50 komponentų perdavinėjo props 5 lygius žemyn, nes „Redux per sudėtingas”.
Kada Redux yra tinkamas pasirinkimas:
– Aplikacija turi daug būsenos, kuri naudojama skirtingose vietose
– Reikia laiko kelionių debugging’ui
– Turite sudėtingą asinchroninę logiką
– Dirba keletas developerių ir reikia aiškios struktūros
– Planuojate ilgalaikį projekto palaikymą
Kada Redux galbūt per daug:
– Mažas projektas su minimalia būsena
– Būsena naudojama tik vienoje aplikacijos dalyje
– Paprastos CRUD operacijos be sudėtingos logikos
– Mokotės React ir Redux kartu (pradėkite nuo Context API)
Alternatyvos, į kurias verta pažiūrėti:
– **Zustand** – minimalistinis, bet galingas. Mano asmeninis favoritas mažesniems projektams.
– **Jotai** – atomic state management, puikus React Concurrent Mode palaikymui.
– **MobX** – jei mėgstate reactive programming ir nenorite boilerplate.
– **Context API + useReducer** – dažnai pakanka, ypač su React 18.
Ką išmokau per metus su Redux production’e
Norėčiau pasidalinti keliais įžvalgomis, kurias gavau ne iš dokumentacijos, o iš realių projektų su tūkstančiais vartotojų.
**Struktūrizuokite pagal features, ne pagal tipus.** Vietoj `actions/`, `reducers/`, `selectors/` katalogų, darykite `features/tasks/`, `features/auth/`, `features/notifications/`. Kiekvienas feature turi savo slice, selektorius, testus. Kai reikia ištrinti ar pakeisti feature – ištrinat vieną folderį, ne 15 failų iš skirtingų vietų.
**Nenaudokite Redux viskam.** UI būsena (ar modalas atidarytas, kokia tab aktyvi) dažniausiai turėtų būti lokali. Redux – bendrai būsenai, kuri svarbi keliems komponentams ar išlieka tarp route’ų.
**Normalizuokite duomenis.** Jei turite nested objektus, normalizuokite juos. Vietoj:
„`javascript
{
tasks: [
{ id: 1, title: ‘Task 1’, author: { id: 5, name: ‘Jonas’ } },
{ id: 2, title: ‘Task 2’, author: { id: 5, name: ‘Jonas’ } },
]
}
„`
Darykite:
„`javascript
{
tasks: {
byId: {
1: { id: 1, title: ‘Task 1’, authorId: 5 },
2: { id: 2, title: ‘Task 2’, authorId: 5 },
},
allIds: [1, 2],
},
users: {
byId: {
5: { id: 5, name: ‘Jonas’ },
},
},
}
„`
Tai palengvina atnaujinimus ir išvengia dubliavimo.
**Investuokite į TypeScript.** Redux su TypeScript yra kitas lygis. Autocompletion, type safety, refactoring tampa trivialia užduotimi. Redux Toolkit turi puikų TypeScript palaikymą.
**Middleware yra jūsų draugas.** Reikia analytics? Middleware. Error tracking? Middleware. API caching? Middleware. Tai galingas įrankis, kurį per mažai kas naudoja.
Redux nėra tobulas, bet jis išbandytas mūšio lauke. Kai suprantate jo principus ir naudojate Redux Toolkit, jis tampa ne našta, o patikimu partneriu kuriant sudėtingas aplikacijas. Svarbiausia – nenaudoti jo ten, kur nereikia, ir nenaudoti per mažai ten, kur reikia. Aukso viduriukas, kaip ir visur programavime.
