Skip to content

React Server Components

helix-ui targets React 18.2 – 19.x, and every component in @helix-ui/core is marked 'use client' at the top of its source. Practical implications below.

TL;DR

Frameworkhelix-ui worksNotes
Vite + ReactDefault development happy path.
Next.js 14 (Pages Router)Drop in.
Next.js 14 / 15 (App Router)helix-ui components automatically opt out of RSC via their 'use client' headers. You can import them from server components without manually adding directives.
Remix / React Router v7Browser runtime; no RSC dance.
Astro IslandsUse client:load / client:idle / client:visible on the helix-ui component import.
Bun (default)Bun’s JSX runtime is React-compatible.
React Server Components (raw)⚠️helix-ui is a client component library. You can import helix-ui components from server components; they hydrate normally. You cannot render them at the server-only boundary.
React NativeOut of scope — helix-ui targets the web.
Stencil / Lit / Web Componentshelix-ui is React-only.

The “use client” decision

Every component in @helix-ui/core starts with:

'use client';
import { useState, useRef, ... } from 'react';
// ...

This is intentional. The whole component surface uses useState, useRef, useContext, useEffect, or forwardRef — none of which are RSC-safe. Rather than make consumers wonder which one is which, we mark them all explicitly.

What this means for you:

  • In a Next.js App Router project, you can import { Button } from '@helix-ui/core' from a server component file. The component will be rendered on the client; React + Next.js handle the boundary.
  • You don’t need to mark your file 'use client' just because you import a helix-ui component. Only mark your file 'use client' if your code uses client-only APIs (state, effects, event handlers).
  • helix-ui components add a JS payload. If you’re trying to render a fully-static marketing page, prefer a primitive like <a> to <Button> where possible. See the “primitives we keep server-safe” section below.

”But I want a server component.”

helix-ui is a behavior design system, not a primitives design system. The components are interactive by nature (Dialog, Sheet, Popover, Menu, …). RSC versions of these are an open research area in the React community; we’re not going to ship half-broken ones.

That said, a handful of helix-ui primitives are pure layout/typography and could in principle be server-only:

ComponentCould be server-safe?Why we still ship as client
Box, Stack, Grid, Flex✅ logicallyConvenience — one boundary instead of two.
Text, Heading✅ logicallySame.
Card, Badge, Callout (no JS)✅ logicallySame.
Everything elseUses hooks.

If you need a Stack-equivalent in a server component, this two-line version costs nothing:

// app/components/Stack.tsx — server-safe Stack
import type { CSSProperties, ReactNode } from 'react';
export function Stack({ children, gap = 4, ...rest }: {
children: ReactNode;
gap?: number;
style?: CSSProperties;
}) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: `${gap * 4}px`, ...rest.style }}>
{children}
</div>
);
}

We’ve thought about extracting helix-ui’s pure-layout primitives into a separate @helix-ui/server package. That’s tracked as RFC 0003 — server primitives split and we’d welcome a contributor.

Next.js App Router — concrete example

// app/page.tsx — this is a server component
import { Card, Text, Button } from '@helix-ui/core';
import { fetchUser } from '@/lib/data';
export default async function Page() {
const user = await fetchUser(); // server-only fetch is fine
return (
<Card>
<Text>Welcome, {user.name}!</Text>
<Button>Continue</Button> {/* ← rendered on the client, no extra dance */}
</Card>
);
}

This compiles and runs. Next.js sees the 'use client' in the helix-ui package’s compiled JS and creates the right boundary automatically.

When you do need ‘use client’ in your file

When your code does anything interactive:

app/profile-form.tsx
'use client'; // ← because YOUR code uses useState
import { useState } from 'react';
import { Button } from '@helix-ui/core';
export function ProfileForm() {
const [name, setName] = useState('');
return <Button onClick={() => save(name)}>Save</Button>;
}

Server Actions (Next 14+)

helix-ui has no built-in useFormStatus integration yet. Server actions work with the bare HTML form element; helix-ui’s <Form> component is a styled wrapper that passes through, so this works:

'use client';
import { Form, TextInput, Button } from '@helix-ui/core';
import { saveProfile } from '@/actions/profile';
export function ProfileForm() {
return (
<Form action={saveProfile}>
<TextInput name="name" />
<Button type="submit">Save</Button>
</Form>
);
}

useFormStatus() works inside the form; we’ll ship a dedicated useHelixUIFormStatus hook + integration when we land RFC 0002 — first-class form story.

Astro Islands

src/pages/index.astro
---
import { Button } from '@helix-ui/core';
---
<Button client:load>Click me</Button>

client:visible and client:idle work for non-critical components. helix-ui’s bundle size makes client:idle viable even on landing pages.

Edge runtime

helix-ui’s runtime has no Node-specific APIs. It runs on:

  • Vercel Edge
  • Cloudflare Workers
  • Deno Deploy
  • Bun

The lazy-loaded exporters (@helix-ui/document’s docx, @helix-ui/slidespptxgenjs) require Node — they’re not edge-compatible. Don’t call exportToDocx/exportToPptx from edge functions; do it from a Node serverless function or in the browser.

React 19

helix-ui supports React 19. Ref-as-prop and the new <form action> patterns work. We have not yet migrated away from forwardRef internally — that’s a v0.2 task. There is no runtime impact today.

Reporting issues

If you hit “Server Component cannot use useState” or similar errors, that’s almost always your file missing 'use client', not a helix-ui bug. But if you’ve checked that twice and it still breaks, please open an issue with:

We treat RSC compatibility as a P0 — Next/Astro are 80% of new helix-ui adoption.