CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-reduxjs--toolkit

The official, opinionated, batteries-included toolset for efficient Redux development

Pending
Overview
Eval results
Files

actions-reducers.mddocs/

Actions & Reducers

Redux Toolkit provides utilities for creating type-safe action creators and reducers with automatic action type generation and Immer-powered immutable updates.

Capabilities

Create Action

Creates a type-safe action creator function with optional payload preparation.

/**
 * Creates a type-safe action creator with optional payload preparation
 * @param type - The action type string
 * @param prepareAction - Optional function to prepare the payload
 * @returns Action creator function
 */
function createAction<P = void, T extends string = string>(
  type: T
): PayloadActionCreator<P, T>;

function createAction<PA extends PrepareAction<any>, T extends string = string>(
  type: T,
  prepareAction: PA
): PayloadActionCreator<ReturnType<PA>['payload'], T, PA>;

interface PayloadActionCreator<P, T extends string, PA extends PrepareAction<any> = PrepareAction<any>> {
  (payload: P): PayloadAction<P, T>;
  type: T;
  toString(): T;
  match(action: AnyAction): action is PayloadAction<P, T>;
}

interface PayloadAction<P = void, T extends string = string, M = never, E = never> {
  payload: P;
  type: T;
  meta?: M;
  error?: E;
}

interface PrepareAction<P> {
  (payload: P): { payload: P };
  (payload: P, meta: any): { payload: P; meta: any };
  (payload: P, meta: any, error: any): { payload: P; meta: any; error: any };
}

Usage Examples:

import { createAction } from '@reduxjs/toolkit';

// Simple action with payload
const increment = createAction<number>('counter/increment');
console.log(increment(5));
// { type: 'counter/increment', payload: 5 }

// Action without payload
const reset = createAction('counter/reset');
console.log(reset());
// { type: 'counter/reset' }

// Action with prepare function
const addTodo = createAction('todos/add', (text: string) => ({
  payload: {
    text,
    id: nanoid(),
    createdAt: Date.now()
  }
}));

console.log(addTodo('Learn Redux Toolkit'));
// {
//   type: 'todos/add',
//   payload: {
//     text: 'Learn Redux Toolkit',
//     id: 'generated-id',
//     createdAt: 1640995200000
//   }
// }

// Action with meta and error handling
const fetchUser = createAction('user/fetch', (id: string) => ({
  payload: id,
  meta: { requestId: nanoid() }
}));

Action Creator Types

Redux Toolkit provides various action creator types for different use cases.

/**
 * Action creator that requires a payload
 */
interface ActionCreatorWithPayload<P, T extends string = string> {
  (payload: P): PayloadAction<P, T>;
  type: T;
  match(action: AnyAction): action is PayloadAction<P, T>;
}

/**
 * Action creator with optional payload
 */
interface ActionCreatorWithOptionalPayload<P, T extends string = string> {
  (payload?: P): PayloadAction<P | undefined, T>;
  type: T;
  match(action: AnyAction): action is PayloadAction<P | undefined, T>;
}

/**
 * Action creator without payload
 */
interface ActionCreatorWithoutPayload<T extends string = string> {
  (): PayloadAction<undefined, T>;
  type: T;
  match(action: AnyAction): action is PayloadAction<undefined, T>;
}

/**
 * Action creator with prepared payload
 */
interface ActionCreatorWithPreparedPayload<Args extends unknown[], P, T extends string = string, E = never, M = never> {
  (...args: Args): PayloadAction<P, T, M, E>;
  type: T;
  match(action: AnyAction): action is PayloadAction<P, T, M, E>;
}

Create Reducer

Creates a reducer function using Immer for immutable updates with a builder callback pattern.

/**
 * Creates a reducer using Immer for immutable updates
 * @param initialState - Initial state value or factory function
 * @param builderCallback - Function that receives builder for adding cases
 * @param actionMatchers - Additional action matchers (deprecated, use builder)
 * @param defaultCaseReducer - Default case reducer (deprecated, use builder)
 * @returns Reducer with getInitialState method
 */
function createReducer<S>(
  initialState: S | (() => S),
  builderCallback: (builder: ActionReducerMapBuilder<S>) => void
): ReducerWithInitialState<S>;

interface ReducerWithInitialState<S> extends Reducer<S> {
  getInitialState(): S;
}

interface ActionReducerMapBuilder<State> {
  /** Add a case reducer for a specific action type */
  addCase<ActionCreator extends TypedActionCreator<string>>(
    actionCreator: ActionCreator,
    reducer: CaseReducer<State, ReturnType<ActionCreator>>
  ): ActionReducerMapBuilder<State>;
  
  /** Add a case reducer for a specific action type string */
  addCase<Type extends string, A extends Action<Type>>(
    type: Type,
    reducer: CaseReducer<State, A>
  ): ActionReducerMapBuilder<State>;
  
  /** Add a matcher for actions */
  addMatcher<A extends AnyAction>(
    matcher: ActionMatcher<A>,
    reducer: CaseReducer<State, A>
  ): ActionReducerMapBuilder<State>;
  
  /** Add a default case reducer */
  addDefaultCase(reducer: CaseReducer<State, AnyAction>): ActionReducerMapBuilder<State>;
}

type CaseReducer<S = any, A extends Action = AnyAction> = (state: Draft<S>, action: A) => S | void | Draft<S>;

Usage Examples:

import { createReducer, createAction } from '@reduxjs/toolkit';

interface CounterState {
  value: number;
}

const increment = createAction<number>('increment');
const decrement = createAction<number>('decrement');
const reset = createAction('reset');

const counterReducer = createReducer(
  { value: 0 } as CounterState,
  (builder) => {
    builder
      .addCase(increment, (state, action) => {
        // Immer allows "mutating" the draft state
        state.value += action.payload;
      })
      .addCase(decrement, (state, action) => {
        state.value -= action.payload;
      })
      .addCase(reset, (state) => {
        state.value = 0;
      })
      .addMatcher(
        (action): action is PayloadAction<number> => 
          typeof action.payload === 'number',
        (state, action) => {
          // Handle all actions with number payloads
        }
      )
      .addDefaultCase((state, action) => {
        // Handle any other actions
        console.log('Unhandled action:', action.type);
      });
  }
);

// Alternative: return new state instead of mutating
const todoReducer = createReducer([], (builder) => {
  builder.addCase(addTodo, (state, action) => {
    // Can return new state instead of mutating
    return [...state, action.payload];
  });
});

Create Slice

Automatically generates action creators and a reducer from a single configuration object.

/**
 * Automatically generates action creators and reducer from configuration
 * @param options - Slice configuration options
 * @returns Slice object with actions, reducer, and utilities
 */
function createSlice<
  State,
  CaseReducers extends SliceCaseReducers<State>,
  Name extends string = string,
  ReducerPath extends string = Name,
  Selectors extends SliceSelectors<State> = {}
>(options: CreateSliceOptions<State, CaseReducers, Name, ReducerPath, Selectors>): Slice<State, CaseReducers, Name, ReducerPath, Selectors>;

interface CreateSliceOptions<State, CR extends SliceCaseReducers<State>, Name extends string, ReducerPath extends string, Selectors extends SliceSelectors<State>> {
  /** Slice name used to generate action types */
  name: Name;
  /** Path in the store where this slice's state will be located */
  reducerPath?: ReducerPath;
  /** Initial state value or factory function */
  initialState: State | (() => State);
  /** Case reducer functions or creator functions */
  reducers: ValidateSliceCaseReducers<State, CR>;
  /** Builder callback for additional action types */
  extraReducers?: (builder: ActionReducerMapBuilder<NoInfer<State>>) => void;
  /** Selector functions */
  selectors?: Selectors;
}

interface Slice<State = any, CaseReducers = {}, Name extends string = string, ReducerPath extends string = Name, Selectors = {}> {
  /** The slice name */
  name: Name;
  /** The reducer path in the store */
  reducerPath: ReducerPath;
  /** The combined reducer function */
  reducer: Reducer<State>;
  /** Generated action creators */
  actions: CaseReducerActions<CaseReducers, Name>;
  /** Individual case reducer functions */
  caseReducers: CaseReducers;
  /** Returns the initial state */
  getInitialState(): State;
  /** Creates selectors bound to the slice state */
  getSelectors(): Selectors & SliceSelectors<State>;
  /** Creates selectors with custom state selector */
  getSelectors<RootState>(selectState: (state: RootState) => State): Selectors & SliceSelectors<State>;
  /** Selectors with default slice state selector */
  selectors: Selectors & SliceSelectors<State>;
  /** Selector to get the slice state */
  selectSlice(state: { [K in ReducerPath]: State }): State;
  /** Inject into a combined reducer */
  injectInto(injectable: WithSlice<Slice<State, CaseReducers, Name, ReducerPath, Selectors>>, config?: { reducerPath?: any }): any;
}

Usage Examples:

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CounterState {
  value: number;
  status: 'idle' | 'loading' | 'succeeded' | 'failed';
}

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0, status: 'idle' } as CounterState,
  reducers: {
    // Standard reducer with payload
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
    
    // Reducer without payload
    increment: (state) => {
      state.value += 1;
    },
    
    // Reducer with prepare callback
    incrementByAmountWithId: {
      reducer: (state, action: PayloadAction<{ amount: number; id: string }>) => {
        state.value += action.payload.amount;
      },
      prepare: (amount: number) => ({
        payload: { amount, id: nanoid() }
      })
    },
    
    // Reset to initial state
    reset: () => ({ value: 0, status: 'idle' } as CounterState)
  },
  
  // Handle external actions
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserById.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchUserById.fulfilled, (state) => {
        state.status = 'succeeded';
      })
      .addCase(fetchUserById.rejected, (state) => {
        state.status = 'failed';
      });
  },
  
  // Selectors
  selectors: {
    selectValue: (state) => state.value,
    selectStatus: (state) => state.status,
    selectIsLoading: (state) => state.status === 'loading'
  }
});

// Export actions (automatically generated)
export const { increment, incrementByAmount, incrementByAmountWithId, reset } = counterSlice.actions;

// Export selectors
export const { selectValue, selectStatus, selectIsLoading } = counterSlice.selectors;

// Export reducer
export default counterSlice.reducer;

// Usage with typed selectors
const selectCounterValue = counterSlice.selectors.selectValue;
const value = selectCounterValue(rootState); // Automatically typed

// Create selectors with custom state selector
const counterSelectors = counterSlice.getSelectors((state: RootState) => state.counter);
const currentValue = counterSelectors.selectValue(rootState);

Case Reducer Types

/**
 * Map of case reducer functions for a slice
 */
type SliceCaseReducers<State> = {
  [K: string]:
    | CaseReducer<State, PayloadAction<any>>
    | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>;
};

/**
 * Case reducer with prepare method
 */
interface CaseReducerWithPrepare<State, Action extends PayloadAction> {
  reducer: CaseReducer<State, Action>;
  prepare: PrepareAction<Action['payload']>;
}

/**
 * Generated action creators from slice case reducers
 */
type CaseReducerActions<CaseReducers, SliceName extends string> = {
  [Type in keyof CaseReducers]: CaseReducers[Type] extends { prepare: any }
    ? ActionCreatorForCaseReducerWithPrepare<CaseReducers[Type], SliceName>
    : ActionCreatorForCaseReducer<CaseReducers[Type], SliceName>;
};

/**
 * Validation helper for slice case reducers
 */
type ValidateSliceCaseReducers<S, ACR extends SliceCaseReducers<S>> = ACR & {
  [T in keyof ACR]: ACR[T] extends {
    reducer(s: S, action?: infer A): any;
  }
    ? {
        prepare(...a: never[]): Omit<A, 'type'>;
      }
    : {};
};

Action Utilities

Redux Toolkit provides utilities for working with actions and action creators.

/**
 * Type guard to check if a value is an RTK action creator
 * @param action - Value to check
 * @returns True if the value is an action creator
 */
function isActionCreator(action: any): action is (...args: any[]) => AnyAction;

/**
 * Checks if an action follows the Flux Standard Action format
 * @param action - Action to check
 * @returns True if action is FSA compliant
 */
function isFSA(action: unknown): action is {
  type: string;
  payload?: any;
  error?: boolean;
  meta?: any;
};

/** Alias for isFSA */
function isFluxStandardAction(action: unknown): action is {
  type: string;
  payload?: any;
  error?: boolean;
  meta?: any;
};

Usage Examples:

import { isActionCreator, isFSA, createAction } from '@reduxjs/toolkit';

const increment = createAction('increment');
const plainAction = { type: 'PLAIN_ACTION' };

console.log(isActionCreator(increment)); // true
console.log(isActionCreator(plainAction)); // false

console.log(isFSA(increment(5))); // true
console.log(isFSA({ type: 'TEST', invalid: true })); // false

Advanced Patterns

Builder Callback Pattern

// Using builder callback for complex reducer logic
const complexSlice = createSlice({
  name: 'complex',
  initialState: { items: [], status: 'idle' },
  reducers: {
    itemAdded: (state, action) => {
      state.items.push(action.payload);
    }
  },
  extraReducers: (builder) => {
    // Handle async thunk actions
    builder
      .addCase(fetchItems.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchItems.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      // Handle multiple action types with matcher
      .addMatcher(
        (action) => action.type.endsWith('/pending'),
        (state) => {
          state.status = 'loading';
        }
      )
      // Default handler
      .addDefaultCase((state, action) => {
        console.log('Unhandled action:', action);
      });
  }
});

Build Create Slice

Advanced slice creation with custom reducer creators like async thunks.

/**
 * Creates a custom createSlice function with additional reducer creators
 * @param config - Configuration with custom creators
 * @returns Custom createSlice function
 */
function buildCreateSlice(config?: {
  creators?: {
    asyncThunk?: typeof asyncThunkCreator;
  };
}): typeof createSlice;

/**
 * Async thunk creator symbol for buildCreateSlice
 * Used to define async reducers directly in slice definitions
 */
const asyncThunkCreator: {
  [Symbol.for('rtk-slice-createasyncthunk')]: typeof createAsyncThunk;
};

/**
 * Enum defining different types of reducers
 */
enum ReducerType {
  reducer = 'reducer',
  reducerWithPrepare = 'reducerWithPrepare',
  asyncThunk = 'asyncThunk',
}

Usage Examples:

import { buildCreateSlice, asyncThunkCreator } from '@reduxjs/toolkit';

// Create custom slice builder with async thunk support
const createSliceWithAsyncThunks = buildCreateSlice({
  creators: { asyncThunk: asyncThunkCreator }
});

// Use it to create slices with async reducers
const userSlice = createSliceWithAsyncThunks({
  name: 'user',
  initialState: { data: null, loading: false },
  reducers: (create) => ({
    // Regular reducer
    clearUser: create.reducer((state) => {
      state.data = null;
    }),
    // Async thunk reducer defined inline
    fetchUser: create.asyncThunk(
      async (userId: string) => {
        const response = await fetch(`/api/users/${userId}`);
        return response.json();
      },
      {
        pending: (state) => {
          state.loading = true;
        },
        fulfilled: (state, action) => {
          state.loading = false;
          state.data = action.payload;
        },
        rejected: (state) => {
          state.loading = false;
        }
      }
    )
  })
});

Slice Injection

// Dynamically inject slices into combined reducers
import { combineSlices } from '@reduxjs/toolkit';

const rootReducer = combineSlices(counterSlice, todosSlice);

// Later, inject a new slice
const withAuthSlice = authSlice.injectInto(rootReducer);

Custom Prepare Functions

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    todoAdded: {
      reducer: (state, action) => {
        state.push(action.payload);
      },
      prepare: (text: string) => {
        return {
          payload: {
            id: nanoid(),
            text,
            completed: false,
            createdAt: Date.now()
          },
          meta: {
            timestamp: Date.now()
          }
        };
      }
    }
  }
});

Install with Tessl CLI

npx tessl i tessl/npm-reduxjs--toolkit

docs

actions-reducers.md

async-thunks.md

core-store.md

entity-adapters.md

index.md

middleware.md

react-integration.md

rtk-query-react.md

rtk-query.md

utilities.md

tile.json