Guide

React Server Components

Import RSC-safe layout primitives from the ./server entry for use in Next.js App Router Server Components, with a client boundary pattern for interactive components.

Overview

Next.js App Router renders Server Components (RSC) by default. React hooks, context, and "use client" are not allowed in Server Components. Stareezy UI ships a dedicated "./server" entry that exports hook-free layout primitives safe for use in RSC.

The "./server" entry contains no useState, useEffect, useContext, or any other React hook. It also has no "use client" directive — it is fully server-safe.

Server-safe primitives

The following primitives are exported from the "./server" entry. They resolve theme tokens via CSS custom properties (var(--token-id)) instead of React hooks:

BoxViewStackTextDivider
// Import from the server entry — safe in any Server Component
import { Box, View, Stack, Text, Divider } from '@stareezy-ui/components/server'

Client entry — interactive components

The default "." client entry is unchanged and continues to export all interactive components with their "use client" boundaries:

// Default client entry — use inside Client Components
import { Button, Input, Modal, Tabs, Dropdown } from '@stareezy-ui/components'

Next.js App Router example

Here is a complete example of a page that uses both RSC-safe primitives and interactive client components:

1
Server Component page (no use client needed)
// app/page.tsx — Server Component (default in App Router)
import { Box, Stack, Text } from '@stareezy-ui/components/server'
import { HeroActions } from './HeroActions'  // Client Component below

export default function HomePage() {
  // Server-side data fetch — no useEffect, no useState
  const data = await fetchData()

  return (
    <Box p={{ base: 16, md: 32 }}>
      <Stack gap={16}>
        <Text style={{ fontSize: 32, fontWeight: 800 }}>
          Welcome
        </Text>
        <Text style={{ fontSize: 16, opacity: 0.7 }}>
          {data.tagline}
        </Text>

        {/* Client Component wrapped in a boundary */}
        <HeroActions />
      </Stack>
    </Box>
  )
}
2
Client Component with interactive elements
// app/HeroActions.tsx — Client Component
'use client'

import { Button, Modal } from '@stareezy-ui/components'
import { useState } from 'react'

export function HeroActions() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <Button onPress={() => setOpen(true)} variant="primary">
        Get started
      </Button>

      <Modal open={open} onClose={() => setOpen(false)} title="Get started">
        {/* Modal content */}
      </Modal>
    </>
  )
}
3
Layout with server primitives and client nav
// app/layout.tsx
import { Box, Stack } from '@stareezy-ui/components/server'
import { ThemeProvider } from '@stareezy-ui/tokens'
import { NavBar } from '@/components/NavBar'  // 'use client'
import './stareezy.config'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <ThemeProvider theme="aurora">
          {/* Server primitive wraps everything */}
          <Stack minH="100vh">
            {/* Client boundary for interactive nav */}
            <NavBar />

            {/* Page content (may be Server or Client Components) */}
            <Box flex={1} p={{ base: 16, md: 32 }}>
              {children}
            </Box>
          </Stack>
        </ThemeProvider>
      </body>
    </html>
  )
}

Client boundary pattern

The recommended pattern is to push "use client" as deep as possible — only the components that actually need hooks or browser APIs need to be Client Components.

// ✅ Good — boundary is at the interactive leaf
// Server Component:
import { Box, Stack, Text } from '@stareezy-ui/components/server'
import { LikeButton } from './LikeButton'  // 'use client'

export default function Post({ post }) {
  return (
    <Box p={20}>
      <Text style={{ fontWeight: 700 }}>{post.title}</Text>
      <Text>{post.body}</Text>
      <LikeButton postId={post.id} />  {/* Only this needs client */}
    </Box>
  )
}

// ❌ Avoid — marking the whole page as client
'use client'
import { Box, Stack, Text } from '@stareezy-ui/components'  // ← not needed
...
Server primitives (Box, Stack, etc.) from the "./server" entry can be used as wrappers around Client Components. Props including BoxLayoutProps work exactly as on the client entry.

ThemeProvider in App Router

ThemeProvider from @stareezy-ui/tokens is a "use client" component because it manages theme state. Wrap it in a client component shell when used in the root layout:

// app/Providers.tsx
'use client'

import { ThemeProvider } from '@stareezy-ui/tokens'
import type { ReactNode } from 'react'

export function Providers({ children }: { children: ReactNode }) {
  return <ThemeProvider theme="aurora">{children}</ThemeProvider>
}

// app/layout.tsx — import Providers, keep layout a Server Component
import { Providers } from './Providers'
import { Box } from '@stareezy-ui/components/server'

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>
          <Box minH="100vh">{children}</Box>
        </Providers>
      </body>
    </html>
  )
}
On web, the server-entry primitives resolve token values through CSS custom properties injected by ThemeProvider. As long as ThemeProvider is an ancestor in the component tree, the server primitives render with the correct theme colors — even though they have no React hooks themselves.