kamod-signals · Persisted state · Familiar .value APIInstall · Compose · Explore

Persisted Preact signals for every storage driver

Reactive state with durable storage — localStorage, sessionStorage, IndexedDB, cookies, and memory — while keeping the familiar @preact/signals .value API.

Decide which values should survive a reload before choosing a storage driver. Use a stable key, define a useful initial value, and keep the lifetime of the signal aligned with the screen, session or application that owns it. Pair it with the component library when building your interface.

In this guide 9 sections

What the package brings

Use persistence for values that should outlive their current render. Decide what the preference belongs to, how long it should survive and how a user can return to the default.

Choose the right approach

A preference across visits

Choose local storage for small, non-sensitive browser preferences.

A saved browser value is neither account synchronization nor authoritative server data.

A shorter-lived browser value

Choose session storage when the lifetime should follow that browser session.

Document which values survive refreshes and which should disappear on sign-out.

Server-aware persistence

Review the cookie driver and createCookieContext at the request boundary.

Cookie scope, response headers and request isolation belong to the server integration.

Persistence built in

persistedSignal and usePersistedSignal sync reactive values to the driver you choose.

Give each preference a stable storage key and a useful initial value. Treat future changes to the stored shape as a migration decision, rather than assuming every returning visitor has fresh data.

Framework friendly

Works beside Preact components and plain modules — share state across controllers without prop drilling.

Share a signal only when its consumers should share the same value. Keep temporary screen state local, and avoid copying persisted values into a second independently writable store.

SSR-aware cookies

createCookieContext and serializeCookie helpers keep cookie-backed signals usable on the server.

Keep request-specific cookie handling at the server boundary. Verify that the server and client agree on the initial value, and check what happens when there is no saved preference.

Installation

Install @kamod-ch/signals with @preact/signals and Preact as peer dependencies when using the package outside the Kamod UI monorepo.

pnpm add @kamod-ch/signals @preact/signals 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

Create a persisted signal with a storage key and driver, then read or write .value as usual. Use usePersistedSignal inside components for scoped instances.

src/example.tsx
import { persistedSignal } from "@kamod-ch/signals";

export const theme = persistedSignal("theme", "dark", { storage: "local" });

// later
theme.value = "light";

Read the example

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

Give the saved value a clear owner

The key identifies browser storage, while the variable identifies the live signal. Decide whether it is a device preference or belongs to a signed-in account before sharing the key across screens or sessions.

const theme = persistedSignal("theme", "dark", { storage: "local" });
Read and write through one signal

Consumers should observe the same instance. Copying the value into unrelated local state creates a second owner that can become stale. Keep derived presentation derived, rather than synchronizing two writable values with effects.

theme.value = "light";
Provide an explicit reset

Reset restores the initial value; clearing a stored entry is a separate operation. Use an explicit user action for preference resets, and verify the behavior after reload rather than only checking the currently rendered label.

theme.reset();

Keep a preference local to a component

A custom hook can keep the persistent preference close to the settings control. The versioned key below leaves room for a future storage-shape change. This example is intentionally a harmless preference, not authentication or private user data.

src/components/DensityPreference.tsx
import { usePersistedSignal } from "@kamod-ch/signals";
import { Button } from "@kamod-ch/ui";

export function DensityPreference() {
  const compact = usePersistedSignal("ui:compact:v1", false, { storage: "local" });
  return (
    <div class="flex flex-wrap gap-2">
      <Button aria-pressed={compact.value} onClick={() => { compact.value = !compact.value; }}>
        Compact spacing
      </Button>
      <Button variant="ghost" onClick={() => compact.reset()}>Restore default</Button>
    </div>
  );
}

Try the boundaries. Toggle, reload and restore the default. Then test without a stored value and with browser storage restricted. For SSR, ensure the first client render agrees with the server; browser-only preferences cannot automatically be known by the server.

Integrate with your application

Persist preferences deliberately. A color scheme or density setting has a different lifetime from a draft or an account-specific value. Name keys clearly, decide when they should be reset and avoid retaining temporary UI state without a reason.

The module-level usage example demonstrates a shared signal. In a server-rendered application, do not share user-specific mutable state across requests. Follow the package’s SSR and cookie guidance when a preference must be available before hydration.

Keep reads and writes through the signal’s .value interface and review how your chosen driver handles unavailable storage. Plan for old stored values when you change a preference’s shape or allowed options.

Choose the owner and lifetime

Name keys by purpose and treat a stored shape as a small data contract. Give defaults the same care as saved values. Decide whether clearing a preference means removing storage, resetting the live value, or both. For account-specific settings, define sign-out behavior and avoid silently applying one user's saved value to the next user on the device.

Account for the environment

Storage availability, hydration and cross-tab updates are separate concerns. Test the chosen driver in the actual environment instead of assuming all storage behaves like synchronous local storage. If a preference changes layout, make the initial server and client output compatible and avoid a second competing copy of the value in component state.

Connect the pieces. Reducer state — Use explicit events and transitions when a workflow needs more structure than a persisted preference.

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.

A value changes back after refresh
Check the storage driver, key and write path. Make sure the code updates .value on the intended persisted signal rather than a separate temporary variable.
A returning visitor sees invalid data
Inspect the saved shape from older releases. Validate or migrate before using it, and provide a reset path when the value cannot be recovered.
Two controls disagree
Confirm that they are meant to share a source and review the driver's synchronization behavior. Matching variable names do not guarantee matching signal instances or storage keys.

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, driver tables, and SSR cookie examples live on the dedicated kamod-signals 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 drivers, API, and examples

Open the kamod-signals docs for getting started, storage showcases, and SSR cookie guides.

  • 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 signals drive UI chrome (theme, sidebar, locale), reflect state in accessible controls and keep preference changes predictable for keyboard and assistive tech users.

Before you ship

  • Try a fresh visit, a saved preference and an explicit reset.
  • Test your chosen driver with unavailable storage and unexpected stored values.
  • Verify that account or request-specific data cannot leak through shared module state.

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.

signals-package-reference.md
# Signals — integration reference

Reactive state with durable storage — localStorage, sessionStorage, IndexedDB, cookies, and memory — while keeping the familiar @preact/signals .value API.

Package: @kamod-ch/signals

## Choose an approach

Use persistence for values that should outlive their current render. Decide what the preference belongs to, how long it should survive and how a user can return to the default.

### A preference across visits

Choose local storage for small, non-sensitive browser preferences.

A saved browser value is neither account synchronization nor authoritative server data.

### A shorter-lived browser value

Choose session storage when the lifetime should follow that browser session.

Document which values survive refreshes and which should disappear on sign-out.

### Server-aware persistence

Review the cookie driver and createCookieContext at the request boundary.

Cookie scope, response headers and request isolation belong to the server integration.

## Installation

Install @kamod-ch/signals with @preact/signals and Preact as peer dependencies when using the package outside the Kamod UI monorepo.

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

## Starting example

```tsx
import { persistedSignal } from "@kamod-ch/signals";

export const theme = persistedSignal("theme", "dark", { storage: "local" });

// later
theme.value = "light";
```

### Give the saved value a clear owner

The key identifies browser storage, while the variable identifies the live signal. Decide whether it is a device preference or belongs to a signed-in account before sharing the key across screens or sessions.

```tsx
const theme = persistedSignal("theme", "dark", { storage: "local" });
```

### Read and write through one signal

Consumers should observe the same instance. Copying the value into unrelated local state creates a second owner that can become stale. Keep derived presentation derived, rather than synchronizing two writable values with effects.

```tsx
theme.value = "light";
```

### Provide an explicit reset

Reset restores the initial value; clearing a stored entry is a separate operation. Use an explicit user action for preference resets, and verify the behavior after reload rather than only checking the currently rendered label.

```tsx
theme.reset();
```

## Keep a preference local to a component

A custom hook can keep the persistent preference close to the settings control. The versioned key below leaves room for a future storage-shape change. This example is intentionally a harmless preference, not authentication or private user data.

File: src/components/DensityPreference.tsx

```tsx
import { usePersistedSignal } from "@kamod-ch/signals";
import { Button } from "@kamod-ch/ui";

export function DensityPreference() {
  const compact = usePersistedSignal("ui:compact:v1", false, { storage: "local" });
  return (
    <div class="flex flex-wrap gap-2">
      <Button aria-pressed={compact.value} onClick={() => { compact.value = !compact.value; }}>
        Compact spacing
      </Button>
      <Button variant="ghost" onClick={() => compact.reset()}>Restore default</Button>
    </div>
  );
}
```

Toggle, reload and restore the default. Then test without a stored value and with browser storage restricted. For SSR, ensure the first client render agrees with the server; browser-only preferences cannot automatically be known by the server.

## Ownership and environment

Name keys by purpose and treat a stored shape as a small data contract. Give defaults the same care as saved values. Decide whether clearing a preference means removing storage, resetting the live value, or both. For account-specific settings, define sign-out behavior and avoid silently applying one user's saved value to the next user on the device.

Storage availability, hydration and cross-tab updates are separate concerns. Test the chosen driver in the actual environment instead of assuming all storage behaves like synchronous local storage. If a preference changes layout, make the initial server and client output compatible and avoid a second competing copy of the value in component state.

Persistence needs a reset policy as well as a save path. Decide which values remain after sign-out, which expire with the session and which should never be stored at all.

## Troubleshooting

### A value changes back after refresh

Check the storage driver, key and write path. Make sure the code updates .value on the intended persisted signal rather than a separate temporary variable.

### A returning visitor sees invalid data

Inspect the saved shape from older releases. Validate or migrate before using it, and provide a reset path when the value cannot be recovered.

### Two controls disagree

Confirm that they are meant to share a source and review the driver's synchronization behavior. Matching variable names do not guarantee matching signal instances or storage keys.

## Resources

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

Sources & attribution

This integration guide accompanies @kamod-ch/signals, 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.