{"_id":"@dmytromykhailiuk/preact-signal-redux","_rev":"2-3c1f8a4d508f9c7d8fe7ca39ce493c5f","name":"@dmytromykhailiuk/preact-signal-redux","dist-tags":{"latest":"1.0.0"},"versions":{"0.1.0":{"name":"@dmytromykhailiuk/preact-signal-redux","version":"0.1.0","keywords":["preact","preact-signals","signals","redux","state-management","store","middleware","redux-devtools","devtools","time-travel","reducer","actions","reactive","zero-rerender","typescript","typed"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/preact-signal-redux@0.1.0","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/preact-signal-redux#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-redux/issues"},"dist":{"shasum":"aa9b3fbe55e7efaf05292174e61f901eb209f737","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/preact-signal-redux/-/preact-signal-redux-0.1.0.tgz","fileCount":9,"integrity":"sha512-gonJKS0MlxGNPSA5FNK9t67XBbymer1TfxIYx4FomVa1okmSXz+fN9Vd+/T2Y0LV+dkTIEnHMCjS3iGai+cqnw==","signatures":[{"sig":"MEUCIQCl8eSUF2xyaykdaq1luoAeNdNvHGA6/XHXi4U58GH7xwIgdGTuiCNw7bwgPE+3njJosuC63XEWwHxBXeYGlach1MY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":75936},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"ad44c9247b30e3182d8630162b90dfae5417489d","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","format":"biome format --write .","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","playground":"vite --config vite.playground.config.ts","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/preact-signal-redux.git","type":"git"},"_npmVersion":"11.6.2","description":"Redux-style state management on Preact signals — createAction, createReducer and createSignalStore with classic redux-compatible middleware and full Redux DevTools time travel. Framework-agnostic, zero re-render.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vite":"^5.4.11","jsdom":"^25.0.1","preact":"^10.25.4","vitest":"^2.1.8","typescript":"^5.7.3","@types/node":"^22.10.5","@biomejs/biome":"^1.9.4","@preact/signals":"^2.0.1","@preact/preset-vite":"^2.10.1","@preact/signals-core":"^1.8.0"},"peerDependencies":{"@preact/signals-core":"^1.6.0"},"_npmOperationalInternal":{"tmp":"tmp/preact-signal-redux_0.1.0_1785097388387_0.45737316724280475","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@dmytromykhailiuk/preact-signal-redux","version":"1.0.0","description":"Redux-style state management on Preact signals — createAction, createReducer and createSignalStore with classic redux-compatible middleware and full Redux DevTools time travel. Framework-agnostic, zero re-render.","type":"module","sideEffects":false,"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","keywords":["preact","preact-signals","signals","redux","state-management","store","middleware","redux-devtools","devtools","time-travel","reducer","actions","reactive","zero-rerender","typescript","typed"],"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","playground":"vite --config vite.playground.config.ts","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","lint":"biome check .","lint:fix":"biome check --write .","format":"biome format --write .","prepublishOnly":"npm run build"},"engines":{"node":">=18"},"peerDependencies":{"@preact/signals-core":"^1.6.0"},"devDependencies":{"@biomejs/biome":"^1.9.4","@preact/preset-vite":"^2.10.1","@preact/signals":"^2.0.1","@preact/signals-core":"^1.8.0","@types/node":"^22.10.5","jsdom":"^25.0.1","preact":"^10.25.4","tsup":"^8.3.5","typescript":"^5.7.3","vite":"^5.4.11","vitest":"^2.1.8"},"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/preact-signal-redux.git"},"bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-redux/issues"},"homepage":"https://dmytromykhailiuk.github.io/preact-signal-redux/","gitHead":"0c496573979214920f09c97d07cb2292c8a73390","_id":"@dmytromykhailiuk/preact-signal-redux@1.0.0","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-oVJ2az5qytC35wLfNLtYb37TsVStJK2g4DujwZAqsnP+20JXZL0oS3KPk/im7/wW4IvrB6AfQ3cI8r9AHda0fw==","shasum":"a49645111550010968eb202dd0bf31c4ed19c2cc","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/preact-signal-redux/-/preact-signal-redux-1.0.0.tgz","fileCount":9,"unpackedSize":75929,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICqW+7NnGzmhCB6fPHRxWLcJAWFotmNIWSLWWud21vG7AiEAidGvfaOOSiBjhmm8/c5KrYJdnkfXE1WlF3lPpacSSN4="}]},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"directories":{},"maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/preact-signal-redux_1.0.0_1786639117309_0.5700907493004059"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-26T20:23:08.300Z","modified":"2026-08-13T16:38:37.656Z","0.1.0":"2026-07-26T20:23:08.518Z","1.0.0":"2026-08-13T16:38:37.445Z"},"bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-redux/issues"},"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","homepage":"https://dmytromykhailiuk.github.io/preact-signal-redux/","keywords":["preact","preact-signals","signals","redux","state-management","store","middleware","redux-devtools","devtools","time-travel","reducer","actions","reactive","zero-rerender","typescript","typed"],"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/preact-signal-redux.git"},"description":"Redux-style state management on Preact signals — createAction, createReducer and createSignalStore with classic redux-compatible middleware and full Redux DevTools time travel. Framework-agnostic, zero re-render.","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"readme":"# @dmytromykhailiuk/preact-signal-redux\n\nRedux-style state management built entirely on [Preact signals](https://preactjs.com/guide/v10/signals/): typed actions, builder-based reducers and a store that **is** a `ReadonlySignal` — with classic redux-compatible middleware and full Redux DevTools time travel. Framework-agnostic, zero re-render.\n\n> **Docs:** the full documentation lives in [`index.html`](https://dmytromykhailiuk.github.io/preact-signal-redux/) — this README is the short form.\n\n**Why this exists.** Redux gives you predictable state transitions, an inspectable action log and a huge middleware ecosystem. Signals give you fine-grained, zero re-render UI updates. This package glues the two: state transitions stay pure and action-driven, while every consumer reads the store as a plain signal — no `useSelector`, no `connect`, no re-renders.\n\n- The store extends `ReadonlySignal<S>` — bind it (or `computed` projections of it) directly in JSX.\n- Middleware uses the classic curried redux signature `(api) => (next) => (action)` — redux-thunk / redux-logger style middleware plugs in unchanged.\n- Redux DevTools support is itself a middleware (`createDevToolsMiddleware`): each store connects under its **own instance name**, with full time travel (jump, rollback, reset, commit) and an `enabled` switch for production builds.\n- No global mutable state: actions are delivered to whoever needs them (e.g. [`@dmytromykhailiuk/preact-signal-effects`](https://github.com/dmytromykhailiuk/preact-signal-effects)) through middleware, not through a shared global signal.\n\n## Install\n\n```sh\nnpm install @dmytromykhailiuk/preact-signal-redux @preact/signals-core\n```\n\nThe only peer dependency is `@preact/signals-core`. In a Preact app you will normally have `@preact/signals` installed — it re-exports the same signal primitives from `@preact/signals-core`, so stores created by this package bind directly in Preact JSX. Outside Preact (vanilla TS, workers, tests) the package works with `@preact/signals-core` alone.\n\n## Quick start\n\n```tsx\nimport { computed } from \"@preact/signals\";\nimport {\n  createAction,\n  createDevToolsMiddleware,\n  createReducer,\n  createSignalStore,\n} from \"@dmytromykhailiuk/preact-signal-redux\";\n\ninterface CounterState {\n  count: number;\n}\n\nconst increment = createAction(\"[COUNTER] increment\");\nconst addAmount = createAction<number>(\"[COUNTER] addAmount\");\n\nconst counterStore$ = createSignalStore<CounterState>(\n  createReducer((builder) =>\n    builder\n      .addCase(increment, (state) => ({ count: state.count + 1 }))\n      .addCase(addAmount, (state, action) => ({ count: state.count + action.payload })),\n  ),\n  { count: 0 },\n  { middlewares: [createDevToolsMiddleware({ name: \"counter\" })] },\n);\n\n// The store IS a signal — bind it directly, never unwrap .value in render:\nconst count$ = computed(() => counterStore$.value.count);\n\nfunction Counter() {\n  return (\n    <button onClick={() => counterStore$.dispatch(increment())}>\n      Count: {count$}\n    </button>\n  );\n}\n```\n\nDispatch an action → the reducer produces the next state → the signal updates → every bound DOM node updates in place. No component re-renders.\n\n## The signal rules\n\n1. **The store is a `ReadonlySignal<S>`.** Read `store.value` inside `computed`/`effect`/other reactive contexts to subscribe; bind the resulting signals directly to JSX.\n2. **`getState()` (and `peek()`) are non-reactive.** Use them in event handlers, middleware and effects when you need the current state without subscribing — exactly like redux's `store.getState()`.\n3. **There is no `subscribe()`.** Signals already are the subscription primitive:\n\n```ts\nimport { effect } from \"@preact/signals-core\";\n\nconst dispose = effect(() => {\n  console.log(\"state changed:\", counterStore$.value);\n});\n```\n\n## `createAction`\n\n```ts\nconst initialization = createAction(\"[APP] initialization\");        // Action<void>\nconst addTodo = createAction<{ text: string }>(\"[TODOS] add\");      // Action<{ text: string }>\n\naddTodo({ text: \"hi\" });  // { type: \"[TODOS] add\", payload: { text: \"hi\" } }\naddTodo.type;             // \"[TODOS] add\"\n`${addTodo}`;             // \"[TODOS] add\" — toString() returns the type\naddTodo.match(action);    // type guard: narrows action to Action<{ text: string }>\n```\n\n- Void creators take no arguments; payload creators require exactly one, fully typed.\n- `match` is the RTK-style type guard — handy in custom middleware and effects.\n\n## `createReducer`\n\nBuilder-based, fully inferred case reducers:\n\n```ts\nconst reducer = createReducer<State>((builder) =>\n  builder\n    .addCase(addTodo, (state, action) => ({\n      ...state,\n      todos: [...state.todos, action.payload],       // payload is typed\n    }))\n    // several creators may share one case — the action is a typed union:\n    .addCase(imageUploaded, imageUploadFailed, (state, action) => ({\n      ...state,\n      status: imageUploaded.match(action) ? \"uploaded\" : \"error\",\n    }))\n    // runs only when no case matched:\n    .addDefaultCase((state, action) => state),\n);\n```\n\nSemantics:\n\n- **Multiple creators per case** — pass any number of creators before the case reducer; the action parameter is the union of their action types.\n- **Sequential same-type cases** — if several `addCase` registrations target the same type, they run in registration order, each receiving the previous one's result.\n- **Unknown actions** return the state unchanged (same reference), so signal subscribers do not fire.\n- `addMatcher` (predicate-based cases) is not included yet; use a default case + `creator.match` if you need it today.\n\n## `createSignalStore`\n\n```ts\nconst store$ = createSignalStore(reducer, initialState, {\n  // classic redux middleware, leftmost outermost — devtools is a middleware too:\n  middlewares: [thunk, logger, createDevToolsMiddleware({ name: \"my-store\" })],\n  modifyInitialState: (state) => rehydrate(state),  // transform initial state once\n  afterUpdate: ({ action, prevState, newState }) => {},  // called after every reduced dispatch\n});\n```\n\n| Member | Meaning |\n| --- | --- |\n| `store$.value` | Reactive read (subscribe from `computed`/`effect`; bind projections in JSX) |\n| `store$.getState()` / `store$.peek()` | Non-reactive read of the current state |\n| `store$.dispatch(action)` | Runs the middleware chain + reducer; returns the action (redux semantics) |\n\nOptions:\n\n- **`middlewares`** — applied left-to-right around the reducer, exactly like `applyMiddleware`. See below.\n- **`modifyInitialState`** — runs once before the store is created; useful for rehydrating persisted state.\n- **`afterUpdate`** — invoked after each dispatched action has been reduced and written to the signal, with `{ action, prevState, newState }`. It is **not** invoked on DevTools time travel (time travel is a silent state write, not a dispatch).\n\n## Middleware\n\nThe classic curried redux signature, structurally compatible with the redux ecosystem:\n\n```ts\nimport type { Middleware } from \"@dmytromykhailiuk/preact-signal-redux\";\n\nconst logger: Middleware<State> = ({ getState, dispatch }) => (next) => (action) => {\n  console.log(\"dispatching\", action.type, \"state before:\", getState());\n  const result = next(action);           // pass along — or don't, to swallow the action\n  console.log(\"state after:\", getState());\n  return result;\n};\n```\n\nWhat middleware can do — identical to redux:\n\n- **Pass the action on** with `next(action)`; the innermost `next` is the reducer step.\n- **Swallow** the action by not calling `next`.\n- **Transform** it — call `next(otherAction)`.\n- **Dispatch more actions** via `api.dispatch(...)` — this re-enters the **full** chain from the top.\n- **Read state** via `api.getState()` — before `next` it is the pre-action state, after `next` the post-action state.\n\nRules enforced at runtime (redux parity):\n\n- Reducers may not dispatch — doing so throws `\"Reducers may not dispatch actions.\"`.\n- Dispatching while the chain is still being constructed (from the middleware's outer `(api) => ...` body) throws.\n\n### Using ecosystem middleware\n\nThe types are structural, so redux-thunk-style and redux-logger-style middleware work as-is. Here is a complete, realistic thunk setup — async data fetching with request de-duplication:\n\n```ts\nimport type { Dispatch, Middleware } from \"@dmytromykhailiuk/preact-signal-redux\";\n\n// The classic thunk middleware — identical to redux-thunk's core:\ntype Thunk<S, R = void> = (dispatch: Dispatch, getState: () => S) => R;\n\nconst thunk: Middleware<State> = (api) => (next) => (action) =>\n  typeof action === \"function\" ? action(api.dispatch, api.getState) : next(action);\n\n// A typed helper so thunks dispatch without casts:\nconst store$ = createSignalStore(reducer, initialState, { middlewares: [thunk] });\nconst dispatchThunk = <R,>(t: Thunk<State, R>): R => (store$.dispatch as any)(t);\n\n// Actions for the request lifecycle:\nconst userRequested = createAction<{ id: string }>(\"[USERS] requested\");\nconst userLoaded = createAction<{ id: string; user: User }>(\"[USERS] loaded\");\nconst userFailed = createAction<{ id: string; message: string }>(\"[USERS] failed\");\n\n// The thunk itself — reads state, awaits IO, dispatches results:\nconst fetchUser =\n  (id: string): Thunk<State, Promise<void>> =>\n  async (dispatch, getState) => {\n    if (getState().users[id]) return;           // already cached — skip the request\n    dispatch(userRequested({ id }));\n    try {\n      const response = await fetch(`/api/users/${id}`);\n      if (!response.ok) throw new Error(response.statusText);\n      dispatch(userLoaded({ id, user: await response.json() }));\n    } catch (error) {\n      dispatch(userFailed({ id, message: String(error) }));\n    }\n  };\n\nawait dispatchThunk(fetchUser(\"42\"));\nstore$.getState().users[\"42\"]; // loaded (or an error recorded by userFailed)\n```\n\nEvery nested `dispatch` inside the thunk re-enters the full middleware chain, so loggers, the DevTools middleware and effects middleware all observe the lifecycle actions.\n\n## Redux DevTools\n\nDevTools support is a **middleware** — register it like any other, one per store; every store gets its **own instance** in the DevTools UI:\n\n```ts\nimport { createDevToolsMiddleware } from \"@dmytromykhailiuk/preact-signal-redux\";\n\nconst counter$ = createSignalStore(counterReducer, counterInitial, {\n  middlewares: [createDevToolsMiddleware({ name: \"app/counter\" })],\n});\nconst todos$ = createSignalStore(todosReducer, todosInitial, {\n  middlewares: [\n    logger,\n    createDevToolsMiddleware({ name: \"app/todos\", maxAge: 100, trace: true }),\n  ],\n});\n```\n\n- **`enabled`** — set `enabled: false` and the middleware stays a transparent pass-through even though it is registered; no DevTools connection is made. Perfect for production builds:\n\n  ```ts\n  createDevToolsMiddleware({ name: \"app/todos\", enabled: import.meta.env.DEV })\n  ```\n\n- **`name`** — omit it to auto-generate a unique name (`signal-store-1`, `signal-store-2`, ...). All other option fields are passed straight to the extension's `connect()`.\n- **Placement** — put it near the **end** of the chain: it records the action + post-reducer state after `next(action)` returns, so actions swallowed by outer middleware never appear in the log.\n- When the extension is missing (or during SSR) the middleware is a pass-through — zero overhead, no errors.\n\n### Time travel\n\nThe integration is bidirectional. Dispatches are recorded (`send`), and DevTools commands are applied to the store:\n\n| DevTools command | Effect on the store |\n| --- | --- |\n| Jump to action / state | State is written silently; signal subscribers (and the UI) update |\n| Rollback | State restored to the last committed baseline |\n| Reset | State restored to the initial state |\n| Commit | Current state becomes the new baseline |\n| Import state | Last computed state from the imported session is applied |\n| Pause recording | Dispatches stop being sent until resumed |\n| Dispatch from the DevTools UI | Goes through the real middleware chain |\n\nSemantics worth knowing:\n\n- Time travel writes state **silently** via the store-provided `api.replaceState`: it bypasses the reducer and the middleware chain, does not call `afterUpdate`, and does not re-trigger side effects (an attached effects middleware never republishes during time travel). This is deliberate — replaying history must not replay its side effects.\n- Time travel serializes state through JSON, so keep store state JSON-serializable if you use DevTools.\n\n## Differences from redux / redux-toolkit\n\n| | redux / RTK | preact-signal-redux |\n| --- | --- | --- |\n| State container | plain object + `subscribe()` | `ReadonlySignal<S>` — signals are the subscription |\n| Reading state in UI | `useSelector` (re-renders) | bind `computed` projections (zero re-render) |\n| Middleware | `(api) => (next) => (action)` | identical, structurally compatible |\n| `combineReducers` | yes | not needed — compose several stores and derive with `computed` |\n| Immer in reducers | RTK: yes | no — reducers must return new state |\n| DevTools | one global enhancer | a per-store middleware, separate instances |\n\nCompared to the original prototype this package grew out of: middleware is registered at creation (`options.middlewares`) instead of `registerMiddleware`, and the store no longer publishes actions into a global `actions$` signal — use an effects middleware (e.g. `@dmytromykhailiuk/preact-signal-effects`) to observe actions.\n\n## TypeScript\n\nEverything is inferred end-to-end: creator payloads (`createAction<P>`), case reducer actions (including unions for multi-creator cases), dispatch return types and store state. The package ships `.d.ts` for ESM and `.d.cts` for CJS. Requires TypeScript 5+.\n\n```ts\nimport type {\n  Action, AnyAction, ActionCreator, CaseReducer, Reducer,\n  Dispatch, Middleware, MiddlewareAPI,\n  SignalStore, CreateSignalStoreOptions, DevToolsMiddlewareOptions,\n} from \"@dmytromykhailiuk/preact-signal-redux\";\n```\n\n## License\n\nMIT © Dmytro Mykhailiuk\n","readmeFilename":"README.md"}