Predictable state container for JavaScript apps
—
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.
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
// }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;
};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;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;// 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
// }// 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);
};The combineReducers function performs several validation checks:
undefined stateundefined for any actionError 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