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 вещи:
- Создать магазин
- Получить тип состояния магазина
- Добавьте некоторые усилители, такие как 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];
Резюме
Как видите, это очень просто, и прямо сейчас вы можете быть уверены, что все типы очень сильно связаны друг с другом, поэтому влияние изменения каждого типа будет очень легко распознать.