Server Components

Server and client components with proper bundling

Server Components

Build a library that ships both React Server Components and Client Components. Packem preserves the "use client" and "use server" directives and scopes the client/server boundaries so app bundlers like Next.js can integrate them correctly.

Overview

This example demonstrates:

  • Library directives "use client" and "use server"
  • How Packem preserves and hoists directives in the output
  • A dedicated react-server runtime bundle via an input-source override
  • Conditional exports that point app bundlers at the right entry

How Packem Handles Directives

From the Packem behavior:

  • If you use "use client" or "use server" in an entry file, the directive is preserved on top and the entry's output becomes a client component.
  • If you use "use client" or "use server" in a file that is a dependency of an entry, that file is split into a separate chunk and the directive is hoisted to the top of that chunk.

This means client and server boundaries stay properly scoped: when the library is integrated into an app such as Next.js, the app bundler can transform the client components and server actions correctly.

Project Structure

react-rsc-lib/
├── src/
│   ├── Counter.tsx              # client component
│   ├── actions.ts              # server actions
│   ├── ServerInfo.tsx          # server component
│   ├── index.ts                # default runtime entry
│   └── index.react-server.ts   # react-server runtime entry
├── package.json
├── packem.config.ts
└── tsconfig.json

Component Implementation

src/Counter.tsx — a client component

'use client'

import React from 'react'

export function Counter() {
  const [count, setCount] = React.useState(0)

  return (
    <button onClick={() => setCount((c) => c + 1)}>
      Count: {count}
    </button>
  )
}

src/actions.ts — server actions

'use server'

export async function submitForm(data: FormData) {
  const name = data.get('name')
  // ...persist on the server
  return { ok: true, name }
}

src/ServerInfo.tsx — a server component (no directive needed)

import React from 'react'

export async function ServerInfo() {
  const now = new Date().toISOString()

  return <p>Rendered on the server at {now}</p>
}

src/index.ts — the default entry

export { Counter } from './Counter'
export { submitForm } from './actions'
export { ServerInfo } from './ServerInfo'

src/index.react-server.ts — the react-server runtime entry

This override file is bundled separately for the react-server condition. Here you can expose a server-safe surface (for example omitting client-only components):

export { submitForm } from './actions'
export { ServerInfo } from './ServerInfo'

The input-source override convention is src/index.react-server.{ext}. Packem builds it into a distinct react-server bundle that you wire up through the react-server export condition.

Configuration

package.json

The react-server condition is a special platform condition. Place it before the generic import/require conditions so app bundlers running in the server layer resolve it first:

{
  "name": "@myorg/react-rsc",
  "version": "1.0.0",
  "type": "module",
  "files": [
    "dist"
  ],
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "react-server": "./dist/index.react-server.js",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  },
  "scripts": {
    "build": "packem build",
    "dev": "packem build --watch"
  },
  "peerDependencies": {
    "react": ">=18.0.0",
    "react-dom": ">=18.0.0"
  },
  "devDependencies": {
    "@visulima/packem": "^2",
    "@types/react": "^18.0.0",
    "@types/react-dom": "^18.0.0",
    "react": "^18.0.0",
    "react-dom": "^18.0.0",
    "typescript": "^5.3.0"
  }
}

Packem Configuration

The react preset handles the JSX/Babel setup. Packem detects the directives in your source and the react-server override entry from the exports map automatically:

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'

export default defineConfig({
  preset: 'react',
  transformer,
  sourcemap: true,
  declaration: true,
})

TypeScript Configuration

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "declaration": true,
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "skipLibCheck": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Building

npm run build

Output Structure

dist/
├── index.js                 # default runtime entry (client boundary preserved)
├── index.cjs
├── index.d.ts
├── index.react-server.js    # react-server runtime bundle
└── ...chunks/               # hoisted "use client" / "use server" chunks

The "use client" directive from Counter.tsx is preserved at the top of its chunk, and the "use server" directive from actions.ts is hoisted to the top of the server-action chunk, so consuming bundlers can identify each boundary.

Shared Modules Across Runtimes

When the default and react-server bundles need to share a single instance of a module (for example a React context), use the shared module convention [name].shared-runtime.{ext}:

'use client'

// src/app-context.shared-runtime.ts
import React from 'react'

export const AppContext = React.createContext(null)

Import it from both entries:

// src/index.ts
import { AppContext } from './app-context.shared-runtime'

// src/index.react-server.ts
import { AppContext } from './app-context.shared-runtime'

Packem bundles app-context.shared-runtime into a single chunk shared across both runtime bundles, ensuring only one instance exists.

Next Steps

Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues