OGE logoOGE

Next.js App Router

The React packages are client components that render on the server: a Server Component can import and render them directly, Next.js server-renders them, and the browser hydrates the markup without a mismatch. This page is about @oge-ui/react-*; the Angular packages are covered in Angular SSR.

React 18 / 19 use client SSR + hydration

Install and import the CSS

npm install @oge-ui/react          # every MIT React family
# or one family at a time
npm install @oge-ui/react-grid @oge-ui/react-inputs

The packages ship class names, not CSS-in-JS: import the stylesheet once in the root layout. The JavaScript deliberately does not import its own CSS, so a server render or a bundler without a CSS loader never has to resolve it.

// app/layout.tsx — a Server Component: global CSS and the client providers
import type { ReactNode } from 'react';
import '@oge-ui/react/styles.css';          // or @oge-ui/react-<family>/styles.css
import '@oge-ui/core/themes/dark.css';      // optional theme, shared with Angular
import { Providers } from './providers';

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

Client components

Every JavaScript file of every React package starts with 'use client' — CI's use-client-check fails a build that loses it. What that means for your code:

Where OGE components can be used in the App Router
FromWorksNote
A Server Component Yes Render an OGE component directly, with serializable props only — arrays, plain objects, strings, numbers.
Function props ( onClick , render props, calculateCellValue ) In a client module Functions cannot cross the server → client boundary; put them in your own 'use client' component.
Refs and imperative methods In a client module Same reason — useRef and component methods live on the client.
A DataSource instance In a client module A class instance is not serializable; create it client-side (or pass the rows array from the server).
// app/orders/page.tsx — a Server Component rendering an OGE client component
import { OgeGrid } from '@oge-ui/react-grid';
import { getOrders } from '@/lib/orders';

const columns = [
  { field: 'id', caption: 'Order', width: 90, dataType: 'number' as const },
  { field: 'customer', caption: 'Customer' },
  { field: 'total', caption: 'Total', dataType: 'number' as const },
];

export default async function OrdersPage() {
  const orders = await getOrders();
  // only serializable props cross the boundary: arrays, plain objects, strings
  return <OgeGrid data={orders} keyField="id" columns={columns} locale="de-DE" />;
}
'use client';

import { useState } from 'react';
import { OgeButton } from '@oge-ui/react-buttons';
import { OgeGrid } from '@oge-ui/react-grid';

interface Order {
  readonly id: number;
  readonly customer: string;
}

const columns = [
  { field: 'id', caption: 'Order', width: 90, dataType: 'number' as const },
  { field: 'customer', caption: 'Customer' },
];

export function OrdersBoard() {
  // event handlers are functions — they need a client module of your own
  const [orders, setOrders] = useState<readonly Order[]>([
    { id: 1, customer: 'Ada' },
  ]);
  const add = () =>
    setOrders((rows) => [...rows, { id: rows.length + 1, customer: 'Grace' }]);
  return (
    <>
      <OgeButton text="Add order" onClick={add} />
      <OgeGrid data={orders} keyField="id" columns={columns} locale="en-US" />
    </>
  );
}

Providers

OgeLocaleProvider and the per-family Oge…ConfigProviders are React context, so they sit in a client module that the layout renders:

'use client';

import type { ReactNode } from 'react';
import { OgeLocaleProvider } from '@oge-ui/react';
import { de } from '@oge-ui/locales/de';

// app/providers.tsx — config and locale providers hold React context, so they
// live in a client module; the layout above stays a Server Component
export function Providers({ children }: { children: ReactNode }) {
  return <OgeLocaleProvider pack={de}>{children}</OgeLocaleProvider>;
}

Server rendering and hydration

  • apps/ssr-smoke/src/react-hydration.spec.tsx renders every React family with renderToString in plain Node — any browser global touched while rendering throws, exactly as on a Next.js server — then hydrates it under <StrictMode> "seven minutes later" and fails on any warning.
  • Ids come from useId(), so server and client agree.
  • In-memory data renders on the server: an array or an ArrayDataSource answers loadSync during the first render, so the first page of rows is in the HTML. Remote sources (CustomDataSource, CursorDataSource, ODataDataSource) start loading after mount.
  • "Today" markers in the scheduler and the Gantt appear only after hydration, so a page rendered minutes earlier still hydrates cleanly.

The repository has no Next.js application under test; these guarantees come from the plain-Node render and hydrate suite above, which is what a Next.js server does with the components.

Locale on the server

Without a locale, a React family formats with navigator.language in the browser and the Node runtime's default on the server — a hydration mismatch for every reader whose language differs from the server's. Pass it explicitly (from the request, a cookie or your i18n router):

React components with a locale prop
PackageComponents with locale
@oge-ui/react-gridOgeGrid
@oge-ui/react-tree-listOgeTreeList
@oge-ui/react-inputsOgeCalendar , OgeDateBox , OgeDateRangeBox , OgeNumberBox , OgeOtpInput , OgeRating
@oge-ui/react-layoutOgeAvatar , OgeBadge , OgeCarousel , OgeDataView , OgeListView , OgeTileLayout , OgeTimeline
@oge-ui/react-scheduler , -gantt , -kanban , -pivotOgeScheduler , OgeGantt , OgeKanban , OgePivotGrid
@oge-ui/react ( OgeLocaleProvider ) Passes the pack’s locale to the families it configures that format numbers or dates (grid, inputs, editor, layout)

Loading heavy families later

A family the first paint does not need can load after hydration with next/dynamic. Export helpers are separate entry points already (@oge-ui/react-grid/export-excel, …) and load only when imported — see Performance.

'use client';

import dynamic from 'next/dynamic';

// a heavy family the first paint does not need: load it after hydration
const Scheduler = dynamic(
  () => import('@oge-ui/react-scheduler').then((m) => m.OgeScheduler),
  { ssr: false, loading: () => <p>Loading the calendar…</p> },
);