Redux withTypeScript — правильный способ строгой типизации в соответствии с Redux Toolkit

Каждый, кто использует TypeScript, видит огромную ценность строгой типизации — она безопасна и предсказуема. Я видел много проектов, в которых используются текущие версии Redux, и в них все еще много самодекларативных интерфейсов. Это огромная проблема, если что-то изменится…

Прежде чем мы начнем, я рекомендую установить Redux Toolkit с помощью npm install --save @reduxjs/toolkit, который включает в себя множество модных создателей, которые, я думаю, вам понравятся!

Шаг 1. Действия

Redux Toolkit предоставляет нам метод createAction, который создает тип действия и необязательную полезную нагрузку. Мне нравится иметь отдельные полезные данные в действии как независимый ключ, так что давайте начнем с этого помощника.

// helpers.ts
import { createAction } from '@reduxjs/toolkit';
function withPayloadType<T>() {
  return (t: T) => ({ payload: t });
}
export const createActionWithPayload = <T>(action: string) =>
  createAction(action, withPayloadType<T>());
export { createAction };

Затем у нас есть два отдельных метода для создателей действий — с полезной нагрузкой или без нее.

Итак, давайте приступим к нашим действиям. Мы создаем Enum для имен действий, но конечным результатом является константа, которая выглядит как перечисление, но это объект ключевой функции с нашими действиями, готовыми к отправке.

// actions.ts
import { createActionWithPayload, createAction } from './helpers';
import { ToDo, ToDoID } from './types';
enum ToDoActionName {
  ADD_TODO = 'ADD_TODO',
  REMOVE_TODO = 'REMOVE_TODO'
}
export const ToDoActions = {
  add: createActionWithPayload<ToDo>(ToDoActionName.ADD_TODO),
  remove: createActionWithPayload<ToDoID>(ToDoActionName.REMOVE_TODO)
);

Шаг 2. Редуктор

В этом суть Redux Toolkit — createReducer с builder, которые основаны на начальном типе состояния, и с помощью метода addCase предоставляют нам типы состояния и действия.

// reducer.ts
import { createReducer } from '@reduxjs/toolkit';
import { ToDo, ToDoID } from './types';
import { ToDoActions } from './actions';
interface ToDoState extends Record<ToDoID, ToDo> {}
const initialState: ToDoState = {};
export const toDoReducer = createReducer(initialState, builder =>
  builder
    .addCase(ToDoActions.add, (state, { payload }) => ({
      ...state,
      [payload.id]: payload
    }))
    .addCase(ToDoActions.remove, (state, { payload }) => {
      const newState = { ...state };
      delete newState[payload];
      return newState;
    })
);

Шаг 3. Магазин

Здесь нам нужно сделать 3 вещи:

  1. Создать магазин
  2. Получить тип состояния магазина
  3. Добавьте некоторые усилители, такие как Redux devtools.
// store.ts
import { createStore, combineReducers, compose } from 'redux';
import { toDoReducer } from './reducer';
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const reducer = combineReducers({
  todos: toDoReducer
});
export type StoreState = ReturnType<typeof reducer>
export const store = createStore(reducer, composeEnhancers());

Шаг 4. Селекторы

И сейчас у нас простая ситуация с проверкой типов — у нас динамический тип StoreState, поэтому будет очень легко создавать новые селекторы, если вы поставите новый редуктор или если вы измените текущий редуктор.

// selectors.ts
import { StoreState } from './store';
import { ToDo, ToDoId } from './types';
export const getToDo = (state: StoreState, id: ToDoId): ToDo => state.todos[id];

Резюме

Как видите, это очень просто, и прямо сейчас вы можете быть уверены, что все типы очень сильно связаны друг с другом, поэтому влияние изменения каждого типа будет очень легко распознать.