Skip to content

refactor(vega): use useEffectEvent for VegaChart callbacks#3735

Draft
cixzhang wants to merge 1 commit into
mainfrom
navi/refactor/use-effect-event
Draft

refactor(vega): use useEffectEvent for VegaChart callbacks#3735
cixzhang wants to merge 1 commit into
mainfrom
navi/refactor/use-effect-event

Conversation

@cixzhang

@cixzhang cixzhang commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What

Replace the onReady/onError ref-callback pattern in VegaChart with useEffectEvent.

Before, the callbacks were stashed in refs and reassigned every render so they could be read inside the View-lifecycle Effect without becoming dependencies:

const onReadyRef = useRef(onReady);
const onErrorRef = useRef(onError);
onReadyRef.current = onReady;
onErrorRef.current = onError;
// ...read as onReadyRef.current?.(view) inside useEffect

Now they're non-reactive Effect Events:

const onReady = useEffectEvent((view: View) => onReadyProp?.(view));
const onError = useEffectEvent((error: Error) => onErrorProp?.(error));
// ...called as onReady(view) inside useEffect

Behavior is identical: the callbacks always see the latest props, and passing fresh inline callbacks still never re-runs the effect or rebuilds the View. This just expresses the intent with the purpose-built primitive instead of manual ref bookkeeping.

Why only VegaChart?

useEffectEvent has a strict contract — an Effect Event may only be called from inside an Effect, never from an event handler, a useCallback, or during render. I audited every ref-callback site in the codebase against that rule:

Site Callback invoked from Fits useEffectEvent?
packages/vega/src/VegaChart.tsx the View-lifecycle useEffect only converted here
Toast.tsx (onDismiss) setTimeout reachable from focus/mouse handlers ❌ not effect-only
Outline/useScrollSpy.ts (onActiveIdChange) Effect and the lockActiveId click handler ❌ mixed
Slider.tsx (onChangeEnd) pointer/keyboard handlers ❌ event handler
hooks/useLongPress.ts (onLongPress) touch handler (useCallback) ❌ event handler
Chat/useChatNewMessages.ts (onResize) ResizeObserver in a useCallback ❌ not an effect
Table/.../useTableSortableState.tsx (onSortChange) read during render, passed to another hook ❌ forbidden
Chat/useSpeechRecognition.ts, useChatDictation.ts (callbacksRef) speech-recognition event handlers ❌ event handlers

Only VegaChart reads its ref callbacks exclusively inside an Effect, so it's the only place where useEffectEvent is correct rather than a rules-of-hooks violation. The other sites are legitimately using refs precisely because they fire from event handlers — converting them would trip the "only call from Effects" rule. Leaving them as-is is intentional.

Peer dependency bump

useEffectEvent is stable as of React 19.2. This raises @astryxdesign/vega's react/react-dom peer floor from >=19.0.0 to >=19.2.0. (@astryxdesign/vega is private / canary-only, so no changeset — the changeset gate rejects one for a non-releasable package.)

Testing

  • pnpm -F @astryxdesign/vega typecheck
  • pnpm -F @astryxdesign/vega build ✓ (declaration emit)
  • pnpm lint ✓ (repo checks + eslint; the 2 remaining warnings are pre-existing in unrelated files)
  • No tests exist for the vega package; behavior is unchanged.

Replace the onReady/onError ref indirection with useEffectEvent. Both
callbacks are invoked only inside the View-lifecycle Effect, so they're a
textbook fit for Effect Events: they always see the latest props without
being reactive dependencies, so fresh inline callbacks no longer risk
re-running the effect and rebuilding the View.

Raises the react/react-dom peer floor to >=19.2.0, where useEffectEvent
is stable.
@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Error Error Jul 9, 2026 11:16am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 9, 2026
@josephfarina

Copy link
Copy Markdown
Contributor

CI is red across build/lint/test/docsite-test/smoke-test, but it's all one root cause: the peerDependencies bump to react/react-dom >=19.2.0 wasn't reflected in the lockfile, so pnpm install --frozen-lockfile fails the frozen-lockfile check (lockfile still says >=19.0.0). A lockfile update should green everything.

The callback migration itself looks clean — onReady/onError go through useEffectEvent, the View lifecycle deps are unchanged, and the fail() path still wraps non-Errors. Note useEffectEvent is React 19.2+, which is why the peerDep bump is needed.

@cixzhang

Copy link
Copy Markdown
Contributor Author

CI red

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants