Skip to content
EliteChart

Next.js App Router

SSR-safe ChartForge integration — client boundaries, dynamic imports, and hydration-warning prevention.

ChartForge's primary target is the Next.js App Router. The library is fully compatible with React Server Components — but the rendering engine itself is browser-only (Canvas, ResizeObserver, devicePixelRatio), so the chart component must mount inside a Client Component. This page covers the four patterns you'll need.

1. The 'use client' boundary

The simplest pattern. Mark the file that calls <EliteChart /> (or any chart component) as a Client Component:

code
'use client';
import { EliteChart } from '@elitechart/elitechart';
import '@elitechart/elitechart/styles.css';

export default function Page() {
  return (
    <div className="h-screen w-screen">
      <EliteChart symbol="BTCUSD" timeframe="1h" />
    </div>
  );
}

This is the recommended path for most apps. The whole route segment becomes a Client Component, hydration runs as normal, and the chart appears on first paint.

@elitechart/react and @elitechart/elitechart already prepend 'use client'; to their bundles, so technically you can import them from a Server Component file — but the file you put your own chart code in still needs 'use client' if it uses hooks, refs, or event handlers around the chart.

2. Stylesheet in app/layout.tsx

Import the EliteChart stylesheet once in your root layout. The layout itself stays a Server Component:

code
import type { ReactNode } from 'react';
import '@elitechart/elitechart/styles.css';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>{children}</body>
    </html>
  );
}

suppressHydrationWarning on <html> is the recommended hedge against theme-flash mismatch warnings (see §4 below).

3. next/dynamic with ssr: false

If you want to defer the chart bundle until after first paint — useful for above-the-fold pages that don't immediately need the chart — wrap the import in next/dynamic:

code
import dynamic from 'next/dynamic';

const Chart = dynamic(
  () => import('@elitechart/elitechart').then((m) => m.EliteChart),
  { ssr: false, loading: () => <div className="h-screen w-screen animate-pulse bg-zinc-900" /> },
);

export default function Page() {
  return (
    <div className="h-screen w-screen">
      <Chart symbol="BTCUSD" timeframe="1h" />
    </div>
  );
}

ssr: false skips server rendering for the dynamic chunk entirely, so there's no hydration mismatch and no flash. The trade-off: the chart is only painted after the chunk arrives client-side.

Use sparingly — for the common case (chart-first page), the 'use client' pattern from §1 is faster and still SSR-friendly.

4. Hydration-warning prevention (theme flash)

If you persist the user's theme preference in localStorage and read it on first render, the server-rendered HTML and the client-rendered HTML will diverge → React fires a hydration warning. The standard fix is a tiny pre-hydration script in <head> that applies the theme attribute before React mounts:

code
const themeBootScript = `
(function () {
  try {
    var raw = localStorage.getItem('chartforge-theme');
    var mode = raw || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
    document.documentElement.setAttribute('data-theme', mode);
    document.documentElement.classList.toggle('dark', mode === 'dark');
  } catch (e) {}
})();
`;

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script dangerouslySetInnerHTML={{ __html: themeBootScript }} />
      </head>
      <body>{children}</body>
    </html>
  );
}

The script runs synchronously, before React hydrates, so the data-theme attribute is already set when ChartForge reads it. suppressHydrationWarning on <html> silences the expected attribute-only diff between server (no data-theme) and client (data-theme="dark").

SSR-safety rules of thumb

When writing your own components around ChartForge, follow these rules to avoid runtime errors during server render:

  • Never read window, document, navigator, or devicePixelRatio at module scope. Lazily resolve inside event handlers, useEffect, or guarded typeof window !== 'undefined' checks.
  • Don't measure the DOM during render. Use useEffect to run measurements after mount.
  • Don't render time-sensitive output without seeding. Render a neutral placeholder server-side; populate client-side.
  • Use Suspense boundaries around components that fetch market data, so streaming SSR can flush the rest of the page.

The internal skill at .claude/skills/nextjs-ssr-safety.md in the repo has more detail and reference examples.

What's next?