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

729f88507d5340987e13dd0ef3d4f390

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

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

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

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

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

Зачала со звездочкой для тех, кто хочет попрактиковаться

  1. Реализовать тип PicKFieldsWithType, которыйвыбирает из объекта только поля с заданным типом

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

Заключение

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

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

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

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

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

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

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

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

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

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

© Habrahabr.ru