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,
> = ({name, initialState, additionalReducer}:{
name: string;
initialState: State;
additionalCaseReducers: ValidateSliceCaseReducers;
}) => {
const slice = createSlice({
name: props.name,
initialState: props.initialState,
reducers: {
// Базовые экшены, которые будут в каждом экземпляре слайса
innerAction: (
state,
action: PayloadAction
) => {
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,
>({
fieldName,
validateFn,
baseName,
}: {
// базовое название, для генерации уникального экшена (Вариант 2)
//(лучше использовать название слайста в который будет добавлять)
baseName: string;
fieldName: FieldKey;
// функция для валидации инпута
validateFn?: (val: string) => InputValidateInfo;
}) {
// Базовое поведения стейта
const setValue = (state: Draft, 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>
): CaseReducer> =>
(state, action) => {
setValue(state, action.payload);
additionalCaseReducer?.(state, action);
};
//Вариант №2 для добавления в extraReducers в slice
//Создаем экшен
const setValueAction = createAction(
`${baseName.toString()}/set/${fieldName.toString()}`
);
const addToExtraReducer = (
//builder из extraReducer
builder: ActionReducerMapBuilder,
// Возможность при создании расширить поведение экшена
additionalCaseReducer?: CaseReducer>
) => {
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({
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
выбирает из объекта только поля с заданным типом, который Создайте на основе варианта №3 генератор, который может создать переиспользуемую логику сразу для нескольких полей ввода, с дополнительным экшеном, который полностью очищает данные поля.
Заключение
Конечно, эти варианты не исключают, а скорее дополняют друг друга и могут использоваться совместно.
Надеюсь, что данная статья была полезна, Вы нашли для себя что-то новое и теперь можете сделать ваш код на redux более гибким и удобным для переиспользования.
Буду рад услышать критику и предложения в комментариях. Спасибо за внимание!
Темы для будущих статей:
Приятные мелочи для удобной работы с redux-toolkit.
Удобная работа с asyncThunk.
ListenerMiddleware и asyncThunk где связь?
Модульность, скрытие и изоляция в redux.
Redux-toolkit и переиспользование кода [2].
Redux и его динамические возможности.