Redux-toolkit и переиспользование кода

от автора

В данной статье приведены несколько вариантов переиспользования кода в 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 — Переиспользование логики в других слайсах

В данном варианте, мы будем решать обратную задачу — как мы можем расширить любой слайс любым количеством переиспользуемых частей, например, фильтром или любым другим функционалом? Не писать же в каждом месте, где есть фильтры, в слайсах одну и ту же логику.

В качестве примера создадим функцию, позволяющую легко добавить поле ввода с валидацией в любой слайс.

Реализуем два варианта добавления в слайс:

  1. Добавляем в поле reducers при создании слайса

  2. Добавляем в поле 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 и позволяет гибко добавлять и комбинировать переиспользуемые куски логики в любом слайсе и расширять их поведение. Благодаря этому по всему проекту будет меньше дублирования кода, и единообразная логика, с возможностью кастомизации.

Задача со звездочкой для тех, кто хочет попрактиковаться
  1. Реализовать тип PicKFieldsWithType<Obj, Type>, которыйвыбирает из объекта только поля с заданным типом

  2. Создайте на основе варианта №3 генератор, который может создать переиспользуемую логику сразу для нескольких полей ввода, с дополнительным экшеном, который полностью очищает данные поля.


Заключение

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

Надеюсь, что данная статья была полезна, Вы нашли для себя что-то новое и теперь можете сделать ваш код на redux более гибким и удобным для переиспользования.

Буду рад услышать критику и предложения в комментариях. Спасибо за внимание!

Темы для будущих статей:

  • Приятные мелочи для удобной работы с redux-toolkit.

  • Удобная работа с asyncThunk.

  • ListenerMiddleware и asyncThunk где связь?

  • Модульность, скрытие и изоляция в redux.

  • Redux-toolkit и переиспользование кода [2].

  • Redux и его динамические возможности.


ссылка на оригинал статьи https://habr.com/ru/articles/771660/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *