`onFocusChange` fires again after each render, also when the focused point does not change
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 初心者へのやさしさ
- 68/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- react, typescript
調査の方向性
Start with the focus restoration and callback logic in dist/renderer.js around lines 157–172, then follow how a definition update reaches the renderer. Check whether tests cover repeated renders with an unchanged focused-point key and focus groups. Done means callbacks fire when focus identity changes, but not when a render restores the same focus.
索引モデルが issue の本文から書いたものです。
説明
Version: @tanstack/charts 1.0.0, React adapter (@tanstack/charts/react)
Problem
After each render that had a focused point, the renderer calls onFocusChange and onFocusGroupChange again. It does this also when the focused point has the same key as before. The point is a new object each time, because restoreChartFocusPoint returns the point from the new scene.
In dist/renderer.js (about lines 157 to 172):
const nextFocusedPoints = trackedPointer
? resolvePointerFocus(...)
: previousFocusedPoint
? (() => {
const restored = restoreChartFocusPoint(presentedPoints, previousFocusedPoint);
return restored ? focusPointsForPoint(restored, presentedPoints) : [];
})()
: [];
const nextFocusedPoint = nextFocusedPoints[0] ?? null;
...
if (previousFocusedPoint || nextFocusedPoint || previousCursorPresentation) {
...
options.onFocusChange?.(nextFocusedPoint);
options.onFocusGroupChange?.(nextFocusedPoints);
}
A render occurs when the definition identity changes (definitionChanged in renderer.js, about line 686). In React, a definition that is not memoized, or a memo dependency that changes with the focus, causes a new render.
The usual React pattern is to put the focused point in state. With that pattern, the result is a render loop:
- The pointer focuses a point.
onFocusChange(point)callssetFocused(point). - React renders again. Something in the definition depends on the focus (for example a highlighted series), so the definition is a new object.
- The adapter calls
update. The renderer restores the same point as a new object and callsonFocusChange(newObject). setFocused(newObject)does not bail out, because the object is new. Go to step 2.
Minimal repro
import { useMemo, useState } from 'react';
import { defineChart, lineY, type ChartPoint } from '@tanstack/charts';
import { Chart } from '@tanstack/charts/react';
import { scaleLinear } from '@tanstack/charts/scales/linear';
import { tooltip } from '@tanstack/charts/tooltip';
const rows = [
{ x: 0, y: 1 },
{ x: 1, y: 3 },
{ x: 2, y: 2 },
];
let renders = 0;
export function Repro() {
const [focused, setFocused] = useState<ChartPoint | null>(null);
// The definition depends on the focus, which is a normal thing to do.
const definition = useMemo(
() =>
defineChart({
marks: [
lineY(rows, {
x: 'x',
y: 'y',
strokeWidth: focused ? 3 : 1,
}),
],
scales: { x: { scale: scaleLinear }, y: { scale: scaleLinear } },
tooltip,
}),
[focused],
);
console.log('render', ++renders);
return (
<Chart
definition={definition}
ariaLabel="Repro"
width={400}
height={200}
onFocusChange={setFocused}
/>
);
}
Move the pointer over a point and keep it there.
Expected
onFocusChange fires once when the focused point changes. It does not fire again when a render restores the same point.
Actual
onFocusChange fires after every render with a new point object for the same key. The component renders again and again while the pointer stays on the point.
Our workaround
We store a primitive, so React bails out of the state update:
const handleFocusChange = useCallback((point: ChartPoint | null) => {
setActiveIndex(point ? point.datumIndex : null);
}, []);
This works, but each consumer must know about it. Nothing in the docs tells you about the risk.
Suggested fix
Call onFocusChange and onFocusGroupChange only when the focus identity changes, for example with sameChartPointIdentity(previousFocusedPoint, nextFocusedPoint) and the same check on each point of the group. If a re-emit on each render is intentional, please document it in the React reference for onFocusChange, next to the memoization guidance from https://github.com/TanStack/charts/pull/111.
Related
- https://github.com/TanStack/charts/pull/111 (docs: React definition ownership and memoization)
Written by an agent (Claude Code, claude-opus-5-5).
- 主要言語
- TypeScript
- スター
- 842
- フォーク
- 53
- 平均マージ
- 4時間 40分
- マージ済み PR(30日)
- 16
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
TanStack/charts のほかの issue
-
[Bug] Focus guides: `url(#id)` paints ignore `idPrefix`対応中かも @getdownandcode が 3 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
TanStack/charts#211 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
[Bug] Printed charts keep their screen width: resize redraw waits for an animation frame対応中かも @piecyk が 3 日前に担当しました。 オープン
難易度 3/5 1〜2日 初心者へのやさしさ 25/100
メンテナーはふだん 1 日以内に返信
-
[Feature] Readable outside labels for crowded pie and donut charts対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
TanStack/charts#227 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 5/5 1週間以上 初心者へのやさしさ 42/100
TanStack/charts#216 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 57/100
メンテナーはふだん 1 日以内に返信
TanStack/charts の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 73/100
anomalyco/opencode#56380 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
-
BUG | `MCP 服务器` | 全局层行的停用/启用与删除报 404 server row not found,编辑保存会在 profile 层复制出重复行対応中かも @w6242652 が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
Chocobozzz/PeerTube#7832 ·
メンテナーはふだん 3 日以内に返信
-
Pay to use?オープン
難易度 1/5 1時間未満 初心者へのやさしさ 66/100
darkreader/darkreader#16108 · コメント 1 件 ·
メンテナーはふだん 2 日以内に返信