kamod-state · Typed reducers · Preact contextInstall · Compose · Explore

Tiny, typed reducer state management for Preact

createStore and createAction with TypeScript-first action matching, Preact context providers, optional middleware, and a signals bridge — without a React runtime.

Model meaningful state transitions with named actions and a focused reducer. Keep one clear source of truth, then connect only the state each component needs through your chosen Preact integration. Pair it with the component library when building your interface.

In this guide 9 sections

What the package brings

Use named actions and reducers when changes need a clear explanation. Model the events in your flow first, then decide which components read state and which dispatch actions.

Choose the right approach

A local open or closed control

Keep simple presentation state local to the component.

A reducer store is useful when events and shared ownership add clarity, not for every boolean.

A shared domain workflow

Model named actions and derive the next state in a pure reducer.

Store domain facts; derive display labels and totals where possible instead of saving duplicates.

A reactive view of store data

Select the smallest useful value with the package's Preact integration.

If a signals bridge is needed, keep one authoritative writer rather than two synchronized stores.

Typed action creators

createAction returns creators with .match() so reducers stay narrow and exhaustiveness-friendly.

Choose action names that describe a domain event. Keep the payload focused so a reducer can make its decision from the current state and that event without reaching into a component.

Preact integration

createStoreContext wires stores into components with useStore, useDispatch, and useSelector entry points.

Place the store boundary around the consumers that need it. Select the state each view actually uses and keep transient interaction details local when they do not belong in the domain model.

Optional signals bridge

Subscribe store slices through @kamod-ch/state/signals when you already use @preact/signals in the UI.

Use a bridge when an existing signal-based view needs a store slice. Keep the store authoritative and derive the view instead of synchronizing two separately editable copies.

Installation

Install @kamod-ch/state with Preact as a peer dependency when using the Preact context entry points.

pnpm add @kamod-ch/state preact

Check your environment

Use your existing package manager and keep the peer dependencies aligned with your application. Compare package.json and your lockfile with the documentation for the version you install; add the package once at the workspace boundary that uses it.

Keep the first change small. Start with the usage example, run your project’s typecheck and build, then connect it to a real screen. The package overview helps you decide how companion libraries fit together.

Usage

Define typed actions, implement a reducer with .match(), create a store, and optionally expose it through createStoreContext for component trees.

src/example.tsx
import { createAction, createStore } from "@kamod-ch/state";

const increment = createAction("counter/increment");

const store = createStore({
  reducer: (state = { count: 0 }, action) =>
    increment.match(action) ? { count: state.count + 1 } : state,
});

store.dispatch(increment());

Read the example

Follow the value from its definition to the interface. Each part below explains a decision to keep when adapting the example.

Name the event

A named event describes an intention, not a particular button. Multiple controls can dispatch it without teaching the reducer about their markup. Keep naming consistent as the feature grows.

const increment = createAction("counter/increment");
Return a new value only for a handled event

The matcher selects the branch. Returning the existing state for unrelated actions protects the current value; returning a new object for the matching action makes the transition explicit. Avoid mutation or network work inside the reducer.

increment.match(action) ? { count: state.count + 1 } : state
Dispatch through the store boundary

A dispatch changes the model. A Preact view also needs a subscription, normally through the package's context and selector hooks. Reading a snapshot once does not turn an arbitrary component into a subscriber.

store.dispatch(increment());

Define the state and event contract together

An explicit state type makes the model readable as soon as a second view needs it. A factory gives each test or server request its own store. Decide where the app owns that instance before wiring a provider around its consumers.

src/state/counter.ts
import { createAction, createStore } from "@kamod-ch/state";

export const increment = createAction("counter/increment");
type CounterState = { count: number };
type CounterAction = ReturnType<typeof increment>;

export function createCounterStore() {
  return createStore<CounterState, CounterAction>({
    reducer: (state = { count: 0 }, action) =>
      increment.match(action) ? { count: state.count + 1 } : state,
  });
}

Try the boundaries. Create two stores and confirm that dispatching to one does not change the other. When adding asynchronous work, represent pending, success and failure deliberately and keep the effect outside the reducer. Consult the package reference for context, selectors and middleware signatures.

Integrate with your application

Name actions for what happened. Keep reducer transitions predictable and place network requests or other effects at a deliberate application boundary. This makes loading, success and failure states easier to inspect and test.

Choose the store’s lifetime explicitly: an isolated flow, a mounted application tree or a request. Avoid a shared server singleton for user-specific state. Use selectors to keep the relationship between component inputs and domain state easy to follow.

If you also use signals, define which layer owns the value before adding a bridge. Derive a view of existing state rather than maintaining two independently writable copies that can drift apart.

Choose the owner and lifetime

Put the store at the boundary of the workflow it represents. A page-local flow can use a page-local owner, while an app-wide session model may need a longer lifetime. For SSR and tests, factories make ownership explicit. Keep reducers deterministic so the same state and event produce the same result without depending on the clock or network.

Account for the environment

Model asynchronous outcomes as deliberate transitions. A submit action can lead to pending, followed by success or failure; the component then renders those states. Keep the request itself outside the reducer and decide how stale results are handled if the user starts another operation or leaves the screen before it completes.

Connect the pieces. Component-owned hooks — Keep transient UI behavior close to its component while the store owns the shared domain model.

When something behaves differently

Start with the smallest failing interaction. Compare a fresh page with the same page after a change or reload, and keep the package version in your reproduction.

Dispatch happens but the view does not update
Verify that the view subscribes through the appropriate context or selector API. Also check that the matching reducer branch returns a new value instead of mutating existing state.
An unrelated action resets the screen
Inspect the reducer's fallback branch. Return the current state for actions it does not handle rather than recreating the initial state.
State leaks between tests or requests
Look for a shared module-level store. Create an instance for each independent owner and verify that updates in one instance cannot affect the other.

If the behavior still differs from the documented API, check existing issues before opening a report. Include the expected result, actual result, package version and a small reproduction without private application data.

API Reference

This page is a Kamod UI overview. Full API docs, middleware, testing helpers, and signals integration live on the dedicated kamod-state docs.

Use this page to understand the integration, then consult the Full API reference on live docs for exact signatures and supported options. Compare those details with the version in your lockfile before adapting an example.

Explore API, middleware, and Preact patterns

Open the kamod-state docs for getting started, context usage, testing utilities, and the signals bridge.

  • Before choosing an API: read its input types, return values and default behavior.
  • Before shipping: review lifecycle or server-rendering notes for the features you use.
  • When behavior differs: reduce the case to a small example and include your package version in the report.
Continue with the dedicated package documentation.Open live docs

Accessibility Notes

When store state drives UI (dialogs, tabs, selections), keep focus management and ARIA state in sync with dispatched actions.

Before you ship

  • Test successful, failed and cancelled transitions with representative actions.
  • Confirm that unrelated actions leave the reducer’s state intact.
  • Check that opening and closing UI through actions also preserves focus and keyboard behavior.

Review the result with real content and keyboard input. Keep visible labels, loading and error feedback, and focus behavior in sync with the state your application exposes.

Take the guide with you

Keep the examples, integration decisions and troubleshooting notes together in your project, or share them with a coding assistant. This reference works with any assistant; no model-specific setup is needed. Check the installed version and your project’s conventions before applying a suggestion.

Download Markdown reference

Display changes only the view. Copy and download include the complete Markdown reference.

state-package-reference.md
# State — integration reference

createStore and createAction with TypeScript-first action matching, Preact context providers, optional middleware, and a signals bridge — without a React runtime.

Package: @kamod-ch/state

## Choose an approach

Use named actions and reducers when changes need a clear explanation. Model the events in your flow first, then decide which components read state and which dispatch actions.

### A local open or closed control

Keep simple presentation state local to the component.

A reducer store is useful when events and shared ownership add clarity, not for every boolean.

### A shared domain workflow

Model named actions and derive the next state in a pure reducer.

Store domain facts; derive display labels and totals where possible instead of saving duplicates.

### A reactive view of store data

Select the smallest useful value with the package's Preact integration.

If a signals bridge is needed, keep one authoritative writer rather than two synchronized stores.

## Installation

Install @kamod-ch/state with Preact as a peer dependency when using the Preact context entry points.

```bash
pnpm add @kamod-ch/state preact
```

## Starting example

```tsx
import { createAction, createStore } from "@kamod-ch/state";

const increment = createAction("counter/increment");

const store = createStore({
  reducer: (state = { count: 0 }, action) =>
    increment.match(action) ? { count: state.count + 1 } : state,
});

store.dispatch(increment());
```

### Name the event

A named event describes an intention, not a particular button. Multiple controls can dispatch it without teaching the reducer about their markup. Keep naming consistent as the feature grows.

```tsx
const increment = createAction("counter/increment");
```

### Return a new value only for a handled event

The matcher selects the branch. Returning the existing state for unrelated actions protects the current value; returning a new object for the matching action makes the transition explicit. Avoid mutation or network work inside the reducer.

```tsx
increment.match(action) ? { count: state.count + 1 } : state
```

### Dispatch through the store boundary

A dispatch changes the model. A Preact view also needs a subscription, normally through the package's context and selector hooks. Reading a snapshot once does not turn an arbitrary component into a subscriber.

```tsx
store.dispatch(increment());
```

## Define the state and event contract together

An explicit state type makes the model readable as soon as a second view needs it. A factory gives each test or server request its own store. Decide where the app owns that instance before wiring a provider around its consumers.

File: src/state/counter.ts

```tsx
import { createAction, createStore } from "@kamod-ch/state";

export const increment = createAction("counter/increment");
type CounterState = { count: number };
type CounterAction = ReturnType<typeof increment>;

export function createCounterStore() {
  return createStore<CounterState, CounterAction>({
    reducer: (state = { count: 0 }, action) =>
      increment.match(action) ? { count: state.count + 1 } : state,
  });
}
```

Create two stores and confirm that dispatching to one does not change the other. When adding asynchronous work, represent pending, success and failure deliberately and keep the effect outside the reducer. Consult the package reference for context, selectors and middleware signatures.

## Ownership and environment

Put the store at the boundary of the workflow it represents. A page-local flow can use a page-local owner, while an app-wide session model may need a longer lifetime. For SSR and tests, factories make ownership explicit. Keep reducers deterministic so the same state and event produce the same result without depending on the clock or network.

Model asynchronous outcomes as deliberate transitions. A submit action can lead to pending, followed by success or failure; the component then renders those states. Keep the request itself outside the reducer and decide how stale results are handled if the user starts another operation or leaves the screen before it completes.

A useful state model explains both what happened and what the user sees next. Pair loading, success and failure transitions with visible feedback and deliberate focus behavior.

## Troubleshooting

### Dispatch happens but the view does not update

Verify that the view subscribes through the appropriate context or selector API. Also check that the matching reducer branch returns a new value instead of mutating existing state.

### An unrelated action resets the screen

Inspect the reducer's fallback branch. Return the current state for actions it does not handle rather than recreating the initial state.

### State leaks between tests or requests

Look for a shared module-level store. Create an instance for each independent owner and verify that updates in one instance cannot affect the other.

## Resources

- [Documentation](https://kamod-ch.github.io/kamod-state/)
- [Source](https://github.com/kamod-ch/kamod-state)
- [npm](https://www.npmjs.com/package/@kamod-ch/state)

Sources & attribution

This integration guide accompanies @kamod-ch/state, maintained in the Kamod ecosystem. The dedicated documentation is the reference for package APIs; examples here show how those APIs fit into a Kamod UI application.

When copying or distributing source, retain the applicable license and attribution notices from the version you use. Check the published package alongside your lockfile when comparing an example with a newer release.