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 hookscreateContext/useContext- Browser APIs (
window,localStorage,document) - Event handlers (
onClick,onChange)
Note on
React.memoandforwardRef: These wrappers don't cause errors in Server Components -- the React Flight renderer silently unwraps them. However, their functionality has no effect:memocan't memoize (Server Components don't re-render), andforwardRefcan't forward refs (therefprop is explicitly rejected by the Flight serializer). Libraries that use these wrappers can still render as Server Components, unlike libraries that call hooks.forwardRefis deprecated in React 19 in favor ofrefas a regular prop.Note on
refin 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 therefprop 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 withuseRef().
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)
| Library | RSC Status | Notes |
|---|---|---|
| styled-components | In 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. |
| Emotion | No native RSC support | Workaround: wrap all Emotion-styled components in 'use client' files. |
Zero-Runtime CSS-in-JS (RSC Compatible)
| Library | Notes |
|---|---|
| Tailwind CSS | No runtime JS. The standard choice for RSC projects. |
| CSS Modules | Built into most frameworks. No runtime overhead. |
| Panda CSS | Zero-runtime, type-safe, created by Chakra UI team. RSC-compatible by design. |
| Pigment CSS | Created by MUI. Compiles to CSS Modules. Check the Pigment CSS repo for current stability status. |
| vanilla-extract | TypeScript-native. Known issue: .css.ts imports in RSC may need swc-plugin-vanilla-extract workaround. |
| StyleX | Facebook's compile-time solution. |
| Linaria | Zero-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
| Library | RSC Pattern | Notes |
|---|---|---|
| React Hook Form | Client-only (uses Context). Create a 'use client' form component, import into Server Component. Submit to Rails controller endpoints via fetch. | Most popular option. |
| TanStack Form | Emerging alternative with RSC-aware architecture. Submit to Rails controller endpoints. | Framework-agnostic. |
| Standard forms | Use 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: trueis required for Rails 5.1–6.0, whereform_withdefaults toremote: true(Ajax). Rails 6.1+ defaults tolocal: 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
| Library | RSC Status | Notes |
|---|---|---|
| Framer Motion / Motion | Client-only. Relies on browser APIs. | Wrap animated elements in 'use client' files. |
| React Spring | Client-only. Uses hooks. | Same wrapper pattern. |
| CSS animations | Fully compatible | @keyframes, transition, Tailwind animate utilities. |
| View Transitions API | Browser-native, compatible | No 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
| Library | RSC Compatibility | Notes |
|---|---|---|
| Nivo | Best RSC support | Pre-renders SVG charts on the server. |
| Recharts | Client-only | SVG + React hooks. Needs 'use client' wrapper. |
| Chart.js / react-chartjs-2 | Client-only | Canvas-based, requires DOM. |
| D3.js | Partially compatible | Data transformation works server-side. DOM manipulation is client-only. |
| Tremor | Client-only | Built 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
| Library | RSC Pattern | Notes |
|---|---|---|
| React on Rails Pro streaming | Recommended for React on Rails. Rails streams components via stream_react_component. | See Data Fetching Migration for details. |
| TanStack Query | Prefetch on server with queryClient.prefetchQuery(), hydrate on client with HydrationBoundary. | See Data Fetching Migration for details. |
| Apollo Client | Server-side queries in Server Components, ApolloProvider for client queries. | Requires 'use client' wrapper for provider. |
| SWR | Client-only hooks. Use fallbackData pattern: fetch in Server Component, pass as props. | See Data Fetching Migration for details. |
Internationalization
| Library | RSC Pattern | Notes |
|---|---|---|
| Rails I18n + react-intl | Pass 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-i18next | Requires '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:
- Client boundary infection: Adding
'use client'to a barrel file forces ALL exports into the client bundle - Tree-shaking failure: Bundlers struggle to eliminate unused exports
- 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/Buttoninstead of{ Button } from '@mui/material'lodash-es/debounceinstead 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
| Category | RSC-Native Choices | Requires 'use client' Wrapper | Avoid / Migrate Away From |
|---|---|---|---|
| Styling | Tailwind, CSS Modules, Panda CSS | vanilla-extract (with workaround) | styled-components (maintenance mode), Emotion |
| UI Components | shadcn/ui, Radix (non-interactive) | MUI, Chakra, Mantine, Radix (interactive) | CSS-in-JS-dependent UI libs without migration path |
| Forms | Rails controller endpoints + standard forms | React Hook Form, TanStack Form | Formik (less maintained) |
| Animation | CSS animations, Tailwind animate | Framer Motion/Motion, React Spring | -- |
| Charts | Nivo (SSR support) | Recharts, Tremor, Chart.js | -- |
| Data Fetching | React on Rails Pro streaming, native fetch in Server Components | TanStack Query (with hydration), Apollo, SWR | -- |
| State | Server Component props, React.cache | Zustand, Jotai (v2.6+), Redux Toolkit | Recoil (discontinued) |
| i18n | Rails I18n + react-intl | react-i18next, i18next | -- |
| Auth | Rails auth (Devise, etc.) via controller props | -- | -- |
| Date Utils | date-fns, dayjs (pure functions) | -- | Moment.js (not tree-shakable) |
Next Steps
- Troubleshooting and Common Pitfalls -- debugging and avoiding problems
- Data Fetching Migration -- migrating from useEffect to server-side fetching