В данной статье приведены несколько вариантов переиспользования кода в Redux-toolkit при создании слайсов, позволяющие сделать работу с ним более гибкой и удобной.
Для адептов других стейт менеджеров
Данная статья, еще один шанс для Вас показать насколько другой стейт-менеджер лучше чем redux, поэтому поделитесь, пожалуйста, кодом, решающим аналогичную задачу на другом стейт-менеджере. И, возможно, ваш пример убедит других разработчиков сделать правильное решение.
Вариант 1 - Полное дублирование слайсов
Самый простой вариант - создать функцию, создающую одинаковые слайсы, но с разными, уникальными названиями. Такой вариант подходит, если необходимо создать несколько идентичных по поведению слайсов, но со своими экземплярами стейта и экшенов.
const createPageSlice = (name: string) => { const initialState = ... return createSlice({name, initialState, reducers: {...}}) } const rootReducer = combineReducers({ page1: createPageslice('page1').reducer, page2: createPageslice('page2').reducer, })
Задача со звездочкой для тех, кто хочет попрактиковаться
Создать генератор слайсов, который может быть использован для быстрого создания списка с фильтром по поиску.
Создайте генератор слайсов, который примет дженериком тип массива , состоящего из объектов произвольного типа, и пропсом название поля из этого объекта по которому будет производиться фильтрация.
В итоге должен получится слайс с полями: список, отфильтрованный список, поиск. И с экшенами setInput, clearInput, setArray
Вариант 2 - Расширения функциональности слайса
Но что если у нас не совсем одинаковые слайсы, а только одинаковое ядро и логика вокруг него? Для расширения функциональности слайса, при создании слайса можно передать дополнительные экшены.
import { SliceCaseReducers, ValidateSliceCaseReducers, createSlice, PayloadAction, } from "@reduxjs/toolkit"; // Базовый тип стейта для слайса export type PageBaseStateSchema = { innerStateField: ...; }; const createPageModel = < // стейт слайса может быть любой, но должен расширять базовый тип State extends PageBaseStateSchema, // Дженерик для того чтобы тайпскрипт корректно подхватывал //тип экшенов в готовом слайсе CaseReducers extends SliceCaseReducers<State>, > = ({name, initialState, additionalReducer}:{ name: string; initialState: State; additionalCaseReducers: ValidateSliceCaseReducers<State, CaseReducers>; }) => { const slice = createSlice({ name: props.name, initialState: props.initialState, reducers: { // Базовые экшены, которые будут в каждом экземпляре слайса innerAction: ( state, action: PayloadAction<PageBaseStateSchema["innerStateField"]> ) => { state.innerStateField = action.payload; }, ...props.additionalCaseReducers, }, }); return slice };
Создание экземпляра слайса:
type ExtendedStateSchema = BaseStateSchema & { extendedStateField: ...; }; const initialState: ExtendedStateSchema = { innerStateField: ... extendedStateField: ..., }; const slice = createExtendedSlice({ name: 'name', initialState: initialState, additionalReducer: { // Дополнительные экшены outerAction: (state, action: PayloadAction<...>) => { state.extendedStateField = ... state.innerStateField = ... }, }, });
Минусы:
Для расширения поведение базовых экшенов, необходимо в типизации пропсов функции, создающей слайс, явно прописывать поля для дополнительного поведения для каждого из внутренних экшенов.
Невозможность простой комбинации нескольких подобных переиспользуемых слайсов
Вариант 3 - Переиспользование логики в других слайсах
В данном варианте, мы будем решать обратную задачу - как мы можем расширить любой слайс любым количеством переиспользуемых частей, например, фильтром или любым другим функционалом? Не писать же в каждом месте, где есть фильтры, в слайсах одну и ту же логику.
В качестве примера создадим функцию, позволяющую легко добавить поле ввода с валидацией в любой слайс.
Реализуем два варианта добавления в слайс:
Добавляем в поле
reducersпри создании слайсаДобавляем в поле
extraReducersпри создании слайса
Вариант с добавлением в extraReducers может пригодиться, если хочется чтобы добавленные экшены были не в общем объекте slice.actions , а отдельным объектом.
Disclaimer
Для примера специально был выбрано создание поля ввода, так что предлагаю обсудить в комментариях плюсы и минусы работы с формами в redux.
// Создадим тип для поля ввода export type InputStateSchema = { value: string; validInfo?: InputValidateInfo; }; function createSingleInput< //Тип будущего стейта необходим, чтобы можно было расширять поведение State extends AnyObject, // Кастомный тип для объекта любого вида // поле в будущем стейте, которое имеет необходимый тип //PickFieldsWithType Кастомный тип, выбирает из объекта только поля с заданным типом FieldKey extends keyof PickFieldsWithType< State, InputStateSchema > = keyof PicKFieldsWithType<State, InputStateSchema>, >({ fieldName, validateFn, baseName, }: { // базовое название, для генерации уникального экшена (Вариант 2) //(лучше использовать название слайста в который будет добавлять) baseName: string; fieldName: FieldKey; // функция для валидации инпута validateFn?: (val: string) => InputValidateInfo; }) { // Базовое поведения стейта const setValue = (state: Draft<State>, value: string) => { const inputInfo = state[ fieldName as keyof typeof state ] as IInputStateSchema; if (validateFn) { inputInfo.validInfo = validateFn(value); } inputInfo.value = value; }; //функция для инициализации стейта const getInitialState = (initValue?: string): InputStateSchema => { return { value: initValue || "", validInfo: validateFn?.(initValue || ""), }; }; //Вариант №1 для добавления в reducers в slice const createSetValueCaseReducer = ( // Возможность при создании расширить поведение экшена additionalCaseReducer?: CaseReducer<State, PayloadAction<string>> ): CaseReducer<State, PayloadAction<string>> => (state, action) => { setValue(state, action.payload); additionalCaseReducer?.(state, action); }; //Вариант №2 для добавления в extraReducers в slice //Создаем экшен const setValueAction = createAction<string>( `${baseName.toString()}/set/${fieldName.toString()}` ); const addToExtraReducer = ( //builder из extraReducer builder: ActionReducerMapBuilder<State>, // Возможность при создании расширить поведение экшена additionalCaseReducer?: CaseReducer<State, PayloadAction<string>> ) => { builder.addCase(setValueAction, createSetValueCaseReducer(additionalCaseReducer)); }; return { //Вариант №1 добавление в reducers в slice createSetValueCaseReducer, //Вариант №2 добавление в extraReducers в slice actions: {setValue: setValueAction}, addToExtraReducer, //Базовое поведение setValue, getInitialState, }; }
Создание экземпляра слайса:
type PageWithInputStateSchema = { // Поле с которым будет работать наша функция input: InputStateSchema; otherState: ...; }; const pageWithInputName = "pageWithInput" //Создаем инпут const inputForPage = createSingleInput<PageWithInputStateSchema>({ fieldName: "input", baseName: pageWithInputName, validateFn: validateLength(3), }); const initialState: PageWithInputStateSchema = { //Получаем стейт для нашего инпута input: inputForPage.getInitialState('initial value'), otherState: ..., }; const pageWithInputSlice = createSlice({ name: pageWithInputName, initialState: initialState, reducers: { otherAction: (state) => { //Можно работать с инпутом внутри любого экшена inputForPage.setValue(state, ""); }, //Вариант №1 добавление в reducers в slice setInput: inputForPage.createSetValueCaseReducer((state, action) => { //Расширение экшена, с возможность описывать сайд эффекты на стейт state.otherState = action.payload.length; }), }, extraReducers: (builder) => { //Вариант №2 добавление в extraReducers в slice inputForPage.addToExtraReducer(builder, (state, action) => { //Расширение экше��а, с возможность описывать сайд эффекты на стейт state.otherState = action.payload.length; }); }, });
Данный вариант лишен недостатков варианта №2 и позволяет гибко добавлять и комбинировать переиспользуемые куски логики в любом слайсе и расширять их поведение. Благодаря этому по всему проекту будет меньше дублирования кода, и единообразная логика, с возможностью кастомизации.
Задача со звездочкой для тех, кто хочет попрактиковаться
Реализовать тип
PicKFieldsWithType<Obj, Type>, которыйвыбирает из объекта только поля с заданным типомСоздайте на основе варианта №3 генератор, который может создать переиспользуемую логику сразу для нескольких полей ввода, с дополнительным экшеном, который полностью очищает данные поля.
Заключение
Конечно, эти варианты не исключают, а скорее дополняют друг друга и могут использоваться совместно.
Надеюсь, что данная статья была полезна, Вы нашли для себя что-то новое и теперь можете сделать ваш код на redux более гибким и удобным для переиспользования.
Буду рад услышать критику и предложения в комментариях. Спасибо за внимание!
Темы для будущих статей:
Приятные мелочи для удобной работы с redux-toolkit.
Удобная работа с asyncThunk.
ListenerMiddleware и asyncThunk где связь?
Модульность, скрытие и изоляция в redux.
Redux-toolkit и переиспользование кода [2].
Redux и его динамические возможности.
