State Management

The @mimopo/croqueta framework includes a simple, fast, and lightweight state management system inspired by Redux and NgRx, but built on top of the TC39 Signals proposal.

It manages a single state tree and allows state updates through dispatched actions and reducers. It supports feature-based state registration, side effects, and state selection with signals.

Fundamental Concepts

The state management system is built around these core concepts:

  • Store: The single source of truth that holds the application state.
  • Actions: Plain objects that describe a change in the state.
  • Reducers: Pure functions that specify how the state changes in response to an action.
  • Selectors: Functions used to extract and derive specific pieces of state.
  • Features: Slices of the global state tree, grouped by functionality.
  • Effects: Side effects that can be used to react to actions, state changes or an external signal.

STORE
STORE
SELECTOR
SELECTOR
COMPONENT
COMPONENT
ACTION
ACTION
EFFECTS
EFFECTS
REDUCER
REDUCER
SERVICE
SERVICE
Text is not SVG - cannot display

Store

The Store is a service that can be injected into your components or other services.

import { Component, Store, inject } from '@mimopo/croqueta';

class MyComponent extends Component {
  private store = inject(Store);

  protected render(): Node {
    return html`<div></div>`;
  }
}
import { Component, Store, inject } from '@mimopo/croqueta';

class MyComponent extends Component {
  store = inject(Store);

  render() {
    return html`<div></div>`;
  }
}

Dispatching Actions

To update the state, you dispatch an action to the store.

this.store.dispatch(myAction({ id: 1 }));
this.store.dispatch(myAction({ id: 1 }));

Selecting State

To read state, you use selectors. The store.select() method returns a Computed Signal, which automatically updates when the state changes.

const count = this.store.select(selectCount);

// In a component effect or template
console.log(count.get());
const count = this.store.select(selectCount);

// In a component effect or template
console.log(count.get());

Actions

Actions are created using the createAction helper. This ensures consistency and simplifies typing (although in JavaScript, it's mostly for organization).

import { createAction } from '@mimopo/croqueta';

// Action with no payload
const increment = createAction<void>('INCREMENT');

// Action with a payload
const addUser = createAction<{ name: string }>('ADD_USER');

// Using the actions
const action = addUser({ name: 'John' });
// returns { type: 'ADD_USER', payload: { name: 'John' } }
import { createAction } from '@mimopo/croqueta';

// Action with no payload
const increment = createAction('INCREMENT');

// Action with a payload
const addUser = createAction('ADD_USER');

// Using the actions
const action = addUser({ name: 'John' });
// returns { type: 'ADD_USER', payload: { name: 'John' } }

Reducers

Reducers are functions that take the current state and an action payload, and return a new state object. They must be pure functions.

import type { Reducer } from '@mimopo/croqueta';

const counterReducer: Reducer<{ count: number }, void> = (state) => {
  return {
    ...state,
    count: state.count + 1,
  };
};

const userReducer: Reducer<{ users: string[] }, string> = (state, payload) => {
  return {
    ...state,
    users: [...state.users, payload],
  };
};
const counterReducer = (state) => {
  return {
    ...state,
    count: state.count + 1,
  };
};

const userReducer = (state, payload) => {
  return {
    ...state,
    users: [...state.users, payload],
  };
};

Selectors

Selectors are functions that extract a piece of state from the global state tree.

Feature Selectors

Use createFeatureSelector to get a top-level slice of the state.

import { createFeatureSelector } from '@mimopo/croqueta';

const selectCounterState = createFeatureSelector<{ count: number }>('counter');
import { createFeatureSelector } from '@mimopo/croqueta';

const selectCounterState = createFeatureSelector('counter');

Derived Selectors

Use createSelector to create complex state derivations. These are memoized for performance.

import { createSelector } from '@mimopo/croqueta';

const selectCount = createSelector(selectCounterState, (state) => state.count);

const selectIsPositive = createSelector(selectCount, (count) => count > 0);
import { createSelector } from '@mimopo/croqueta';

const selectCount = createSelector(selectCounterState, (state) => state.count);

const selectIsPositive = createSelector(selectCount, (count) => count > 0);

Features

Features allow you to group your state, actions, and reducers into logical slices. This is the recommended way to organize state in your application.

Use createFeature to define a feature and store.registerFeature() to add it to the global store.

Defining a Feature

import { createFeature, createAction, createFeatureSelector, createSelector, type Reducers } from '@mimopo/croqueta';

const key = 'counter';
const initialState = { count: 0 };

const actions = {
  increment: createAction<void>('increment'),
  decrement: createAction<void>('decrement'),
};

const reducers: Reducers<typeof initialState, typeof actions> = {
  increment: (state) => ({ ...state, count: state.count + 1 }),
  decrement: (state) => ({ ...state, count: state.count - 1 }),
};

const featureSelector = createFeatureSelector<typeof initialState>(key);
const selectors = {
  count: createSelector(featureSelector, (state) => state.count),
};

export const counterFeature = createFeature({
  key,
  initialState,
  actions,
  reducers,
  selectors,
});
import { createFeature, createAction, createFeatureSelector, createSelector } from '@mimopo/croqueta';

const key = 'counter';
const initialState = { count: 0 };

const actions = {
  increment: createAction('increment'),
  decrement: createAction('decrement'),
};

const reducers = {
  increment: (state) => ({ ...state, count: state.count + 1 }),
  decrement: (state) => ({ ...state, count: state.count - 1 }),
};

const featureSelector = createFeatureSelector(key);
const selectors = {
  count: createSelector(featureSelector, (state) => state.count),
};

export const counterFeature = createFeature({
  key,
  initialState,
  actions,
  reducers,
  selectors,
});

Registering a Feature

Features are usually registered at the application level or within a specific service.

import { Store, inject } from '@mimopo/croqueta';
import { counterFeature } from './counter.feature';

const store = inject(Store);
store.registerFeature(counterFeature);
import { Store, inject } from '@mimopo/croqueta';

import { counterFeature } from './counter.feature';

const store = inject(Store);
store.registerFeature(counterFeature);

Effects

Effects handle side effects like data fetching or complex workflows. An effect is a function that receives the Store instance and can optionally return an action to be dispatched.

Effects are reactive; they run inside a signal effect and can react to state or action changes.

import { type Effect, Store } from '@mimopo/croqueta';

const loadUsersEffect: Effect = async (store: Store) => {
  // We can react to an action being dispatched
  const lastAction = store.actions.get();

  if (lastAction.type === 'LOAD_USERS') {
    const response = await fetch('/api/users');
    const users = await response.json();

    // Return an action to update the state
    return usersLoadedAction(users);
  }
};

// Registering the effect
store.registerEffect(loadUsersEffect);
import { Store } from '@mimopo/croqueta';

const loadUsersEffect = async (store) => {
  // We can react to an action being dispatched
  const lastAction = store.actions.get();

  if (lastAction.type === 'LOAD_USERS') {
    const response = await fetch('/api/users');
    const users = await response.json();

    // Return an action to update the state
    return usersLoadedAction(users);
  }
};

// Registering the effect
store.registerEffect(loadUsersEffect);

You can also pass effects directly in the feature configuration:

export const usersFeature = createFeature({
  // ...
  effects: [loadUsersEffect],
});
export const usersFeature = createFeature({
  // ...
  effects: [loadUsersEffect],
});

Examples

For a complete working example of the state management in action, check out the State Example Application.

You can open it directly with StackBlitz ⚡️