`onFocusChange` fires again after each render, also when the focused point does not change
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 68/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- react, typescript
- Domain
- data-visualization, frontend
Research direction
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.
Written by the indexing model from the issue text.
Description
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).
- Dominant language
- TypeScript
- Stars
- 819
- Forks
- 49
- Avg merge
- 9h 37m
- Merged PRs (30d)
- 8
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from TanStack/charts
-
Renderer treats a sub-pixel width difference as a resize, which cancels the `motion()` entranceOpen
Difficulty 1/5 Under an hour Newbie friendliness 85/100
TanStack/charts#182 · 1 comment ·
Maintainers usually reply within 1 day
-
brushX with `values` can shorten the selection range when it’s dragged to the plot edgePossibly taken @andersk claimed this 4 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
Maintainers usually reply within 1 day
-
Grid and axis lines have no `shape-rendering` option, so hairlines anti-alias across two device rowsOpen
Difficulty 3/5 1-2 days Newbie friendliness 76/100
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Agenta-AI/agenta#7542 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
code quality security
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
enhancement good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
-
Difficulty 1/5 Under an hour Newbie friendliness 82/100
homarr-labs/dashboard-icons#2939 ·
Maintainers usually reply within 1 day