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
| Framework | helix-ui works | Notes |
|---|---|---|
| Vite + React | ✅ | Default 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 v7 | ✅ | Browser runtime; no RSC dance. |
| Astro Islands | ✅ | Use 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 Native | ❌ | Out of scope — helix-ui targets the web. |
| Stencil / Lit / Web Components | ❌ | helix-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:
| Component | Could be server-safe? | Why we still ship as client |
|---|---|---|
Box, Stack, Grid, Flex | ✅ logically | Convenience — one boundary instead of two. |
Text, Heading | ✅ logically | Same. |
Card, Badge, Callout (no JS) | ✅ logically | Same. |
| Everything else | ❌ | Uses hooks. |
If you need a Stack-equivalent in a server component, this two-line version costs nothing:
// app/components/Stack.tsx — server-safe Stackimport 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 componentimport { 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:
'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
---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/slides’
pptxgenjs) 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:
- Framework + version (
[email protected],[email protected], etc.). - The exact error message.
- A 10-line repro.
We treat RSC compatibility as a P0 — Next/Astro are 80% of new helix-ui adoption.