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. The v6.3.x series added incremental RSC compatibility fixes (e.g., suppressing server-side warnings in v6.3.3, fixing createGlobalStyle unmount behavior with React 19's precedence attribute in v6.3.9).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. Submit to Rails controller endpoints via fetch.Most popular option.
TanStack FormEmerging alternative with RSC-aware architecture. Submit to Rails controller endpoints.Framework-agnostic.
Standard formsUse Rails' standard form helpers (form_with, form_tag) for non-React forms. For React forms, submit via fetch to Rails API endpoints.No library needed for simple forms.

Form Submission Pattern

Important: React on Rails does not support Server Actions ('use server'). Server Actions run on the Node renderer, which has no access to Rails models, sessions, cookies, or CSRF protection. Use Rails controllers for all form submissions.

// UserForm.jsx -- Client Component
'use client';

import { useState } from 'react';
import ReactOnRails from 'react-on-rails';

export default function UserForm() {
const [name, setName] = useState('');

async function handleSubmit(e) {
e.preventDefault();
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': ReactOnRails.authenticityToken(),
},
body: JSON.stringify({ user: { name } }),
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
setName('');
}

return (
<form onSubmit={handleSubmit}>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
}
<%# ERB view %>
<%= stream_react_component("UserForm") %>

Pattern 2: Standard Rails form (no JavaScript required)

<%= form_with(model: @user, url: users_path, local: true) do |f| %>
<%= f.text_field :name %>
<%= f.submit "Submit" %>
<% end %>

Note: local: true is required for Rails 5.1–6.0, where form_with defaults to remote: true (Ajax). Rails 6.1+ defaults to local: true, so it can be omitted on newer versions.

Both patterns leverage Rails' full controller/model layer -- authentication, authorization, CSRF protection, and validations all work as expected.

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 streamingRecommended for React on Rails. Rails streams components via stream_react_component.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(only: [:id, :name, :email]) })

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
FormsRails controller endpoints + standard formsReact 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 streaming, 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