ApexCharts with Next.js
ApexCharts requires a live browser DOM to mount its SVG canvas, measure container dimensions, and attach event listeners. Next.js App Router runs most of your code on the server by default, which means you need to be deliberate about where ApexCharts executes. This page covers the three main integration patterns and explains when to reach for each one.
Installation
npm install react-apexcharts apexcharts
react-apexcharts is the official React wrapper. It already has "use client" at the top of its own source, so the library itself will not run during server rendering. apexcharts is the core library and is listed as a peer dependency.
Pattern A: Client Component (most common)
The standard approach is to wrap the chart in a Client Component. Any component file that contains 'use client' at the top will be rendered in the browser, which is where ApexCharts needs to run.
// components/SalesChart.tsx
'use client'
import ReactApexChart from 'react-apexcharts'
import type { ApexOptions } from 'apexcharts'
const options: ApexOptions = {
chart: { type: 'bar' },
xaxis: { categories: ['Jan', 'Feb', 'Mar'] }
}
const series = [{ name: 'Sales', data: [44, 55, 41] }]
export default function SalesChart() {
return (
<ReactApexChart
type="bar"
series={series}
options={options}
height={350}
/>
)
}
You can then import this component from a Server Component page without adding 'use client' to the page itself. Next.js automatically treats SalesChart as a client boundary.
// app/dashboard/page.tsx (Server Component — no 'use client' needed)
import SalesChart from '@/components/SalesChart'
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
<SalesChart />
</main>
)
}
The 'use client' directive on SalesChart.tsx marks a boundary. Everything inside that file and its imports runs in the browser. Everything in page.tsx above it can still run on the server, including async/await data fetches, database calls, and so on.
Pattern B: Dynamic import with ssr: false
When you want to prevent the chart bundle from being included in the server-rendered HTML at all, use Next.js's dynamic() helper with ssr: false. This is useful for:
- Reducing server payload for charts that are below the fold
- Lazy-loading chart code only when the user navigates to a page
- Integrating a third-party chart component you cannot modify to add
'use client'
// app/dashboard/page.tsx
import dynamic from 'next/dynamic'
const SalesChart = dynamic(() => import('@/components/SalesChart'), {
ssr: false,
loading: () => <div style={{ height: 350 }}>Loading chart...</div>
})
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
<SalesChart />
</main>
)
}
The loading prop renders a placeholder while the chart JavaScript downloads and hydrates. Setting the placeholder height to match the chart height avoids layout shift.
Note that ssr: false only prevents the chart from rendering during the server pass. The underlying SalesChart component still needs 'use client' if it uses React state, because it will run in the browser once the bundle loads.
Pattern C: Server-rendered SVG
For static charts where interactivity is not needed (PDF reports, email previews, read-only dashboards), ApexCharts can produce an SVG string on the server. No browser is involved.
// app/report/page.tsx (Server Component)
import ApexCharts from 'apexcharts/ssr'
export default async function ReportPage() {
const html = await ApexCharts.renderToHTML(
{
chart: { type: 'bar' },
series: [{ name: 'Sales', data: [44, 55, 41] }],
xaxis: { categories: ['Jan', 'Feb', 'Mar'] }
},
{ width: 600, height: 350 }
)
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
The dangerouslySetInnerHTML call is safe in this context. The HTML comes from renderToHTML, which builds an SVG from your own chart configuration. No user input flows into it. The chart is fully rendered before the page reaches the browser, so it appears immediately without any JavaScript hydration.
For the full SSR API reference, see Server-Side Rendering.
Fetching data server-side, rendering client-side
The recommended pattern for dashboards is to fetch data in a Server Component and pass it as props to a Client Component chart. This keeps your API calls on the server (so API keys stay private and there is no client-side waterfall) while the chart itself stays interactive.
// app/dashboard/page.tsx (Server Component)
import SalesChart from '@/components/SalesChart'
async function getSalesData(): Promise<number[]> {
const res = await fetch('https://api.example.com/sales', {
next: { revalidate: 3600 }
})
return res.json()
}
export default async function DashboardPage() {
const data = await getSalesData()
return (
<main>
<h1>Dashboard</h1>
<SalesChart data={data} />
</main>
)
}
// components/SalesChart.tsx
'use client'
import ReactApexChart from 'react-apexcharts'
import type { ApexOptions } from 'apexcharts'
interface Props {
data: number[]
}
const options: ApexOptions = {
chart: { type: 'bar' },
xaxis: { categories: ['Jan', 'Feb', 'Mar'] }
}
export default function SalesChart({ data }: Props) {
return (
<ReactApexChart
type="bar"
series={[{ name: 'Sales', data }]}
options={options}
height={350}
/>
)
}
The rule is: props crossing a Server-to-Client boundary must be serializable (plain objects, arrays, strings, numbers). Functions and class instances cannot cross this boundary.
Updating chart data based on user interaction
When the chart needs to respond to user actions (date range pickers, filters, tab switches), manage the series state inside the Client Component. You can still seed the initial data from the server via props.
// components/FilterableChart.tsx
'use client'
import { useState } from 'react'
import ReactApexChart from 'react-apexcharts'
import type { ApexOptions } from 'apexcharts'
interface Props {
initialData: number[]
}
const options: ApexOptions = {
chart: { type: 'bar' },
xaxis: { categories: ['Jan', 'Feb', 'Mar'] }
}
export default function FilterableChart({ initialData }: Props) {
const [series, setSeries] = useState([{ name: 'Sales', data: initialData }])
async function fetchFiltered(period: string) {
const data: number[] = await fetch(`/api/sales?period=${period}`).then(r => r.json())
setSeries([{ name: 'Sales', data }])
}
return (
<>
<div style={{ marginBottom: 12 }}>
<button onClick={() => fetchFiltered('week')}>Last week</button>
<button onClick={() => fetchFiltered('month')}>Last month</button>
</div>
<ReactApexChart
type="bar"
series={series}
options={options}
height={350}
/>
</>
)
}
When setSeries is called, React re-renders the component and react-apexcharts diffs the new series against the previous one, animating the transition automatically.
Common mistakes
Importing apexcharts directly in a Server Component. The core library accesses window and document at import time. If you write import ApexCharts from 'apexcharts' in a file without 'use client', Next.js will attempt to run that module on the server and throw ReferenceError: window is not defined. Use react-apexcharts for client rendering, or apexcharts/ssr for server-side SVG output.
Forgetting 'use client' when the chart component uses state. If your chart component calls useState, useEffect, or any other React hook, it must be a Client Component. Omitting 'use client' causes Next.js to throw a build-time error explaining that hooks cannot run in Server Components.
Accessing window at module scope inside a dynamically imported component. Using dynamic(() => import(...), { ssr: false }) prevents the component from rendering on the server, but the module-level code in that file is still evaluated on the server before the component function runs. Any window reference at the top level of the module (outside of useEffect or event handlers) will still throw. Move those references inside a useEffect callback or a user event handler.
Related
- React Charts - full guide to the
react-apexchartswrapper, props, and TypeScript types - Server-Side Rendering - complete
apexcharts/ssrAPI reference