Skip to main content

RSC Migration: Third-Party Library Compatibility

Most third-party React libraries were built before Server Components existed. Many rely on hooks, Context, or browser APIs that are unavailable in Server Components. This guide covers how to identify incompatible libraries, create wrapper patterns, and choose RSC-compatible alternatives.

Part 5 of the RSC Migration Series | Previous: Data Fetching Migration | Next: Troubleshooting

Why Libraries Break in Server Components

Server Components cannot use:

  • useState, useEffect, useRef, useReducer, and most React hooks
  • createContext / useContext
  • Browser APIs (window, localStorage, document)
  • Event handlers (onClick, onChange)

Note on React.memo and forwardRef: These wrappers don't cause errors in Server Components -- the React Flight renderer silently unwraps them. However, their functionality has no effect: memo can't memoize (Server Components don't re-render), and forwardRef can't forward refs (the ref prop is explicitly rejected by the Flight serializer). Libraries that use these wrappers can still render as Server Components, unlike libraries that call hooks. forwardRef is deprecated in React 19 in favor of ref as a regular prop.

Note on ref in Server Components: React.createRef() is available in the server runtime (it's a plain function, not a hook), but the resulting ref cannot be attached to any element. The Flight serializer explicitly rejects the ref prop on any element -- including Client Components -- with: "Refs cannot be used in Server Components, nor passed to Client Components." Refs are inherently a client-side concept -- if a Client Component needs a ref, it should create one itself with useRef().

Any library that relies on these features must be used within a 'use client' boundary. The React Working Group maintains a canonical tracking list of library RSC support status.

The Thin Wrapper Pattern

The most common solution for incompatible libraries: create a minimal 'use client' file that re-exports the component. This works because 'use client' marks a boundary -- the wrapper establishes the server-to-client transition point, and the library code below it automatically becomes client code.

Direct Re-export (Simplest)

// ui/carousel.jsx
'use client';

import { Carousel } from 'acme-carousel';

export default Carousel;

Then use it in a Server Component:

// Page.jsx -- Server Component
import Carousel from './ui/carousel';

export default function Page() {
return (
<div>
<p>View pictures</p>
<Carousel />
</div>
);
}

Named Re-exports (Multiple Components)

// ui/chart-components.jsx
'use client';

export { AreaChart, BarChart, LineChart, Tooltip, Legend } from 'recharts';

Wrapper with Default Props

// ui/date-picker.jsx
'use client';

import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';

export default function AppDatePicker(props) {
return <DatePicker dateFormat="yyyy-MM-dd" {...props} />;
}

Provider Wrapper

// providers/query-provider.jsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';

export default function QueryProvider({ children }) {
const [queryClient] = useState(() => new QueryClient());
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}

CSS-in-JS Libraries

CSS-in-JS is the most impactful compatibility challenge for RSC migration. Runtime CSS-in-JS libraries depend on React Context and re-rendering, which Server Components fundamentally lack.

Runtime CSS-in-JS (Problematic)

LibraryRSC StatusNotes
styled-componentsIn maintenance mode. v6.3.0+ added RSC support via React's <style> tag hoisting before entering maintenance mode.The maintainer stated: "For new projects, I would not recommend adopting styled-components." React Context dependency is the root incompatibility.
EmotionNo native RSC supportWorkaround: wrap all Emotion-styled components in 'use client' files.

Zero-Runtime CSS-in-JS (RSC Compatible)

LibraryNotes
Tailwind CSSNo runtime JS. The standard choice for RSC projects.
CSS ModulesBuilt into most frameworks. No runtime overhead.
Panda CSSZero-runtime, type-safe, created by Chakra UI team. RSC-compatible by design.
Pigment CSSCreated by MUI. Compiles to CSS Modules. Check the Pigment CSS repo for current stability status.
vanilla-extractTypeScript-native. Known issue: .css.ts imports in RSC may need swc-plugin-vanilla-extract workaround.
StyleXFacebook's compile-time solution.
LinariaZero-runtime with familiar styled API.

Migration advice: If you're currently using styled-components or Emotion and your app's performance is acceptable, there's no urgency to migrate. But for new RSC projects, choose a zero-runtime solution.

UI Component Libraries

shadcn/ui

Best RSC compatibility. Copy-paste model means you own the source code and control exactly which components have 'use client'. Built on Radix + Tailwind.

Radix UI

Best-in-class RSC compatibility among full-featured headless libraries. Non-interactive primitives can be Server Components. Interactive primitives (Dialog, Popover, etc.) still need 'use client'.

Material UI (MUI)

All MUI components require 'use client' due to Emotion dependency. None can be used as pure Server Components. v5.14.0+ added 'use client' directives, so components work alongside Server Components without manual wrappers. Use direct imports (e.g., import Button from '@mui/material/Button') instead of barrel imports to avoid bundling the entire library.

Chakra UI

Requires 'use client' for all components due to Emotion runtime. The Chakra team created Panda CSS and Ark UI as RSC-compatible alternatives.

Mantine

All components include 'use client' directives. Cannot use compound components (<Tabs.Tab />) in Server Components -- use non-compound equivalents (<TabsTab />).

Form Libraries

LibraryRSC PatternNotes
React Hook FormClient-only (uses Context). Create a 'use client' form component, import into Server Component. Can combine with Server Actions for submission.Most popular option.
TanStack FormEmerging alternative with RSC-aware architecture.Framework-agnostic.
React 19 built-inuseActionState + useFormStatus hooks work natively with Server Actions.Reduces need for third-party form libraries.

Server Action Form Pattern

// actions.js
'use server';

export async function submitForm(formData) {
const name = formData.get('name');
// Server Actions run in Node.js, so this fetch needs an absolute URL.
// Point RAILS_BASE_URL at Rails' internal URL (for example
// http://127.0.0.1:3000 in development), not the public-facing domain.
const railsBaseUrl = process.env.RAILS_BASE_URL;
if (!railsBaseUrl) {
throw new Error('RAILS_BASE_URL environment variable is required for Server Actions');
}
// Note: This fetch runs server-side, so the Rails endpoint will not receive
// the browser's CSRF token. Use an API-only route or another non-session
// auth boundary. If you switch a Rails endpoint to
// `protect_from_forgery with: :null_session`, add another trust check
// (for example signed tokens, API keys, or same-origin validation) because
// `null_session` avoids the CSRF failure but does not authenticate the
// request.
const res = await fetch(new URL('/api/users', railsBaseUrl), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user: { name } }),
});
if (!res.ok) {
throw new Error(`Failed to create user: ${res.status}`);
}
}
// Page.jsx -- Server Component (works without JavaScript)
import { submitForm } from './actions';

export default function Page() {
return (
<form action={submitForm}>
<input type="text" name="name" />
<button type="submit">Submit</button>
</form>
);
}

React on Rails note: In most React on Rails applications, form submissions go through Rails controllers via standard form posts or API endpoints. Server Actions are a React concept that can complement this, but they are not a replacement for Rails' controller/model layer. For most mutations, continue using your existing Rails API endpoints.

Animation Libraries

LibraryRSC StatusNotes
Framer Motion / MotionClient-only. Relies on browser APIs.Wrap animated elements in 'use client' files.
React SpringClient-only. Uses hooks.Same wrapper pattern.
CSS animationsFully compatible@keyframes, transition, Tailwind animate utilities.
View Transitions APIBrowser-native, compatibleNo React dependency.

Animation Wrapper Pattern

// ui/animated-div.jsx
'use client';

import { motion } from 'motion/react';

export default function AnimatedDiv({ children, ...props }) {
return <motion.div {...props}>{children}</motion.div>;
}

Charting Libraries

LibraryRSC CompatibilityNotes
NivoBest RSC supportPre-renders SVG charts on the server.
RechartsClient-onlySVG + React hooks. Needs 'use client' wrapper.
Chart.js / react-chartjs-2Client-onlyCanvas-based, requires DOM.
D3.jsPartially compatibleData transformation works server-side. DOM manipulation is client-only.
TremorClient-onlyBuilt on Recharts + Tailwind.

Date Libraries

All major date libraries work in Server Components since they are pure utility functions with no React or browser dependencies:

  • date-fns -- tree-shakable, recommended
  • dayjs -- lightweight alternative
  • Moment.js -- works but deprecated and not tree-shakable

Performance benefit: These dependencies stay entirely server-side when used in Server Components, removing them from the client bundle.

Data Fetching Libraries

LibraryRSC PatternNotes
React on Rails Pro async propsRecommended for React on Rails. Rails streams props incrementally via stream_react_component_with_async_props.See Data Fetching Migration for details.
TanStack QueryPrefetch on server with queryClient.prefetchQuery(), hydrate on client with HydrationBoundary.See Data Fetching Migration for details.
Apollo ClientServer-side queries in Server Components, ApolloProvider for client queries.Requires 'use client' wrapper for provider.
SWRClient-only hooks. Use fallbackData pattern: fetch in Server Component, pass as props.See Data Fetching Migration for details.

Internationalization

LibraryRSC PatternNotes
Rails I18n + react-intlPass translations from Rails controller as props. Server Components use the translations object directly; Client Components use <IntlProvider> + useIntl().Recommended for React on Rails. See Context guide.
i18next / react-i18nextRequires 'use client' for hook-based usage. Server Components can use i18next directly (no hooks).Framework-agnostic alternative.

Authentication

In React on Rails, authentication is handled by Rails (Devise, OmniAuth, etc.) before the React component renders. The controller passes the authenticated user as props:

# Rails controller handles auth, passes user to component
stream_react_component("Dashboard", props: { user: current_user.as_json })

This is a simpler model than client-side auth libraries -- Rails middleware handles sessions, CSRF protection, and authorization before any React code executes. See the auth provider pattern for passing auth data to nested Client Components via Context.

The Barrel File Problem

Barrel files (index.js files that re-export from many modules) cause serious issues with RSC.

The Problem

// components/index.js -- barrel file
export { Button } from './Button';
export { Modal } from './Modal';
export { Chart } from './Chart';
// ... hundreds more

When you import { Button } from './components', the bundler must parse the entire barrel file and all transitive imports. With RSC:

  1. Client boundary infection: Adding 'use client' to a barrel file forces ALL exports into the client bundle
  2. Tree-shaking failure: Bundlers struggle to eliminate unused exports
  3. Mixed server/client exports: A barrel file that re-exports both server and client components can cause unexpected bundle inclusion

The Solution: Direct Imports

The most reliable fix is to bypass barrel files entirely. Use direct imports instead:

// BAD: Import from barrel -- pulls in everything
import { Button } from './components';
import { AlertIcon } from 'lucide-react';

// GOOD: Import directly -- only bundles what you use
import Button from './components/Button';
import AlertIcon from 'lucide-react/dist/esm/icons/alert';

For third-party packages, check if the library provides direct import paths (most popular libraries do). For example:

  • @mui/material/Button instead of { Button } from '@mui/material'
  • lodash-es/debounce instead of { debounce } from 'lodash-es'

For Your Own Code

Avoid creating barrel files that mix server and client components. If you must use a barrel file, keep separate barrels for server and client exports:

components/
├── server/index.js # Only server components
├── client/index.js # Only 'use client' components
├── ServerHeader.jsx
├── ClientSearch.jsx
└── ...

The server-only and client-only Packages

These packages act as build-time guards to prevent code from running in the wrong environment:

// lib/database.js
import 'server-only'; // Build error if imported in a Client Component

export async function getUser(id) {
return await db.users.findUnique({ where: { id } });
}
// lib/analytics.js
import 'client-only'; // Build error if imported in a Server Component

export function trackEvent(event) {
window.analytics.track(event);
}

Use server-only for:

  • Database access modules
  • Modules that use API keys or secrets
  • Server-side utility functions

Use client-only for:

  • Browser analytics
  • Modules that access window, document, localStorage
  • Client-specific utilities

Library Compatibility Decision Matrix

CategoryRSC-Native ChoicesRequires 'use client' WrapperAvoid / Migrate Away From
StylingTailwind, CSS Modules, Panda CSSvanilla-extract (with workaround)styled-components (maintenance mode), Emotion
UI Componentsshadcn/ui, Radix (non-interactive)MUI, Chakra, Mantine, Radix (interactive)CSS-in-JS-dependent UI libs without migration path
FormsReact 19 useActionState + Server ActionsReact Hook Form, TanStack FormFormik (less maintained)
AnimationCSS animations, Tailwind animateFramer Motion/Motion, React Spring--
ChartsNivo (SSR support)Recharts, Tremor, Chart.js--
Data FetchingReact on Rails Pro async props, native fetch in Server ComponentsTanStack Query (with hydration), Apollo, SWR--
StateServer Component props, React.cacheZustand, Jotai (v2.6+), Redux ToolkitRecoil (discontinued)
i18nRails I18n + react-intlreact-i18next, i18next--
AuthRails auth (Devise, etc.) via controller props----
Date Utilsdate-fns, dayjs (pure functions)--Moment.js (not tree-shakable)

Next Steps