CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-redux

Predictable state container for JavaScript apps

Pending
Overview
Eval results
Files

reducer-composition.mddocs/

Reducer Composition

Utilities for combining multiple reducer functions into a single root reducer. Essential for organizing state management in larger applications with multiple state slices that need to be managed independently but combined into a single state tree.

Capabilities

Combine Reducers

Turns an object whose values are different reducer functions into a single reducer function.

/**
 * Combines multiple reducer functions into a single reducer function
 * @param reducers - Object whose values correspond to different reducer functions
 * @returns A reducer function that invokes every reducer inside the passed object
 */
function combineReducers<M>(
  reducers: M
): M[keyof M] extends Reducer<any, any, any> | undefined
  ? Reducer<
      StateFromReducersMapObject<M>,
      ActionFromReducersMapObject<M>,
      Partial<PreloadedStateShapeFromReducersMapObject<M>>
    >
  : never;

function combineReducers(reducers: {
  [key: string]: Reducer<any, any, any>
}): Reducer<any, any, any>;

Usage Examples:

import { combineReducers } from "redux";

// Individual reducers
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
};

const todosReducer = (state = [], action) => {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, action.payload];
    case "REMOVE_TODO":
      return state.filter(todo => todo.id !== action.payload.id);
    default:
      return state;
  }
};

// Combine into root reducer
const rootReducer = combineReducers({
  counter: counterReducer,
  todos: todosReducer,
  user: userReducer
});

// Resulting state shape will be:
// {
//   counter: number,
//   todos: Todo[],
//   user: User
// }

Reducer Function Type

The fundamental reducer function type that combines reducers must implement.

/**
 * A reducer is a function that accepts an accumulation and a value and returns a new accumulation
 * @template S - The type of state consumed and produced by this reducer
 * @template A - The type of actions the reducer can potentially respond to  
 * @template PreloadedState - The type of state consumed by this reducer the first time it's called
 */
type Reducer<S = any, A extends Action = UnknownAction, PreloadedState = S> = (
  state: S | PreloadedState | undefined,
  action: A
) => S;

Usage Examples:

// Basic reducer implementation
const countReducer: Reducer<number> = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    default:
      return state;
  }
};

// Typed reducer with specific actions
interface CounterAction {
  type: "INCREMENT" | "DECREMENT" | "RESET";
  payload?: number;
}

const typedCounterReducer: Reducer<number, CounterAction> = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + (action.payload || 1);
    case "DECREMENT":
      return state - (action.payload || 1);
    case "RESET":
      return 0;
    default:
      return state;
  }
};

// Reducer with different preloaded state type
interface AppState {
  count: number;
  name: string;
}

interface PreloadedAppState {
  count?: number;
  name?: string;
}

const appReducer: Reducer<AppState, UnknownAction, PreloadedAppState> = (
  state = { count: 0, name: "" },
  action
) => {
  // Implementation
  return state;
};

Reducer Map Object

Object type whose values correspond to different reducer functions.

/**
 * Object whose values correspond to different reducer functions
 * @template S - The combined state of the reducers
 * @template A - The type of actions the reducers can potentially respond to
 * @template PreloadedState - The combined preloaded state of the reducers
 */
type ReducersMapObject<S = any, A extends Action = UnknownAction, PreloadedState = S> = 
  keyof PreloadedState extends keyof S
    ? {
        [K in keyof S]: Reducer<
          S[K],
          A,
          K extends keyof PreloadedState ? PreloadedState[K] : never
        >
      }
    : never;

State Inference Types

Type utilities for inferring state shapes from reducer maps.

/**
 * Infer a combined state shape from a ReducersMapObject
 * @template M - Object map of reducers as provided to combineReducers(map: M)
 */
type StateFromReducersMapObject<M> = M[keyof M] extends
  | Reducer<any, any, any>
  | undefined
  ? {
      [P in keyof M]: M[P] extends Reducer<infer S, any, any> ? S : never
    }
  : never;

/**
 * Infer reducer union type from a ReducersMapObject
 * @template M - Object map of reducers as provided to combineReducers(map: M)
 */
type ReducerFromReducersMapObject<M> = M[keyof M] extends
  | Reducer<any, any, any>
  | undefined
  ? M[keyof M]
  : never;

/**
 * Infer action type from a reducer function
 * @template R - Type of reducer
 */
type ActionFromReducer<R> = R extends Reducer<any, infer A, any> ? A : never;

/**
 * Infer action union type from a ReducersMapObject
 * @template M - Object map of reducers as provided to combineReducers(map: M)
 */
type ActionFromReducersMapObject<M> = ActionFromReducer<
  ReducerFromReducersMapObject<M>
>;

/**
 * Infer a combined preloaded state shape from a ReducersMapObject
 * @template M - Object map of reducers as provided to combineReducers(map: M)
 */
type PreloadedStateShapeFromReducersMapObject<M> = M[keyof M] extends
  | Reducer<any, any, any>
  | undefined
  ? {
      [P in keyof M]: M[P] extends (
        inputState: infer InputState,
        action: UnknownAction
      ) => any
        ? InputState
        : never
    }
  : never;

Usage Examples:

// Infer state type from reducer map
const reducerMap = {
  counter: counterReducer,
  todos: todosReducer,
  user: userReducer
};

// StateFromReducersMapObject<typeof reducerMap> will be:
// {
//   counter: number;
//   todos: Todo[];
//   user: User;
// }
type AppState = StateFromReducersMapObject<typeof reducerMap>;

// Use inferred types
const selectCounter = (state: AppState) => state.counter;
const selectTodos = (state: AppState) => state.todos;

Advanced Patterns

Nested Reducer Composition

// Nested reducer composition
const featuresReducer = combineReducers({
  featureA: featureAReducer,
  featureB: featureBReducer
});

const rootReducer = combineReducers({
  auth: authReducer,
  features: featuresReducer,
  ui: uiReducer
});

// Resulting state shape:
// {
//   auth: AuthState,
//   features: {
//     featureA: FeatureAState,
//     featureB: FeatureBState
//   },
//   ui: UIState
// }

Conditional Reducer Composition

// Dynamic reducer composition based on environment
const createRootReducer = (environment: string) => {
  const baseReducers = {
    core: coreReducer,
    data: dataReducer
  };

  if (environment === "development") {
    return combineReducers({
      ...baseReducers,
      debug: debugReducer
    });
  }

  return combineReducers(baseReducers);
};

Reducer Validation

The combineReducers function performs several validation checks:

  • Each slice reducer must return their initial state when called with undefined state
  • Reducers must return the previous state for unrecognized actions
  • Reducers must not return undefined for any action
  • The state object passed to reducers should only contain keys corresponding to the reducer map

Error Examples:

// This will throw an error - reducer returns undefined
const badReducer = (state, action) => {
  if (action.type === "RESET") {
    return undefined; // ❌ Never return undefined
  }
  return state;
};

// This will cause warnings - unexpected state keys
const store = createStore(combineReducers({
  counter: counterReducer
}), {
  counter: 0,
  unexpected: "value" // ⚠️ Will warn about unexpected key
});

Install with Tessl CLI

npx tessl i tessl/npm-redux

docs

actions.md

index.md

middleware.md

reducer-composition.md

store-management.md

utilities.md

tile.json