From 0b13f5f586e093c6865fdcbcf0997f0e5d121fbf Mon Sep 17 00:00:00 2001 From: cixzhang Date: Thu, 9 Jul 2026 11:14:54 +0000 Subject: [PATCH] refactor(vega): use useEffectEvent for VegaChart callbacks 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. --- packages/vega/package.json | 4 ++-- packages/vega/src/VegaChart.tsx | 31 +++++++++++++++++-------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/vega/package.json b/packages/vega/package.json index a23703f73d2e..7a6ab8968c22 100644 --- a/packages/vega/package.json +++ b/packages/vega/package.json @@ -47,8 +47,8 @@ "typecheck": "tsc --noEmit" }, "peerDependencies": { - "react": ">=19.0.0", - "react-dom": ">=19.0.0", + "react": ">=19.2.0", + "react-dom": ">=19.2.0", "vega": ">=6.0.0", "vega-lite": ">=6.0.0" }, diff --git a/packages/vega/src/VegaChart.tsx b/packages/vega/src/VegaChart.tsx index 7d3d9e383757..a031563e24cc 100644 --- a/packages/vega/src/VegaChart.tsx +++ b/packages/vega/src/VegaChart.tsx @@ -9,7 +9,7 @@ * SYNC: When modified, update /packages/vega/README.md */ -import React, {useEffect, useRef} from 'react'; +import React, {useEffect, useEffectEvent, useRef} from 'react'; import {parse, View} from 'vega'; import {compile} from 'vega-lite'; import {parseSchema} from './schema'; @@ -36,8 +36,9 @@ import type {VegaChartProps, VegaSpec, VegaLiteSpec} from './types'; * it when `spec`, `parseConfig`, `parseOptions`, or `viewOptions` changes, * and calls `view.finalize()` on cleanup to release all runtime resources. * - * Callbacks (`onReady`, `onError`) are stable across renders via refs -- - * you don't need to memoize them. Pass stable references (or `useMemo`) + * Callbacks (`onReady`, `onError`) are non-reactive Effect Events -- they + * always see the latest props and never re-run the View lifecycle, so you + * don't need to memoize them. Pass stable references (or `useMemo`) * for `parseConfig`, `parseOptions`, `viewOptions`, and `data` to avoid * unnecessary re-renders. * @@ -80,17 +81,21 @@ export function VegaChart({ className, style, ref, - onReady, - onError, + onReady: onReadyProp, + onError: onErrorProp, ...props }: VegaChartProps) { const containerRef = useRef(null); - // Keep callbacks in refs so they don't need to be in the dep array. - const onReadyRef = useRef(onReady); - const onErrorRef = useRef(onError); - onReadyRef.current = onReady; - onErrorRef.current = onError; + // The Effect fires these callbacks without treating them as reactive + // dependencies, so the View isn't torn down and rebuilt when a parent + // passes fresh inline callbacks on every render. + const onReady = useEffectEvent((view: View) => { + onReadyProp?.(view); + }); + const onError = useEffectEvent((error: Error) => { + onErrorProp?.(error); + }); useEffect(() => { const container = containerRef.current; @@ -103,9 +108,7 @@ export function VegaChart({ const fail = (err: unknown) => { if (!cancelled) { - onErrorRef.current?.( - err instanceof Error ? err : new Error(String(err)), - ); + onError(err instanceof Error ? err : new Error(String(err))); } }; @@ -149,7 +152,7 @@ export function VegaChart({ return; } if (view) { - onReadyRef.current?.(view); + onReady(view); } }) .catch(fail);