Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

`onFocusChange` fires again after each render, also when the focused point does not change

Open
#178 0 comments 0 reactions 0 assignees View on GitHub

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

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:

  1. The pointer focuses a point. onFocusChange(point) calls setFocused(point).
  2. React renders again. Something in the definition depends on the focus (for example a highlighted series), so the definition is a new object.
  3. The adapter calls update. The renderer restores the same point as a new object and calls onFocusChange(newObject).
  4. 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


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

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from TanStack/charts

All issues in TanStack/charts

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.