The four options
| Library | Renders with | Expo Go | Strength | Trade-off |
|---|---|---|---|---|
| react-native-gifted-charts | react-native-svg | ✅ Yes | Fastest to a good-looking chart | Heavy on very large datasets |
| Victory Native | Skia + Reanimated | ❌ Dev build | Gestures, tooltips, big data | More setup, more concepts |
| react-native-chart-kit | react-native-svg | ✅ Yes | Very simple API | Slow-moving, limited styling |
| Raw Skia / SVG | You | ⚠️ Depends | Anything you can draw | You own axes, scales, labels |
The honest decision rule is short. If your chart is a summary someone glances at — this week’s workouts, this month’s spending, weight over 90 days — take gifted-charts and move on with your day. If the chart is the product, with scrubbing, tooltips, and thousands of points, take Victory Native and accept the development build.
And the thing not on the list: rendering a web charting library inside a WebView. It works, it demos fine, and it costs you scroll performance, theme consistency, and the ability to style anything the way the rest of your app looks. Avoid it unless you are embedding a chart you genuinely cannot rebuild.
The common case: gifted-charts
npx expo install react-native-gifted-charts react-native-svgInstall react-native-svg through expo install rather than npm so the native version matches your SDK — a mismatch there is one of the more confusing red-screen errors in React Native.
A weekly bar chart, complete:
import { useMemo } from 'react';
import { Dimensions, StyleSheet, Text, View } from 'react-native';
import { BarChart } from 'react-native-gifted-charts';
const WIDTH = Dimensions.get('window').width;
export default function WeeklyActivity({ sessions }) {
// aggregate raw rows into 7 buckets — never hand the chart raw data
const data = useMemo(() => {
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const counts = new Array(7).fill(0);
for (const s of sessions) counts[dayIndex(s.completedAt)] += 1;
return counts.map((value, i) => ({
value,
label: days[i],
frontColor: value > 0 ? '#fb923c' : '#2a2a2a',
}));
}, [sessions]);
return (
<View style={styles.card}>
<Text style={styles.title}>This week</Text>
<BarChart
data={data}
width={WIDTH - 88}
height={180}
barWidth={22}
barBorderRadius={6}
spacing={16}
noOfSections={3}
yAxisThickness={0}
xAxisThickness={0}
yAxisTextStyle={{ color: '#8a8a8a', fontSize: 11 }}
xAxisLabelTextStyle={{ color: '#8a8a8a', fontSize: 11 }}
rulesColor="rgba(255,255,255,0.06)"
isAnimated
/>
</View>
);
}
const styles = StyleSheet.create({
card: { backgroundColor: '#141414', borderRadius: 16, padding: 20, margin: 16 },
title: { color: '#fff', fontWeight: '700', fontSize: 16, marginBottom: 16 },
});Most of those props exist to remove things: axis lines off, faint grid rules, small muted labels. A mobile chart competes with a 6-inch screen, and the default look of every charting library is a desktop chart with too much furniture. Strip it back and the data reads at a glance.
When the chart is the product: Victory Native
Victory Native renders through @shopify/react-native-skia with Reanimated for gestures, which is what makes smooth scrubbing over a long series possible. It needs a development build:
npx expo install victory-native @shopify/react-native-skia \
react-native-reanimated react-native-gesture-handler
npx expo run:ios # or run:android — Expo Go can't load SkiaThe API is compositional — you provide the chart bounds and data keys, then draw the marks yourself:
import { CartesianChart, Line } from 'victory-native';
export default function WeightTrend({ points }) {
return (
<CartesianChart
data={points} // [{ day: 1, kg: 82.4 }, …]
xKey="day"
yKeys={['kg']}
axisOptions={{ lineColor: 'rgba(255,255,255,0.06)', labelColor: '#8a8a8a' }}
>
{({ points: p }) => (
<Line points={p.kg} color="#fb923c" strokeWidth={3} curveType="natural" />
)}
</CartesianChart>
);
}That extra ceremony is the trade: you write more, and in exchange the chart stays at 60fps while a finger drags across it. If nobody is going to drag a finger across it, you did not need this.
Three mistakes that make charts feel slow
- Building the data array inline.
data={sessions.map(…)}creates a new array on every parent render, so the chart rebuilds every SVG node every time — including when an unrelated piece of state changes.useMemoon the transform, andReact.memoon the chart component, fixes the majority of “my charts screen is janky” reports. - Rendering every point you have. A year of daily weigh-ins is 365 points on a screen about 350 points wide. Aggregate to weekly before rendering — it is faster and, more importantly, the trend becomes visible instead of a noisy hairline.
- Charts inside a scrolling list. Putting a chart in a
FlatListrow means it re-renders on scroll. Hoist it intoListHeaderComponent, or keep it on its own screen.
All three are versions of the same rule — render less, less often. The React Native performance fixes guide has the general form of it.
One chart usually beats four
The most common failure of a progress screen is not the library — it is four charts nobody reads. A tracking app generally needs one number the user cares about, one trend line for it, and one comparison against last period. Everything else is a settings screen for your analytics.
If you want to see how a chart reads on a phone before committing to a library, describe the screen — “a weekly bar chart of workouts completed and a line of body weight over 90 days” — and ShipNative builds it as real React Native, aggregation included, running on your device. Export the project and swap libraries later if you outgrow the first choice.