Redux Easy

createReduxSlice.tsx
import { bindActionCreators, createSlice, Dispatch, Reducer } from '@reduxjs/toolkit';

type SettersFor<U> = {
  [K in keyof U]: U[K] extends Array<infer T>
  ? { set: (value: T[] | ((prev: T[]) => T[])) => void } & Array<SettersFor<T>>
  : U[K] extends object
  ? { set: (value: U[K] | ((prev: U[K]) => U[K])) => void } & SettersFor<U[K]>
  : { set: (value: U[K] | ((prev: U[K]) => U[K])) => void };
};

type ReturnSlice<T> = {
  setters: SettersFor<T>,
  reducer: Reducer<T>
}

export const createReduxSlice = <T extends object>(
  sliceName: string,
  initialState: T,
  dispatch: Dispatch<any>
) : ReturnSlice<T> => {


const generateSetters = (obj: T, path: string[] = [], prefix = ''): SettersFor<T> => {
  const setters: any = {};
  if (!obj) return setters;

  const actions: any = {};

  Object.keys(obj).forEach((key) => {
    const value = obj[key];
    const currentPath = path.concat([key]);
    const actionType = `${prefix}${key}`;

    if (Array.isArray(value)) {
      actions[actionType] = (newValue: any) => ({
        type: `${prefix}${key}`,
        payload: { path: currentPath, value: newValue },
      });
      setters[key] = {
        set: (newValue: any) => dispatch(actions[actionType](newValue)),
        ...generateSetters(value, currentPath, `${actionType}.`),
      };
    } else if (typeof value === 'object') {
      actions[actionType] = (newValue: any) => ({
        type: `${prefix}${key}`,
        payload: { path: currentPath, value: newValue },
      });
      setters[key] = {
        set: (newValue: any) => dispatch(actions[actionType](newValue)),
        ...generateSetters(value, currentPath, `${actionType}.`),
      };
    } else {
      actions[actionType] = (newValue: any) => ({
        type: `${prefix}${key}`,
        payload: { path: currentPath, value: newValue },
      });
      setters[key] = {
        set: (newValue: any) => dispatch(actions[actionType](newValue)),
      };
    }
  });

  setters.actions = bindActionCreators(actions, dispatch);

  return setters as SettersFor<T>;
};

  const slice = createSlice({
    name: sliceName,
    initialState,
    reducers: {},
    extraReducers: (builder) => {
      builder.addDefaultCase((state, action) => {
        const type = action?.type?.split('/')?.[0];
        if (type !== sliceName) return state;
        const path = action?.type?.split('/')?.[1];
        const value = action?.payload?.value;
        const pathParts = path?.split('.');
        // console.log({ state, action, path, value })
        let currentLevel = state;
        for (let i = 0; i < pathParts.length - 1; i++) {
          currentLevel = currentLevel[pathParts[i]];
        }
        const lastKey = pathParts[pathParts.length - 1];
        const currentValue = currentLevel[lastKey];

        if (typeof value === 'function') {
          currentLevel[lastKey] = value(currentValue);
        } else {
          currentLevel[lastKey] = value;
        }
      });
    },
  });

  return {
    reducer: slice.reducer,
    setters: generateSetters(initialState, [], `${sliceName}/`),
  } as ReturnSlice<T>;
};

Example usage

Last updated

Was this helpful?