feat(state-api): add @effectionx/state-api with useState#193
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a new Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Possibly related PRs
Suggested reviewers
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
commit: |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
effect-ts/effect-runtime.tseffect-ts/package.jsonpackage.jsonpnpm-workspace.yamlstate-api/README.mdstate-api/mod.tsstate-api/package.jsonstate-api/state.test.tsstate-api/state.tsstate-api/tsconfig.jsontsconfig.json
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
state-api/README.mdstate-api/state.test.tsstate-api/state.ts
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (9)
package.jsonpnpm-workspace.yamlstate-api/README.mdstate-api/mod.tsstate-api/package.jsonstate-api/state.test.tsstate-api/state.tsstate-api/tsconfig.jsontsconfig.json
cowboyd
left a comment
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (3)
state-api/package.jsonstate-api/state.tsstate-api/tsconfig.json
|
@cowboyd how does this version look? |
d70e6a7 to
117f77a
Compare
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).
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).useStateprovides a reducer-like API inspired by React'suseReducerand TC39 Signals, where state transitions are named, type-safe, and interceptable.Approach
New package
@effectionx/state-apiwith a single entry point:useState<T>(initial, reducers?).useState()call creates its owncreateApiinstance from@effectionx/context-api, so middleware installed on one container does not affect others.set(value),update(fn),get()— all returnOperation<T>for the resulting state.(state, ...args) => newState; thestateargument is injected and callers pass only the remaining args.State<T>implementsStream<T, void>for subscribing to changes viaeach.around()from@effectionx/context-api, keeping the package on the monorepo's stable Effection version instead of pinning the repo toeffection/experimental.Validation
node --env-file=.env --test state-api/state.test.tspnpm check:tsrefspnpm lintpnpm fmt:checkpnpm checkSummary by CodeRabbit
New Features
Documentation
Tests
Chores