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:
Let’s walk through how we solved performance issues, structured custom hooks, and handled state transitions cleanly.
When building a zoomable/swipable chart to show a swimmer's progress over time, we needed to:
useChartNavigation
This hook manages internal chart state:
const {
activeIndex,
setActiveIndex,
goToPrev,
goToNext,
isPrevDisabled,
isNextDisabled,
updateVisibleIndices,
handleResetZoom,
handleZoom,
handleChartClick,
} = useChartNavigation(chartRef, data);
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.handleZoom()
and handleResetZoom()
with internal tooltip clearing to avoid jitter.ProgressChart
This chart has:
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.
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.
To make chart interaction even better:
keydown
eventsArrowLeft
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]);
This setup is a great real-world example of:
useMemo
and useCallback
effectivelyWe’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!