Repository navigation
fix(react): apply controlled updates against the latest snapshot - #210
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Controlled useColor/useMultiColor read `state` from the render closure, so
two updates in one tick (e.g. setChannel('l') then setChannel('c')) each
started from the same stale value and the second overwrote the first.
- Add an internal `useLatestSnapshot` (color-store.ts): updates write a
pending snapshot keyed to the committed value; reads return it until the
controlled prop (or committed uncontrolled state) changes. Refs are only
touched in event handlers and a layout effect, never during render.
- onChange timing: both hooks now call onChange synchronously inside the
setter, before React re-renders, in controlled and uncontrolled modes,
once per effective update and never for no-ops. Uncontrolled useMultiColor
previously emitted from a post-commit effect; it now computes the event
eagerly from the latest snapshot and still commits via
setInternalModel(prev => ...). Synchronous emission makes it StrictMode-
safe without the post-commit sequence-dedupe queue, which is removed.
- Move single-color transitions into driver reducers (setColorRequested,
setColorChannel, setColorActiveGamut, setColorActiveView) and use them
from useColor; no-op semantics are unchanged.
- removeColor drops its unused `source` param (hard cut-over; removal has
no entry left to carry a source).
- useColor hex/rgb/hsl/hsv/oklch are lazy getters cached per requested
color, so the non-reactive <Color> provider no longer converts every render.
- Docs: document onChange timing and controlled composition.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
setColorRequested and setColorChannel returned the old state when only the requested gamutMapMethod differed, so the override was ignored. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…fects useLatestSnapshot refreshed its committed ref in a layout effect, which runs after descendants' layout effects, so a child calling a setter from useLayoutEffect composed onto the previous controlled state (l=0.2 instead of 0.8). Sync the ref in an insertion effect, which React runs for the whole tree before any layout effect; no ref is touched during render. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…effects useLatestSnapshot refreshed committedRef in an insertion effect but still dropped the pending write in its layout effect, which runs after descendants'. When a controlled parent rejected an update, a child that called a setter from useLayoutEffect on that commit composed onto the rejected write (l=0.9,h=90 instead of l=0.6,h=90), in useColor and useMultiColor. Expire the pending write in the same insertion effect; same-tick composition and reject-then-accept are unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
7e9b72f to
546f702
Compare

Controlled useColor/useMultiColor read
statefrom the render closure, sotwo updates in one tick (e.g. setChannel('l') then setChannel('c')) each
started from the same stale value and the second overwrote the first.
useLatestSnapshot(color-store.ts): updates write apending snapshot keyed to the committed value; reads return it until the
controlled prop (or committed uncontrolled state) changes. Refs are only
touched in event handlers and a layout effect, never during render.
setter, before React re-renders, in controlled and uncontrolled modes,
once per effective update and never for no-ops. Uncontrolled useMultiColor
previously emitted from a post-commit effect; it now computes the event
eagerly from the latest snapshot and still commits via
setInternalModel(prev => ...). Synchronous emission makes it StrictMode-
safe without the post-commit sequence-dedupe queue, which is removed.
setColorChannel, setColorActiveGamut, setColorActiveView) and use them
from useColor; no-op semantics are unchanged.
sourceparam (hard cut-over; removal hasno entry left to carry a source).
color, so the non-reactive provider no longer converts every render.
Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with
pnpm buildandpnpm test; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.🤖 Generated with Claude Code