Every data-driven mobile app eventually needs charts. Whether you're building a fitness tracker, a financial dashboard, or an analytics tool, displaying data visually just makes your app more engaging and way easier to understand. In the React Native ecosystem, several charting libraries compete for your attention — but honestly, one stands clearly above the rest in 2026: Victory Native.
Victory Native (formerly known as Victory Native XL) is a from-scratch rewrite purpose-built for React Native. It leverages React Native Skia for GPU-accelerated rendering, Reanimated for buttery-smooth animations on the UI thread, and Gesture Handler for native-feeling touch interactions. The result? Charts that animate at over 100 FPS even on lower-end devices.
In this guide, you'll set up Victory Native in an Expo project from scratch, build line charts, bar charts, area charts, and pie charts, add interactive tooltips driven by touch gestures, and pick up some performance best practices for production apps. So, let's dive in.
Why Victory Native Over Other Libraries
Before diving into code, it helps to understand why Victory Native is the go-to choice for performance-critical charts in 2026:
- Skia rendering: Victory Native draws directly on the GPU via React Native Skia, bypassing the JS bridge entirely for rendering operations. This eliminates the jank that SVG-based libraries like
react-native-chart-kitorreact-native-svg-chartsstruggle with on large datasets. - Reanimated animations: All animations run on the UI thread via shared values. No React re-renders, no bridge traffic — just smooth 60-120 FPS transitions.
- Gesture-first interactivity: Built-in hooks like
useChartPressStatetrack touch gestures and expose the nearest data point as a shared value, making tooltips and crosshairs trivial to implement. - D3-powered math: Axis scaling, domain calculations, and data transformations are handled internally by D3, so you focus on presentation instead of math.
- Composable API: Instead of a monolithic chart config object, you compose charts from small building blocks —
Line,Bar,Area, axis components — giving you full control over every visual detail.
| Feature | Victory Native | react-native-gifted-charts | react-native-chart-kit |
|---|---|---|---|
| Rendering engine | Skia (GPU) | SVG | SVG |
| Animation system | Reanimated (UI thread) | JS-based | None built-in |
| Touch interactivity | Gesture Handler + shared values | onPress callbacks | Limited |
| Performance (large data) | Excellent (100+ FPS) | Good | Moderate |
| Customization | Full Skia canvas access | Props-based | Props-based |
| Expo support | Yes (dev client) | Yes (Expo Go) | Yes (Expo Go) |
If your charts are simple and you want Expo Go compatibility, react-native-gifted-charts is a perfectly fine choice. But if you need performance at scale, fluid animations, or interactive gestures, Victory Native is the clear winner.
Project Setup and Installation
Victory Native depends on native modules (Skia, Reanimated, Gesture Handler), so you'll need an Expo development build rather than Expo Go. Here's how to get everything set up:
Create the Expo Project
npx create-expo-app@latest ChartDemo
cd ChartDemo
Install Victory Native and Peer Dependencies
npx expo install victory-native @shopify/react-native-skia react-native-reanimated
react-native-gesture-handler ships with new Expo projects by default, so you typically don't need to install it separately. Just verify it's in your package.json — if not, add it:
npx expo install react-native-gesture-handler
Configure Babel for Reanimated
Open your babel.config.js and add the Reanimated plugin as the last item in the plugins array. This part is important — get the order wrong and you'll be scratching your head over animation bugs later:
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: ["react-native-reanimated/plugin"],
};
};
Create a Development Build
Since Skia requires native code, you can't just use Expo Go. Create a development build instead:
npx expo prebuild
npx expo run:ios
# or
npx expo run:android
Alternatively, use EAS Build for cloud builds:
eas build --profile development --platform all
Building a Line Chart
The CartesianChart component is the foundation for all axis-based charts in Victory Native. It takes your raw data, transforms it into chart coordinates, and exposes those coordinates to child render functions where you draw the actual visual elements.
Let's start with a simple line chart showing weekly revenue:
import { View, StyleSheet } from "react-native";
import { CartesianChart, Line } from "victory-native";
import { useFont } from "@shopify/react-native-skia";
const DATA = [
{ day: 1, revenue: 4200 },
{ day: 2, revenue: 5800 },
{ day: 3, revenue: 5100 },
{ day: 4, revenue: 7300 },
{ day: 5, revenue: 6900 },
{ day: 6, revenue: 8400 },
{ day: 7, revenue: 9100 },
];
export default function RevenueLineChart() {
const font = useFont(require("./assets/fonts/Inter-Medium.ttf"), 12);
return (
`Day ${value}`,
formatYLabel: (value) => `$${(value / 1000).toFixed(1)}k`,
}}
>
{({ points }) => (
)}
);
}
const styles = StyleSheet.create({
container: { height: 300, padding: 16 },
});
Here's a quick breakdown of the key props:
data— An array of objects. Each object represents one data point.xKey— The property name used for the x-axis.yKeys— An array of property names for the y-axis. You can plot multiple lines by adding more keys.axisOptions— Configures axis labels. Thefontprop requires a Skia font loaded viauseFont.curveType— Controls interpolation. Use"natural"for smooth curves or"linear"for straight segments.animate— Animates path transitions when data changes using Reanimated on the UI thread.
Plotting Multiple Lines
Need to compare two series on the same chart? Just add a second key to yKeys and render multiple Line components:
const DATA = [
{ day: 1, revenue: 4200, expenses: 3100 },
{ day: 2, revenue: 5800, expenses: 3400 },
{ day: 3, revenue: 5100, expenses: 2900 },
{ day: 4, revenue: 7300, expenses: 4100 },
{ day: 5, revenue: 6900, expenses: 3800 },
{ day: 6, revenue: 8400, expenses: 4600 },
{ day: 7, revenue: 9100, expenses: 5200 },
];
{({ points }) => (
<>
>
)}
Building a Bar Chart with Gradient Fill
The Bar component works similarly to Line but also needs chartBounds to know where the bars should sit. You can make bars look really polished by adding rounded corners and Skia gradient fills:
import { CartesianChart, Bar } from "victory-native";
import { LinearGradient, vec } from "@shopify/react-native-skia";
const SALES_DATA = [
{ month: 1, sales: 12400 },
{ month: 2, sales: 15800 },
{ month: 3, sales: 13200 },
{ month: 4, sales: 18900 },
{ month: 5, sales: 21300 },
{ month: 6, sales: 19700 },
];
export default function SalesBarChart() {
const font = useFont(require("./assets/fonts/Inter-Medium.ttf"), 12);
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];
return (
months[value - 1] || "",
formatYLabel: (value) => `$${(value / 1000).toFixed(0)}k`,
}}
>
{({ points, chartBounds }) => (
)}
);
}
The domainPadding prop adds horizontal space so bars don't get clipped at the chart edges. And if you're wondering about bar spacing — the innerPaddingFraction prop (default 0.2) controls the gap between bars. Set it to 0 for a histogram effect or bump it up for wider spacing.
Building an Area Chart
The Area component creates a filled region beneath a data line. I like to combine it with a Line to get both the stroke and fill — it just looks more complete that way:
import { CartesianChart, Line, Area } from "victory-native";
import { LinearGradient, vec } from "@shopify/react-native-skia";
const TRAFFIC_DATA = [
{ hour: 0, visitors: 120 },
{ hour: 4, visitors: 45 },
{ hour: 8, visitors: 380 },
{ hour: 12, visitors: 720 },
{ hour: 16, visitors: 890 },
{ hour: 20, visitors: 540 },
{ hour: 24, visitors: 210 },
];
export default function TrafficAreaChart() {
const font = useFont(require("./assets/fonts/Inter-Medium.ttf"), 12);
return (
`${value}:00`,
}}
>
{({ points, chartBounds }) => (
<>
>
)}
);
}
The y0 prop tells the Area where its bottom edge should be. Using chartBounds.bottom fills all the way down to the x-axis. The gradient uses hex alpha (80 for 50% opacity, 05 for near-transparent) to create a soft fade effect that looks surprisingly nice.
Building a Pie and Donut Chart
Pie charts in Victory Native use the Pie component, which is separate from CartesianChart since pie charts are polar, not Cartesian:
import { View, StyleSheet } from "react-native";
import { Pie } from "victory-native";
const CATEGORY_DATA = [
{ label: "Electronics", value: 45, color: "#6366f1" },
{ label: "Clothing", value: 25, color: "#f43f5e" },
{ label: "Food", value: 20, color: "#10b981" },
{ label: "Other", value: 10, color: "#f59e0b" },
];
export default function CategoryPieChart() {
return (
{CATEGORY_DATA.map((item) => (
))}
);
}
const styles = StyleSheet.create({
container: {
height: 300,
alignItems: "center",
justifyContent: "center",
},
});
To create a donut chart, just add the innerRadius prop to Pie.Chart. Something like innerRadius="60%" hollows out the center, leaving room for a summary label. It's a small change that looks great.
Adding Interactive Tooltips
This is where Victory Native truly shines — and honestly, it's the feature that sold me on the library. The useChartPressState hook tracks the touch position and resolves the closest data point, all as Reanimated shared values that never trigger React re-renders:
import { View, StyleSheet } from "react-native";
import { CartesianChart, Line, useChartPressState } from "victory-native";
import { Circle, useFont, Text as SkiaText } from "@shopify/react-native-skia";
import { useDerivedValue } from "react-native-reanimated";
const DATA = [
{ day: 1, revenue: 4200 },
{ day: 2, revenue: 5800 },
{ day: 3, revenue: 5100 },
{ day: 4, revenue: 7300 },
{ day: 5, revenue: 6900 },
{ day: 6, revenue: 8400 },
{ day: 7, revenue: 9100 },
];
function Tooltip({ state, font }) {
const cx = useDerivedValue(() => state.x.position.value);
const cy = useDerivedValue(() => state.y.revenue.position.value);
const label = useDerivedValue(
() => `$${state.y.revenue.value.value.toFixed(0)}`
);
return (
<>
cy.value - 16)}
text={label}
font={font}
color="#1e1b4b"
/>
>
);
}
export default function InteractiveLineChart() {
const font = useFont(require("./assets/fonts/Inter-Medium.ttf"), 14);
const { state, isActive } = useChartPressState({
x: 0,
y: { revenue: 0 },
});
return (
{({ points }) => (
<>
{isActive && }
>
)}
);
}
const styles = StyleSheet.create({
container: { height: 300, padding: 16 },
});
Here's how it all fits together:
useChartPressStatecreates a shared value object that mirrors the shape of your data keys. You pass it toCartesianChartvia thechartPressStateprop.- When the user presses and drags on the chart, the hook updates
state.x.position,state.y.revenue.position, andstate.y.revenue.valueon the UI thread. - The
Tooltipcomponent reads those shared values withuseDerivedValueto position a Skia circle and text label — zero React re-renders involved. isActiveis a boolean shared value that'strueonly while the user is touching the chart, so the tooltip appears and disappears naturally.
This architecture — gesture updates on the UI thread, Skia rendering without React — is what makes Victory Native tooltips feel indistinguishable from a fully native app.
Animating Data Changes
Victory Native animates path transitions automatically when you provide the animate prop. This is super useful when switching between data sets, like toggling between weekly and monthly views:
import { useState } from "react";
import { View, Pressable, Text, StyleSheet } from "react-native";
import { CartesianChart, Bar } from "victory-native";
const WEEKLY = [
{ period: 1, value: 320 },
{ period: 2, value: 480 },
{ period: 3, value: 410 },
{ period: 4, value: 590 },
];
const MONTHLY = [
{ period: 1, value: 1200 },
{ period: 2, value: 1850 },
{ period: 3, value: 1600 },
{ period: 4, value: 2100 },
];
export default function AnimatedBarChart() {
const [data, setData] = useState(WEEKLY);
const font = useFont(require("./assets/fonts/Inter-Medium.ttf"), 12);
return (
setData(WEEKLY)} style={styles.btn}>
Weekly
setData(MONTHLY)} style={styles.btn}>
Monthly
{({ points, chartBounds }) => (
)}
);
}
const styles = StyleSheet.create({
wrapper: { padding: 16 },
buttons: { flexDirection: "row", gap: 12, marginBottom: 12 },
btn: {
paddingHorizontal: 16,
paddingVertical: 8,
backgroundColor: "#e0e7ff",
borderRadius: 8,
},
chart: { height: 300 },
});
The animate prop accepts two types:
timing— a linear or eased animation with a fixed duration. Good for predictable, consistent transitions.spring— a physics-based spring animation. Tweakstiffnessanddampingto get the feel you want. Higher stiffness produces snappier motion; lower damping adds bounce.
When data changes, Victory Native morphs the previous path into the new one. And if the new dataset has more or fewer points than before, the library handles interpolation automatically — you don't have to worry about it.
Building a Dashboard with Multiple Charts
In a real app, you'll probably want to combine multiple chart types into a scrollable dashboard. Here's a pattern I've found works well and keeps things performant:
import { ScrollView, View, Text, StyleSheet } from "react-native";
import { CartesianChart, Line, Bar, Area } from "victory-native";
import { useFont } from "@shopify/react-native-skia";
import { LinearGradient, vec } from "@shopify/react-native-skia";
export default function Dashboard({ revenueData, salesData, trafficData }) {
const font = useFont(require("./assets/fonts/Inter-Medium.ttf"), 11);
return (
Revenue Trend
{({ points }) => (
)}
Monthly Sales
{({ points, chartBounds }) => (
)}
Site Traffic
{({ points, chartBounds }) => (
<>
>
)}
);
}
const styles = StyleSheet.create({
scroll: { flex: 1, backgroundColor: "#f8fafc" },
heading: {
fontSize: 16,
fontWeight: "600",
color: "#1e293b",
marginTop: 20,
marginBottom: 8,
paddingHorizontal: 16,
},
chartCard: {
height: 260,
marginHorizontal: 16,
backgroundColor: "white",
borderRadius: 12,
padding: 12,
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
},
});
Performance Best Practices
Victory Native is fast out of the box, but there are a few things you can do to squeeze even more performance out of it.
Memoize Your Data
Every time data changes reference identity, CartesianChart recalculates its internal D3 scales. Wrap your data in useMemo to prevent unnecessary recalculations:
const chartData = useMemo(
() => rawData.map((d) => ({ x: d.timestamp, y: d.value })),
[rawData]
);
Limit Data Points
While Victory Native handles large datasets well, there's honestly no visual benefit to plotting 10,000 points on a 400px-wide chart. Downsample to a reasonable number — 100 to 200 points is usually more than enough for a smooth curve. Libraries like d3-array provide utilities like bin() for aggregating data.
Lazy Load Charts Below the Fold
If your dashboard has charts that aren't immediately visible, defer their rendering. Use a simple intersection observer pattern or render them only after the parent ScrollView scrolls near them. This prevents multiple Skia canvases from being initialized all at once on mount.
Use a Single Font Instance
Call useFont once at the top of your screen component and pass the font down to all charts. Loading the same font file multiple times wastes memory — it's an easy optimization to miss.
Avoid Inline Functions in Render
The children of CartesianChart is a render function. Avoid creating new objects or arrays inside it, since this function can get called frequently during animations and gestures.
Troubleshooting Common Issues
Here are the problems that trip up most developers when getting started (I've hit a few of these myself):
- Chart renders blank: The parent
Viewmust have an explicitheight.CartesianChartmeasures its container and simply won't render if the height is zero. This one catches almost everyone. - Axis labels are invisible: You need to pass a Skia font via
axisOptions.font. IfuseFontreturnsnull(font still loading), the chart renders without labels. Guard against this with a loading state. - Peer dependency conflicts: Skia, Reanimated, and Gesture Handler versions must match your Expo SDK. Always use
npx expo installto get compatible versions — don't npm install them manually. - Charts not rendering in Expo Go: Victory Native requires native modules that Expo Go doesn't include. You'll need a development build or EAS Build instead.
- Animations feel jerky: Make sure the Reanimated Babel plugin is the last plugin in your
babel.config.js. An incorrect plugin order can cause Reanimated to miss worklet transformations, and the resulting bugs are not obvious at all.
Frequently Asked Questions
What is the best chart library for React Native in 2026?
For performance-critical apps, Victory Native is the best choice. It uses Skia for GPU-accelerated rendering and Reanimated for UI-thread animations, delivering 100+ FPS even on lower-end devices. For simpler use cases where Expo Go compatibility matters, react-native-gifted-charts is a solid alternative that works without native modules.
Can I use Victory Native with Expo?
Yes, but you'll need an Expo development build or EAS Build. Victory Native depends on @shopify/react-native-skia, react-native-reanimated, and react-native-gesture-handler, which require native code that Expo Go doesn't include. Run npx expo prebuild or use eas build --profile development to create a compatible build.
How do I add tooltips to Victory Native charts?
Use the useChartPressState hook to track touch gestures. Pass the returned state to CartesianChart via the chartPressState prop. The hook exposes the nearest data point's position and value as Reanimated shared values, which you can use to draw Skia elements like circles and text labels that follow the user's finger — all on the UI thread with no React re-renders.
Does Victory Native support web rendering?
Victory Native is built for iOS and Android. While React Native Skia has experimental web support via WebAssembly, Victory Native doesn't officially support web targets. If you need charts on both mobile and web, consider using the Victory web library (victory) for your web app alongside Victory Native for mobile, and share the data layer between them.
How many data points can Victory Native handle?
Victory Native handles hundreds of data points smoothly at 60+ FPS. For datasets with thousands of points, downsample the data before passing it to the chart. There's really no visual benefit to plotting 10,000 points on a mobile screen — aggregating data into 100-200 representative points gives you identical visual results with much better performance.