Recharts Patterns
Quick Guide: A Recharts chart is an assembly of independent child components —
XAxis,YAxis,Tooltip,Legend,CartesianGridand one or more data series — rather than one component taking a config object. Features are added and removed by adding and removing children. Charts render nothing without dimensions, so every chart needsResponsiveContainer, theresponsiveprop, or explicitwidth/height. Data and callback props are compared by reference, so an inline.map()recomputes the whole chart every render. In v3accessibilityLayerdefaults totrueand internal state is read through hooks rather than cloned props.
Detailed Resources:
- examples/core.md — line, bar, area and pie charts, responsive sizing, custom tooltips, multi-axis, stacking
- examples/advanced.md — ComposedChart, Brush, reference lines, synchronized charts, real-time updates, scatter shapes, radar
- reference.md — chart type table and prop cheat sheets for every component
Before writing Recharts code
Give every chart dimensions — ResponsiveContainer, the responsive prop, or explicit width and height. A chart with none renders an empty SVG and no error.
Give ResponsiveContainer a parent with a resolved height. It measures its parent, so a parent at height: 0 or display: none measures zero and the chart never appears.
Hold data arrays and callback props stable across renders with useMemo and useCallback. Recharts compares them by reference, so a fresh array or arrow function each render re-derives every point.
Return HTML from a custom Tooltip content component. The tooltip renders into an HTML overlay positioned above the SVG, so SVG elements inside it do not display.
Auto-detection: recharts, LineChart, BarChart, AreaChart, PieChart, ComposedChart, ScatterChart, RadarChart, RadialBarChart, FunnelChart, Treemap, ResponsiveContainer, XAxis, YAxis, CartesianGrid, dataKey, yAxisId, stackId, syncId, LabelList, ReferenceLine, ReferenceArea, Brush, PolarAngleAxis, isAnimationActive, throttleDelay, accessibilityLayer
Applies to:
- Line, bar, area, pie, scatter, radar, radial, funnel and treemap charts
- Composing several series, or several chart types, into one plot
- Multi-axis charts where series carry different units or scales
- Custom tooltips, legends, axis tick formatting and data labels
- Reference lines and areas, brush range selection, cross-chart tooltip sync
- Animation control and real-time update strategies
Handled elsewhere:
- Visualizations with no standard chart shape — building marks from scales and path generators is a different job from composing chart components
- Canvas or WebGL rendering for very large point counts — Recharts emits SVG, so every mark is a DOM node
- Physics-based or gesture-driven motion — a series animates between two data states on a duration and an easing, and there is no spring model and no drag-to-animate
- Geographic projections and 3D scenes
- Which colours the series use —
strokeandfilltake whatever values a product's visual language supplies - Accessibility conformance targets —
accessibilityLayersupplies keyboard navigation and ARIA on the chart, and which level a product must meet is settled elsewhere
Recharts is composition rather than configuration. Adding a <Tooltip /> adds tooltips; deleting it removes them. There is no options object, and no feature flag — the child list is the configuration.
Two consequences follow. JSX order is z-order, because SVG has no z-index: an Area written before a Bar renders behind it. And dataKey is the whole binding contract — the chart takes one data array and each child names the field it reads, so a series without a dataKey has nothing to draw.
Which chart type
Change over time?
Continuous trend -> LineChart or AreaChart
Discrete periods -> BarChart
Trend over totals -> ComposedChart (Bar + Line)
Part of a whole?
Few categories (< 8) -> PieChart; innerRadius > 0 for a donut
Correlation?
Two variables -> ScatterChart; encode a third in the shape's radius
Multi-dimensional?
3+ measures per item -> RadarChart
Stages of a process?
Successive drop-off -> FunnelChart
Hierarchy?
Nested proportions -> Treemap
Which responsive approach
ResponsiveContainer where debounce, aspect, onResize, minWidth or maxHeight is needed — it measures with a ResizeObserver. The responsive prop otherwise, which takes the size from the parent's CSS box and saves a wrapper element.
When to turn animation off
Data updating more than once a second, point counts in the thousands, print and export, and dashboards rendering many charts at once. Animation redraws the series on every data change.
Core patterns
Pattern 1: Chart composition
A container holding grid, axes, overlays and one child per series. Each child is an independent feature.
<ResponsiveContainer width="100%" height={CHART_HEIGHT}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="revenue" stroke="#8884d8" />
<Line type="monotone" dataKey="expenses" stroke="#82ca9d" />
</LineChart>
</ResponsiveContainer>
Full code: examples/core.md
Pattern 2: Responsive sizing
ResponsiveContainer measures its parent and passes concrete pixel dimensions down. The responsive prop uses the parent's CSS box directly.
<ResponsiveContainer width="100%" aspect={16 / 9} minWidth={MIN_WIDTH}>
<LineChart data={data}>{/* ... */}</LineChart>
</ResponsiveContainer>
// Or, with the parent sized in CSS:
<BarChart data={data} responsive>{/* ... */}</BarChart>
Server rendering has no ResizeObserver, so initialDimension supplies the first paint's size.
Full code: examples/core.md
Pattern 3: Custom tooltips
content takes an element or a function. The component receives active, payload and label, and payload is empty until a mark is hovered.
function CustomTooltip({
active,
payload,
label,
}: TooltipProps<ValueType, NameType>) {
if (!active || !payload?.length) return null;
return (
<div className="custom-tooltip">
<p>{label}</p>
{payload.map((entry) => (
<p key={entry.name} style={{ color: entry.color }}>
{entry.name}: {entry.value}
</p>
))}
</div>
);
}
<Tooltip content={<CustomTooltip />} />;
Passing an element rather than the component is how extra props reach it: content={<CustomTooltip currencySymbol="EUR " />}.
Full code: examples/core.md
Pattern 4: Axis configuration
domain accepts literals, "auto", "dataMin", "dataMax" and arithmetic on those. tickFormatter owns label text.
<XAxis dataKey="date" tickFormatter={DATE_FORMATTER} angle={-45} textAnchor="end" height={60} />
<YAxis tickFormatter={CURRENCY_FORMATTER} domain={[0, "dataMax + 1000"]} width={80} />
XAxis defaults to type="category" and YAxis to type="number" — a numeric x-axis needs type="number" set explicitly, or the values are treated as labels and spaced evenly.
Full code: examples/core.md
Pattern 5: Pie and donut charts
PieChart is the container and Pie the series. Cell children colour slices individually; innerRadius above zero makes it a donut.
<PieChart width={CHART_SIZE} height={CHART_SIZE}>
<Pie
data={data}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
outerRadius={OUTER_RADIUS}
innerRadius={INNER_RADIUS}
label
>
{data.map((_, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
</PieChart>
Full code: examples/core.md
Pattern 6: Multiple axes and multiple series
yAxisId pairs a series with an axis. Two axes without distinct ids collide on the default id 0.
<ComposedChart data={data}>
<XAxis dataKey="month" />
<YAxis yAxisId="left" />
<YAxis yAxisId="right" orientation="right" />
<Bar dataKey="sales" yAxisId="left" fill="#8884d8" />
<Line type="monotone" dataKey="trend" yAxisId="right" stroke="#ff7300" />
</ComposedChart>
stackId is the equivalent for stacking — bars sharing one stackId stack rather than group.
Full code: examples/advanced.md
Pattern 7: Animation control
Series animate on mount and on data change. The three props are per series.
<Line
dataKey="value"
isAnimationActive
animationDuration={ANIMATION_DURATION}
animationEasing="ease-in-out"
animationBegin={0}
/>
animationBegin staggers series that would otherwise all start together.
Full code: examples/advanced.md
Pattern 8: Keeping large or fast-changing charts responsive
Reference stability first, then node count, then event rate.
const chartData = useMemo(() => aggregate(rawData), [rawData]); // stable identity
const formatTick = useCallback((value: number) => `$${value}`, []);
<LineChart data={chartData} throttleDelay={THROTTLE_DELAY_MS}>
<Line dataKey="value" isAnimationActive={false} dot={false} />
</LineChart>;
dot={false} removes one DOM node per point, which is usually the largest single win — aggregating 50,000 rows to 500 before rendering is the next.
Full code: examples/advanced.md
Red flags
Breaks at runtime:
- A series with no
dataKey—Line,Bar,AreaandPieeach need one to know which field to read - A custom tooltip with no
active/payloadguard —payloadis empty before the first hover, sopayload[0].valuethrows data={rawData.map(...)}written inline — a new array identity every render, so the chart re-derives everything- An arrow function as
dataKeyortickFormatterwritten inline — same reference churn, per tick - Two
YAxiselements without distinctyAxisId— both claim id0and the series bind ambiguously CartesianGridleft on the default ids while the axes use customxAxisId/yAxisId— no grid lines drawn
Surprising behaviour:
- SVG has no
z-index— stacking follows JSX order, so a series written later draws on top ResponsiveContainermeasures with a ResizeObserver, which has not fired during server rendering;initialDimensioncovers the first paintdomainon an axis re-derives when data changes unlessallowDataOverflowis setsyncIdcouples every chart carrying the same value — hover and brush state are shared, including with charts elsewhere in the treeBrushadds a second miniature chart's worth of DOM, which is heavy on a dashboard of many charts- Pie
labelacceptstrue, an element or a function, and the default labels overlap on small slices — a label function returningnullbelow a percentage threshold is the usual fix - All slices render the same colour until
Cellchildren are added accessibilityLayeris on by default in v3, so keyboard navigation and ARIA attributes are present without being asked for- Animation redraws the series on every data change, so a chart updating each second is redrawing continuously