Галоўная / Артыкулы

Redux Toolkit: адаптация з First Slice да асінхронных дадзеных — рабочая ментальная модэль

Дазвольце даклэ научыцца, як разам працуюць хранальнік Redux Toolkit, фрагменты, редуктары на адварце Immer, селектары і createAsyncThunk, выкорыстоўваючы лічылку, корзіну пакупак і спіс продуктав.

Выдавец: REACTAPP.TOP9 хв чытання2008 слоў

Крыніцы: 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() падходзяць для большасці дапраў.