Skip to content

feat(state-api): add @effectionx/state-api with useState#193

Merged
taras merged 6 commits intomainfrom
feat/state-api
Apr 19, 2026
Merged

feat(state-api): add @effectionx/state-api with useState#193
taras merged 6 commits intomainfrom
feat/state-api

Conversation

@taras
Copy link
Copy Markdown
Member

@taras taras commented Mar 14, 2026

Motivation

Add a reactive state container primitive to EffectionX that supports typed reducer actions and per-container middleware. This fills a gap between the existing @effectionx/signals (synchronous mutations, no middleware) and the API pattern used by fetch/process/fs (stateless operations).

useState provides a reducer-like API inspired by React's useReducer and TC39 Signals, where state transitions are named, type-safe, and interceptable.

Approach

New package @effectionx/state-api with a single entry point: useState<T>(initial, reducers?).

  • Each useState() call creates its own createApi instance from @effectionx/context-api, so middleware installed on one container does not affect others.
  • Built-in operations: set(value), update(fn), get() — all return Operation<T> for the resulting state.
  • User-defined reducer actions follow (state, ...args) => newState; the state argument is injected and callers pass only the remaining args.
  • State<T> implements Stream<T, void> for subscribing to changes via each.
  • Middleware uses around() from @effectionx/context-api, keeping the package on the monorepo's stable Effection version instead of pinning the repo to effection/experimental.

Validation

  • node --env-file=.env --test state-api/state.test.ts
  • pnpm check:tsrefs
  • pnpm lint
  • pnpm fmt:check
  • pnpm check

Summary by CodeRabbit

  • New Features

    • Added a new State API package providing reactive state management with useState, typed reducers, stream subscriptions, and middleware support.
  • Documentation

    • Added comprehensive State API docs with usage examples, middleware patterns, and scoping semantics.
  • Tests

    • Added extensive test coverage validating state operations, streams, reducers, and middleware behavior.
  • Chores

    • Registered the State API package in the workspace and added its package manifest and build configuration.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new state-api package to the monorepo: implements a typed, middleware-capable reactive state container built on Effection, includes tests, documentation, TypeScript project config, and package manifest; and registers the package in workspace/tsconfig.

Changes

Cohort / File(s) Summary
Workspace & TS Config
pnpm-workspace.yaml, tsconfig.json, state-api/tsconfig.json
Registers state-api in the pnpm workspace and TS project references; configures state-api build outputs and excludes tests.
Package Manifest
state-api/package.json
Adds @effectionx/state-api package manifest with exports map (types/development/import/default), peer/dev/workspace deps, files to publish, and ESM config. Review export map and peerDependency bounds.
Core Implementation
state-api/state.ts
New implementation of useState providing Stream-based state, set/update/get, typed reducers, reserved-name validation, middleware via around(), signal lifecycle/cleanup, and API construction using createApi. High-density logic — focus review here.
Public Exports
state-api/mod.ts
Re-exports useState, State, and ReducerMap from state.ts.
Tests
state-api/state.test.ts
Comprehensive test suite covering basic ops, stream emissions, reducers, middleware behavior, and scoping.
Docs
state-api/README.md
Adds usage documentation with examples for basic state, reducers, streams, and middleware semantics.

Sequence Diagram

sequenceDiagram
    actor User
    participant useState as "useState Operation"
    participant Signal as "Effection Signal"
    participant API as "createApi / Core Handlers"
    participant Middleware as "around() Middleware"
    participant Subscriber as "Stream Subscriber"

    User->>useState: invoke useState(initial, reducers?)
    useState->>Signal: createSignal()
    useState->>API: build core handlers (set/update/get + reducers)
    useState->>User: return State object (Stream + ops + around)

    User->>State: call operation (e.g., set(newValue))
    State->>Middleware: around() intercepts operation
    Middleware->>API: invoke core handler (possibly transformed)
    API->>Signal: update internal state ref and send value
    Signal->>Subscriber: emit new state value
    Subscriber->>User: receive update
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Suggested reviewers

  • cowboyd

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Policy Compliance ❓ Inconclusive Policy files (.policies/ directory and referenced markdown files) do not exist in the repository, making policy validation impossible. Create .policies/ directory with index.md, package-json-metadata.md, version-bump.md, and no-agent-marketing.md files to enable policy validation.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding a new state-api package with the useState feature to EffectionX.
Description check ✅ Passed The description follows the template structure with clear Motivation and Approach sections, explaining both the problem and the implementation details comprehensively.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/state-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Mar 14, 2026

Open in StackBlitz

npm i https://pkg.pr.new/thefrontside/effectionx/@effectionx/state-api@193

commit: b8a4266

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@state-api/README.md`:
- Around line 24-53: The examples use yield* and identifiers like useState,
todos, and counter without showing the surrounding generator runtime, making
them non-copy/paste; either wrap each example in a complete runnable snippet
(e.g., run(function* () { /* example code using yield*, useState, todos, counter
*/ })) or add a one-line header to each block stating "continuation from
previous snippet" and include the minimal setup (e.g., const todos = yield*
useState(...) or a comment like // assumes run(generator) and todos already
defined) so readers can run examples directly; update the Typed reducers block
and the other affected blocks (mentions of todos, counter, yield*) accordingly.

In `@state-api/state.test.ts`:
- Line 2: Replace the timing-based sleep(0) test sync with a deterministic
"subscriber ready" resolver: in the tests that import sleep, withResolvers,
spawn, and each (notably the update() and reducer-stream tests referenced),
create a resolver via withResolvers() (e.g. const { resolve: markReady, promise:
ready } = withResolvers()), pass or expose that resolver to the subscriber
setup, have the subscriber call markReady() once it is subscribed/consuming, and
have the producer/creator await ready before emitting/dispatching; remove all
sleep(0) calls and use this ready promise to gate producer actions so tests
deterministically synchronize without timing-based sleeps.

In `@state-api/state.ts`:
- Around line 154-165: The reducer registration loop in state.ts assigns user
reducers into core using their keys, which allows user reducers named
set/update/get/around to shadow built-in operations; update the registration
code that iterates Object.keys(reducers) (the block that assigns core[key] =
function* (...) and the equivalent second reducer-registration block) to
validate each key against a denylist ["set","update","get","around"] and reject
— e.g., throw a clear Error — when a user attempts to register one of those
reserved names so built-in operations (core, api.operations) cannot be silently
overridden.
- Around line 171-177: The state object currently assigns the iterator by
copying the function reference ([Symbol.iterator]: signal[Symbol.iterator])
which loses the receiver and breaks stream semantics; replace that assignment
with a generator method on state that delegates to signal (i.e., implement a
*[Symbol.iterator]() { yield* signal; } style delegation) so the iterator runs
with the correct receiver and follows the established
.policies/stateless-streams.md pattern; update the code that builds state (the
object literal containing [Symbol.iterator], signal, and api.operations) to use
the generator method instead of copying signal[Symbol.iterator].
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b250e5d3-d6a3-4e86-9c48-9e66e21e8bd7

📥 Commits

Reviewing files that changed from the base of the PR and between 35b018c and b134887.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • effect-ts/effect-runtime.ts
  • effect-ts/package.json
  • package.json
  • pnpm-workspace.yaml
  • state-api/README.md
  • state-api/mod.ts
  • state-api/package.json
  • state-api/state.test.ts
  • state-api/state.ts
  • state-api/tsconfig.json
  • tsconfig.json

Comment thread state-api/README.md
Comment thread state-api/state.test.ts
Comment thread state-api/state.ts Outdated
Comment thread state-api/state.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@state-api/state.test.ts`:
- Around line 309-329: The test expectation is inverted: because createApi
composes middleware so each new around() wraps the prior one, the most recently
added middleware runs first; update the assertion in the "chains multiple
middleware" test to expect ["second","first"] (and optionally adjust the test
description) for the useState counter.around / counter.set case so it matches
the middleware composition behavior implemented in createApi.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 36de19c6-bd8f-43f4-9a18-ac80bcc4ee64

📥 Commits

Reviewing files that changed from the base of the PR and between b134887 and 0cc80fe.

📒 Files selected for processing (3)
  • state-api/README.md
  • state-api/state.test.ts
  • state-api/state.ts

Comment thread state-api/state.test.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@state-api/package.json`:
- Around line 25-27: The peer dependency range for effection in state-api's
package.json is too restrictive; update the "peerDependencies" entry for
"effection" to the repository standard range (e.g., "^3 || ^4") so it matches
other packages and restores broader compatibility—modify the "effection" value
under peerDependencies in package.json accordingly.

In `@state-api/state.ts`:
- Around line 182-183: Update the misleading inline comment near the spread of
api.operations: change the mention of the built-in operation name from "valueOf"
to "get" so it correctly reads something like "// Spread all operations (set,
update, get, + reducers)". Locate the comment adjacent to the spread of
api.operations and edit only the comment text; do not modify the code behavior
of the api.operations spread or any related function names.
- Around line 156-166: Validate the incoming reducers before wiring them: ensure
`reducers` is a non-null object (not an array) and that every property value is
a function, otherwise throw a clear initialization Error naming the invalid key
and its type; update the block that iterates `Object.keys(reducers)` (where
`core[key] = function* (...args: any[]): Operation<T>` and `ref.current =
reducer(ref.current, ...args)` are set) to perform these checks first and only
then bind the generator wrappers, so invalid shapes fail fast with descriptive
messages referencing the offending reducer key.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f6a6192d-88b2-4bda-9b86-1b3f9280c2bb

📥 Commits

Reviewing files that changed from the base of the PR and between 0cc80fe and 53f112f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • package.json
  • pnpm-workspace.yaml
  • state-api/README.md
  • state-api/mod.ts
  • state-api/package.json
  • state-api/state.test.ts
  • state-api/state.ts
  • state-api/tsconfig.json
  • tsconfig.json

Comment thread state-api/package.json
Comment thread state-api/state.ts Outdated
Comment thread state-api/state.ts Outdated
Copy link
Copy Markdown
Member

@cowboyd cowboyd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems quite legit!

The only problem is that there are way to many any ignores for it not to be a code smell. Something is not quite right with the types if there has to be that many. Usually, it means that there is either a lurking incoherence in the type model, or that the number of code-paths need to be reduced. Are we sure that these are necessary?

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@state-api/state.ts`:
- Around line 149-160: The reducer registration allows a user key of "__proto__"
which will mutate core's prototype and silently drop the reducer; update the
reserved names logic by adding "__proto__" to the reserved Set (the variable
reserved) or otherwise block keys equal to "__proto__" before assigning into
core (see the reducers loop and the core[key] = ... assignment) so that
"__proto__" cannot be registered as a reducer and the prototype cannot be
mutated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6a7a6824-c1f0-41c5-9733-6ec3046a3e61

📥 Commits

Reviewing files that changed from the base of the PR and between 53f112f and a6c6413.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • state-api/package.json
  • state-api/state.ts
  • state-api/tsconfig.json

Comment thread state-api/state.ts Outdated
@taras taras requested a review from cowboyd March 27, 2026 01:08
@taras
Copy link
Copy Markdown
Member Author

taras commented Mar 27, 2026

@cowboyd how does this version look?

taras added 6 commits April 19, 2026 13:19
Reactive state container with typed reducer actions and middleware
support, built on Effection's experimental createApi.

- useState<T>(initial) for simple state with set/update/get
- useState<T, R>(initial, reducers) for typed reducer actions
- All operations return Operation<T> (the resulting state)
- State<T> implements Stream<T> for subscribing to changes
- Per-container middleware via around() for any operation
- Middleware is scoped and does not leak across scope boundaries
- Guard against reserved reducer names (set, update, get, around)
  that would shadow built-in operations
- Add test for reserved name rejection
- Add policy-compliance comments on sleep(0) usage in stream tests
- Make README examples self-contained with run() wrappers

Note: kept [Symbol.iterator] reference copy pattern — it matches all
existing signal packages and works correctly because createSignal's
iterator is a closure. The *[Symbol.iterator]() delegation pattern
breaks stream semantics with each().
Align with monorepo-wide test runner migration (PR #204).
@taras taras merged commit 6982b1c into main Apr 19, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants