Redux Toolkit: адаптация з First Slice да асінхронных дадзеных — рабочая ментальная модэль
Дазвольце даклэ научыцца, як разам працуюць хранальнік Redux Toolkit, фрагменты, редуктары на адварце Immer, селектары і createAsyncThunk, выкорыстоўваючы лічылку, корзіну пакупак і спіс продуктав.
Крыніцы: Redux Toolkit: A Complete Guide from Basics to Production
Калі додатак на базе React расте, спяльны стан, такі як даныя прыняцоў, кошыка, настаўкі, фільтраў і даныя API, распадаецца между багатымі экранамі, і перадача ўсьго гэтага через пропы становіцца нестабільной. Redux рашае гэтыя проблемы за дапамогою центральнага хранілня, а Redux Toolkit (RTK) — гэта рекамендаваны спосаб напісання коду на Redux сэродзеўні. Шырыя пашагоўна ад простага кантэйнера да кошыка з вырахаванымі загальнымі сумамі і спісу, падтрымванага API, ўпэўніцца, што вы зможаце адкрыта чытаць і пісаць код на RTK.
Спачатку пераканайцеся, чы вам ваўсе трэба хранілня: толькі викорыстоўванне пропаў сама па сабе не є прычыной для інсталляціі Redux.
Основная ідея Redux
Redux зберагае спяльны стан у аднам хранілня і зменяе яго толькі через дзеянні, якія обробляюцься редукерамі, тады кожная змена є прагнозаванай. Без яго даныя пераходзяць па шляху пропаў слой за слоем.
Component
↓
Props
↓
Component
↓
Props
↓
Component
За дапамою магазіна компаненты чытаюць тое, што ў іх патрэбна, і адправляюць дзеяння для запыту пра змены.
Redux Store
↑
│
┌─────────┴─────────┐
│ │
Component A Component B
│ │
└──── dispatch ─────┘
Што дадае Redux Toolkit
RTK аб’еднвае інструменты для распашчых Redux-задач.
Redux Toolkit
│
├── configureStore()
├── createSlice()
├── createAsyncThunk()
├── createSelector()
└── RTK Query
У класычным Redux адна функцыя розпадалася на многа файлы.
Action Types
Action Creators
Reducers
Constants
Store
Middleware
createSlice() дазволяе выражыць большую частку гэтага ў аднам месцы.
Як даныя трапляюцца ў прыемліку RTK
Компаненты чытаюць даныя за дапамою useSelector() і адправляюць дзеяння за дапамою useDispatch(); редукеры ствараюць новы стан.
React Application
│
↓
useSelector()
│
↓
Redux Store
↑
│
useDispatch()
↑
│
Actions
│
↓
Reducer
│
↓
New State
Асновны цикл:
UI
↓
dispatch(action)
↓
Reducer
↓
State update
↓
Subscribed components
↓
UI re-render
Інсталяванне та стварэнне магазіна
Інсталюйце два пакеты:
npm install @reduxjs/toolkit react-redux
@reduxjs/toolkit мае API Redux; react-redux спаявае іх з React.
useSelector()
useDispatch()
Provider
Прыкладны прыемлік-лямпачка арганізаваны па функцыях.
src/
│
├── app/
│ └── store.js
│
├── features/
│ └── counter/
│ └── counterSlice.js
│
├── App.jsx
└── main.jsx
configureStore() прыёмліе карточку з редукерамі сляйсаў; кожны ключ становіцца галоўной галуззю статауса.
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
Рэзультатны статаус:
{
counter: {
value: 0
}
}
Сляйсы: адна функцыя, адны файл
Сляйс контролюе адну логічную частку статауса, і ў дапытках зазвычай існуе калькі такіх сляйсаў.
authSlice
cartSlice
productSlice
userSlice
notificationSlice
Сляйс для лічыльніка адзначае назву, пачатковы статаус і аднаго редукера на кожную операцыю, пасля чаго экспортуе ствараныя функціі для дзействаў і редукер.
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
value: 0,
};
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment(state) {
state.value += 1;
},
decrement(state) {
state.value -= 1;
},
incrementByAmount(state, action) {
state.value += action.payload;
},
},
});
export const {
increment,
decrement,
incrementByAmount,
} = counterSlice.actions;
export default counterSlice.reducer;
Адна вызовка заменяе тры ручныя рэалізацыі.
Action Types
+
Action Creators
+
Reducer
Што стварае createSlice
Возьмім мінімальны сляйс:
createSlice({
name: "counter",
initialState,
reducers: {
increment(state) {
state.value += 1;
},
},
});
RTK спаўнае назву сляйса і ключ редукера ў тип дзейства.
"counter/increment"
і стварае функцыю для дзейства.
increment()
Такім чынам dispatch(increment()) ўзроўнае дыспэтчаванню:
dispatch({
type: "counter/increment",
});
Паўязка хранілніцы з React
У файле main.jsx элемент Provider робіць стоар доступным для кожнага компонента, які знаходзяцца нижэй яго.
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App";
import { store } from "./app/store";
ReactDOM.createRoot(
document.getElementById("root")
).render(
<Provider store={store}>
<App />
</Provider>
);
Чытанне дадзеных за дапамою useSelector
Функцыя useSelector() выбірае самэ гарадус стану, які патрэбен компоненту.
import { useSelector } from "react-redux";
function Counter() {
const count = useSelector(
state => state.counter.value
);
return <h1>{count}</h1>;
}
Компонент перерысваецца толькі тады, калі змянюецца гэты значэнне; дакледзе прыгляніцеся да статті як React-Redux выбірае, калі перерысваць компонент.
Запіс дадзеных за дапамою useDispatch
Функцыя useDispatch() вяртае элемент dispatch стоара.
import { useDispatch } from "react-redux";
import { increment } from "./counterSlice";
function Counter() {
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(increment())}>
Increment
</button>
);
}
Потым адпаведныя дзеяння выглядаюць так:
Button Click
↓
dispatch(increment())
↓
Action
↓
Reducer
↓
State changes
↓
useSelector detects relevant change
↓
Component re-renders
Перадача дадзеных за дапамою payload
Дзеяння несуць дадзеныя ў атрыбуте payload, які бераецца з другага аргумента редуктара:
incrementByAmount(state, action) {
state.value += action.payload;
}
Аварыстацель перадае гэта значэнне:
dispatch(incrementByAmount(10));
што стварае такое дзеяння:
{
type: "counter/incrementByAmount",
payload: 10
}
Кожныя дзеянні маюць type і, за выборам, payload.
action
│
├── type
└── payload
Чаму тут дазволяецца мутацыя стану
Класычны пытальнік у адзінаванні: гэты редюсер, здаецца, мутуе стан, чаго Redux не дазваляе.
increment(state) {
state.value += 1;
}
RTK запускае редюсеры через Immer, які пасылае праект-праблей, фіксуе змяны і стварае новы незменны об’ект.
Reducer
↓
Immer Proxy
↓
Track changes
↓
Create new immutable state
Вы отрымліваеце чытабельны код без нарушэння прынцыпа незменнасці. Можна альбо зменіць праект, альбо вернуць новую значэнне, ніколи і тое, і другое, і не мутавацыя такім чынам параду RTK-редюсероў.
Частка каравана пакупак
Караван для е-камеры начынаецца пустым.
const initialState = {
items: [],
};
Гэта частка дадае, выключае і чыстае элементы; removeItem прызначае новы масіў, які таксама прыймае Immer.
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
items: [],
};
const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
addItem(state, action) {
state.items.push(action.payload);
},
removeItem(state, action) {
state.items = state.items.filter(
item => item.id !== action.payload
);
},
clearCart(state) {
state.items = [];
},
},
});
export const {
addItem,
removeItem,
clearCart,
} = cartSlice.actions;
export default cartSlice.reducer;
Дадаць элемент
Якщо є об’ект продукту:
const product = {
id: 101,
name: "Laptop",
price: 60000,
};
Адміністратар пры нажатыі апрашоўвае яго:
const dispatch = useDispatch();
const handleAddToCart = () => {
dispatch(addItem(product));
};
і весь продукт стае пэйлоадам.
{
type: "cart/addItem",
payload: {
id: 101,
name: "Laptop",
price: 60000
}
}
Потым редюсер дадвае яго ў:
state.items
Адзёрванне элемента
Фільтрацыя па id для адзёрвання:
removeItem(state, action) {
state.items = state.items.filter(
item => item.id !== action.payload
);
}
Компонент перадае толькі id:
dispatch(removeItem(101));
таму ўнутры редюсера:
action.payload === 101
Вычысленне значэнняў заместа ўтримвання іх
Падазроўваючы, што кошачка мае такія элементы:
[
{
price: 100,
quantity: 2,
},
{
price: 50,
quantity: 3,
}
]
Аб’ём вылічваецца безпосередна:
100 × 2 + 50 × 3
= 200 + 150
= 350
Хочацеся зберагчы аб’ём пад элементамі:
{
items,
total
}
але тады кожны редюсер павінен синхронізаваць іх оба. Лепш яго вычысляць:
const selectCartTotal = state =>
state.cart.items.reduce(
(total, item) =>
total + item.price * item.quantity,
0
);
і чытаць яго:
const total = useSelector(selectCartTotal);
Зберагчы аднаковы источнік даных; вычысляць усе, што можна вырахаваць, на яго аднойчыні.
Селектары як адзіны пункт доступу
Селектар — это функцыя, якая чытае стан.
const selectCartItems = state =>
state.cart.items;
Компаненты выкарыстоўваюць яго замест таго, каб ведаць пра форму стану.
const items = useSelector(selectCartItems);
Інакша гэты шлях будзе павтарацца ўсюды:
state.cart.items
За дапамогою селектара рэструктурызацыя паўтараецца толькі ў адной функцыі.
Зберагчэнне рэзультатаў за дапамогою createSelector
createSelector() кэшуе отрыманы рэзультат пакуль не змянююцца яго вхідныя даны.
import { createSelector } from "@reduxjs/toolkit";
const selectCartItems = state =>
state.cart.items;
export const selectCartTotal = createSelector(
[selectCartItems],
items =>
items.reduce(
(total, item) =>
total + item.price * item.quantity,
0
)
);
Логіка простая:
Input unchanged
↓
Return cached result
Input changed
↓
Recalculate
Гэта дапамагае практычна ў дорогіх вылічэннях і калі селектары вяртаюць новыя масэвы або об’екты, якія інакша будуць перзапрацоўваны ў кожны раз, калі змінюецца стан.
Завантажэнне дадзеных за дапамогою createAsyncThunk
Програмы вызываюць API такія, як:
GET /api/products
Запрос праходзіць ад стану «idle» через стан «loading» да стану «success»
idle
↓
loading
↓
success
альбо да стану «error».
idle
↓
loading
↓
error
createAsyncThunk() моделюе гэта: павінен быць заданы прыфікс типу і асінхронная функцыя; выкалекванне сігналізуе пра невыполненны запрос.
import {
createAsyncThunk,
createSlice,
} from "@reduxjs/toolkit";
export const fetchProducts = createAsyncThunk(
"products/fetchProducts",
async () => {
const response = await fetch("/api/products");
if (!response.ok) {
throw new Error("Failed to fetch products");
}
return response.json();
}
);
Ён стварае тры дзеянні жыцёвага циклу:
fetchProducts.pending
fetchProducts.fulfilled
fetchProducts.rejected
Адрабатаванне жыцёвага циклу ў фрагменте
Фрагмент продуктав пазіруе элементы, статус і прычыны каштоў, апдэйтуючы іх у extraReducers.
const productsSlice = createSlice({
name: "products",
initialState: {
items: [],
status: "idle",
error: null,
},
reducers: {},
extraReducers: builder => {
builder
.addCase(
fetchProducts.pending,
state => {
state.status = "loading";
}
)
.addCase(
fetchProducts.fulfilled,
(state, action) => {
state.status = "success";
state.items = action.payload;
}
)
.addCase(
fetchProducts.rejected,
(state, action) => {
state.status = "failed";
state.error =
action.error.message;
}
);
},
});
reducers проты extraReducers
reducers вядомае сабе дзеянні фрагмента, з створанымі для іх выканання функціямі.
reducers: {
addItem() {}
}
extraReducers адпавядае на дзеянні, вядомыя ў іншых частках коду, напрыклад, тыя, якія стварае createAsyncThunk():
pending
fulfilled
rejected
Фрагмент проста слухае гэтыя дзеянні.
Адправка thunk-а з компонента
Пачніце запыт пад час стварэння компонента:
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchProducts());
}, [dispatch]);
У режыме Strict Mode эфекты выкананы два разы пад час розработы, таму магчыма будзе побачыць два запыты. Пасля таго выберіце статус
const {
items,
status,
error,
} = useSelector(
state => state.products
);
і атрыбуты, якія залежнаць ад статусу:
if (status === "loading") {
return <p>Loading...</p>;
}
if (status === "failed") {
return <p>{error}</p>;
}
return (
<div>
{items.map(product => (
<div key={product.id}>
{product.name}
</div>
))}
</div>
);
Для болей сложнага запрашання дадзэных RTK Query можа заменіць гэты патерн.
Мідлвэр і configureStore
Мідлвэр знаходзится межы dispatch() і редюсерамі.
dispatch(action)
↓
Middleware
↓
Reducer
↓
New State
Звычныя выкарыстанні:
- Асінхронная логіка
- Лагаванне
- Аналітыка
- Процесы аутантыкаціі
- Побачныя эфекты
- Перехопленне дзеянняў
configureStore() заменяе ручную настройку, дадаючы стандартны мідлвэр (заключаючы thunks) і Redux DevTools.
const store = configureStore({
reducer: {
auth: authReducer,
cart: cartReducer,
products: productsReducer,
},
});
Галоўныя выводы
- Slice аб’еднвае стан, редюсеры і стваральнікі дзеянняў для адной функцыяльнасці.
- Immer робіць редюсеры у стыле мутацый безпечнымі, але толькі ўнутры редюсераў RTK.
- Зберагайце мінімальны стан; раштатныя даны трэба атрымваць за дапамогою селектараў, калі ўжо ўтрата часу є значным.
createAsyncThunk()у поўнай зусіллі разам зextraReducersмоделюе станы загрузкі, успеху і нявезення.- Стандартныя настройкі
configureStore()падходзяць для большасці дапраў.