Reducers
Reducers are responsible for turning (currentState, action) into nextState.
In HexaJS, reducer classes use decorators:
@Reducer()on the class.@Reduce(ACTION_TYPE)on each handler method.
Reducer example
import { HexaReducer, Reduce, Reducer } from '@hexajs-dev/core';
import * as BackgroundActions from './background.actions';
@Reducer()
export class LastContentCallReducer extends HexaReducer<LastContentCallState> {
initialState: LastContentCallState = {
message: '',
timestamp: 0,
};
@Reduce(BackgroundActions.CONTENT_CALLED)
onContentCalled(
state: LastContentCallState,
action: ReturnType<typeof BackgroundActions.contentCalled>
): LastContentCallState {
if (!action.payload?.message || !action.payload?.timestamp) {
return state;
}
return { ...action.payload };
}
}
Reducer rules
- Return a new object when state changes.
- Return
stateunchanged when payload is invalid or action is not applicable. - Keep reducer methods deterministic and side-effect free.
- Perform logging/IO in services/controllers/handlers, not reducers.
Async initial state with initState
initialState is synchronous. When a slice needs to start from persisted or asynchronous data — storage, a token, a remote value — override initState instead.
import { HexaReducer, Reduce, Reducer } from '@hexajs-dev/core';
import { inject } from '@hexajs-dev/common';
import { StoragePort } from '@hexajs-dev/ports';
import * as Actions from './background.actions';
@Reducer()
export class ClipsReducer extends HexaReducer<ClipItem[]> {
initialState: ClipItem[] = [];
async initState(): Promise<ClipItem[]> {
const result = await inject(StoragePort).get('local', 'clips');
const stored = result['clips'];
return Array.isArray(stored) ? stored : [];
}
@Reduce(Actions.CLIPS_UPDATED)
onClipsUpdated(_state: ClipItem[], action: ReturnType<typeof Actions.clipsUpdated>): ClipItem[] {
return [...action.payload.clips];
}
}
How it works:
- The generated bootstrap calls
initState()on every reducer that defines it, before the context starts handling any messages. - The resolved value replaces
initialStatefor that slice. inject(...)is safe insideinitStatebecause the DI container is fully initialized before bootstrap calls it.- If
initStateis not defined,initialStateis used as-is. - If
initStatereturns a plain value (sync), the generated code uses it directly. If it returns aPromise(or uses theasynckeyword), the generated code awaits it.
Use initState whenever a slice's starting value depends on anything that can't be evaluated synchronously at class definition time. Common cases: loading from StoragePort, reading a token, or fetching a remote default.
CLI and compiler behavior
- Use
hexa generate reducer <name> <context>to scaffold reducer classes. - Use
hexa generate state <name> <context>to register reducer slices under@State(...). - The CLI scanner (
packages/cli/src/compiler/store/reducer/scanner.ts) discovers reducer metadata during build.
Where reducers are used
Reducers power both:
- Background store transitions driven by controllers/actions.
- Content store transitions driven by handlers/actions.
Functions
createReducer
Creates a reducer function from an initial state and one or more action handlers
import { createReducer } from '@hexajs-dev/core';
function createReducer<T>(initialState: T, ...handlers: OnHandler<T, any>[]): (state: T | undefined, action: HexaAction | HexaActionWithPayload<string, any>) => T