reduxjs / redux-toolkit

The official, opinionated, batteries-included toolset for efficient Redux development
https://redux-toolkit.js.org
MIT License
10.57k stars 1.14k forks source link

Custom reducer creators #3837

Open EskiMojo14 opened 9 months ago

EskiMojo14 commented 9 months ago

courtesy of @phryneas

With the addition of the create callback syntax for createSlice in 2.0, it could be useful to allow custom builders of some fashion to be passed to createSlice (or a function that makes a createSlice function)

creators would need to return a definition which is built immediately when createSlice is called, and then handle that definition by adding any action creators, case reducers, and matchers necessary.

for example, the new create.asyncThunk creator:

type-wise, because the state type is dependent on the individual createSlice call, the only way i could see it working would be with a module augmentation based system like buildCreateApi uses.

this would also be a potential solution for createSlice always pulling in createAsyncThunk for the create.asyncThunk builder - though we are investigating simpler solutions for 2.0.

EskiMojo14 commented 9 months ago

General gist of what a module system for builders would have to look like:

type KeysWithReducerDefinition<
  CaseReducers extends SliceCaseReducers<any>,
  Type extends ReducerType
> = KeysMatching<CaseReducers, ReducerDefinition<Type>>

interface SliceReducerCreators<
  State = any,
  CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
  Name extends string = string
> {
  reducer: {
    create(
      caseReducer: CaseReducer<State, PayloadAction>
    ): CaseReducerDefinition<State, PayloadAction>
    create<Payload>(
      caseReducer: CaseReducer<State, PayloadAction<Payload>>
    ): CaseReducerDefinition<State, PayloadAction<Payload>>

    actions: {
      [Type in KeysWithReducerDefinition<
        CaseReducers,
        ReducerType.reducer
      >]: CaseReducers[Type] extends CaseReducerDefinition<State, any>
        ? ActionCreatorForCaseReducer<
            CaseReducers[Type],
            SliceActionType<Name, Type>
          >
        : never
    }

    caseReducers: {
      [Type in KeysWithReducerDefinition<
        CaseReducers,
        ReducerType.reducer
      >]: CaseReducers[Type] extends CaseReducerDefinition<State, infer A>
        ? CaseReducer<State, A>
        : never
    }
  }
  preparedReducer: {
    create<Prepare extends PrepareAction<any>>(
      prepare: Prepare,
      reducer: CaseReducer<
        State,
        ReturnType<_ActionCreatorWithPreparedPayload<Prepare>>
      >
    ): CaseReducerWithPrepareDefinition<State, Prepare>

    actions: {
      [Type in KeysWithReducerDefinition<
        CaseReducers,
        ReducerType.reducerWithPrepare
      >]: CaseReducers[Type] extends CaseReducerWithPrepareDefinition<
        State,
        infer Prepare
      >
        ? _ActionCreatorWithPreparedPayload<
            Prepare,
            SliceActionType<Name, Type>
          >
        : never
    }

    caseReducers: {
      [Type in KeysWithReducerDefinition<
        CaseReducers,
        ReducerType.reducerWithPrepare
      >]: CaseReducers[Type] extends CaseReducerWithPrepareDefinition<
        State,
        infer Prepare
      >
        ? CaseReducer<
            State,
            ReturnType<_ActionCreatorWithPreparedPayload<Prepare>>
          >
        : never
    }
  }
  asyncThunk: {
    create: AsyncThunkCreator<State>

    actions: {
      [Type in KeysWithReducerDefinition<
        CaseReducers,
        ReducerType.asyncThunk
      >]: CaseReducers[Type] extends AsyncThunkSliceReducerDefinition<
        State,
        infer ThunkArg,
        infer Returned,
        infer ThunkApiConfig
      >
        ? AsyncThunk<Returned, ThunkArg, ThunkApiConfig>
        : never
    }

    caseReducers: {
      [Type in KeysWithReducerDefinition<
        CaseReducers,
        ReducerType.asyncThunk
      >]: CaseReducers[Type] extends AsyncThunkSliceReducerDefinition<
        State,
        infer ThunkArg,
        infer Returned,
        infer ThunkApiConfig
      >
        ? Id<
            Pick<
              Required<
                AsyncThunkSliceReducerConfig<
                  State,
                  ThunkArg,
                  Returned,
                  ThunkApiConfig
                >
              >,
              'fulfilled' | 'pending' | 'rejected' | 'settled'
            >
          >
        : never
    }
  }
}

type Creators<State> = {
  [Name in keyof SliceReducerCreators]: SliceReducerCreators<State>[Name]['create']
}

type CounterCases = {
  increment: CaseReducerDefinition<number, PayloadAction>
  decrementBy: CaseReducerWithPrepareDefinition<
    number,
    (amount: number) => { payload: number }
  >
  fetchAmount: AsyncThunkSliceReducerDefinition<number, void>
}

type Actions = Id<
  UnionToIntersection<
    SliceReducerCreators<number, CounterCases, 'counter'>[
      | 'asyncThunk'
      | 'reducer'
      | 'preparedReducer']['actions']
  >
>

type CaseReducers = Id<
  UnionToIntersection<
    SliceReducerCreators<number, CounterCases, 'counter'>[
      | 'asyncThunk'
      | 'reducer'
      | 'preparedReducer']['caseReducers']
  >
>