Optimizing React Charts with Custom Hooks, useMemo, and Chart.js
Adam C. |

Working with interactive data visualizations in React is incredibly powerful, especially when using Chart.js via react-chartjs-2. But it can also be tricky—especially when performance and usability are key. In this post, we’ll break down how to build a performant and maintainable chart component with features like:

  • Zooming and panning
  • Data point navigation
  • Tooltips and highlight syncing

Let’s walk through how we solved performance issues, structured custom hooks, and handled state transitions cleanly.

Photo by Markus Winkler on Unsplash

✅ Core Challenges

When building a zoomable/swipable chart to show a swimmer's progress over time, we needed to:

  • Prevent the chart from resetting zoom on every re-render
  • Highlight selected data points on click
  • Add navigation buttons to move to the previous/next visible point
  • Keep performance smooth across updates

🧠 Hook: useChartNavigation

This hook manages internal chart state:

const {
  activeIndex,
  setActiveIndex,
  goToPrev,
  goToNext,
  isPrevDisabled,
  isNextDisabled,
  updateVisibleIndices,
  handleResetZoom,
  handleZoom,
  handleChartClick,
} = useChartNavigation(chartRef, data);

Key Implementation Details:

  • useCallback for all handlers: Avoids function re-creation and keeps stable references for useEffect deps.
  • updateVisibleIndices: Calculates visible data points inside the chart area.
  • activeIndex & tooltip sync: When you select a point, the tooltip and highlight sync to it.
  • Zooming: Wrapped in handleZoom() and handleResetZoom() with internal tooltip clearing to avoid jitter.

🎨 Component: ProgressChart

This chart has:

  • Line and scatter datasets
  • Custom tooltips
  • Navigation buttons
  • Responsive design for mobile

Performance Enhancements:

  • chartData and options in useMemo: Prevents recalculation unless dependencies change.
  • <ChartDisplay> is wrapped in React.memo(): Stops full chart reset when only state like activeIndex changes.
const ChartDisplay = React.memo(function ChartDisplay({ chartRef, chartData, options }) {
  return <Chart ref={chartRef} data={chartData} options={options} />;
});

Without React.memo, selecting a point (which updates state) would trigger a re-render and reset zoom.

Chart Click Handling

const handleChartClick = useCallback((event) => {
  const chart = chartRef.current;
  const elements = chart.getElementsAtEventForMode(event, "nearest", { intersect: false }, false);
  const lineElement = elements.find((el) => el.datasetIndex === 0);
  if (lineElement) setActiveIndex(lineElement.index);
}, [chartRef]);

We check datasetIndex === 0 to only target the line series and ignore scatter points.

🔜 Next Steps: Keyboard Navigation

To make chart interaction even better:

  • Listen for keydown events
  • Use ArrowLeft and ArrowRight to trigger goToPrev() and goToNext()

This can be added via:

useEffect(() => {
  const handleKey = (e) => {
    if (e.key === 'ArrowLeft') goToPrev();
    if (e.key === 'ArrowRight') goToNext();
  };
  window.addEventListener('keydown', handleKey);
  return () => window.removeEventListener('keydown', handleKey);
}, [goToPrev, goToNext]);

✅ Final Thoughts

This setup is a great real-world example of:

  • Structuring reusable logic with hooks
  • Using useMemo and useCallback effectively
  • Avoiding unnecessary chart re-renders
  • Supporting user interaction like zoom + navigation

We’ve now got a powerful, fast, and user-friendly chart experience.

Let us know if you want to extend this to handle keyboard input or multiple chart types!