Building Bulletproof React Components

plurby2 pts0 comments

Building Bulletproof React Components - Shu Ding

Building Bulletproof React Components

I skate to where the puck is going to be, not where it has been.

— Wayne Gretzky

Most components are built for the happy path. They work—until they don’t. The real world is hostile. Server rendering. Hydration. Multiple instances. Concurrent rendering. Async children. Portals... Your component could face all of them. The question is whether it survives.

The real test isn’t whether your component works on your current page. It’s whether it works when someone else uses it—in conditions you didn’t plan for. That’s when fragile components break.

Here’s how to make it survive.

Make It Server -Proof

Make It Hydration -Proof

Make It Instance -Proof

Make It Concurrent -Proof

Make It Composition -Proof

Make It Portal -Proof

Make It Transition -Proof

Make It Activity -Proof

Make It Leak -Proof

Make It Future -Proof*

#Make It Server-Proof

A simple theme provider that reads the user’s preference from localStorage:

function ThemeProvider({ children }) {<br>const [theme, setTheme] = useState(<br>localStorage.getItem('theme') || 'light'

return div className={theme}>{children}div><br>Sidenote: Crashes in SSR—reads theme from localStorage<br>But localStorage doesn’t exist on the server. In Next.js, Remix, or any SSR framework, this crashes the build. Move browser APIs into useEffect:

function ThemeProvider({ children }) {<br>const [theme, setTheme] = useState('light')

useEffect(() => {<br>setTheme(localStorage.getItem('theme') || 'light')<br>}, [])

return div className={theme}>{children}div><br>Sidenote: useEffect defers localStorage to client-side only<br>Now it renders on the server without crashing.

#Make It Hydration-Proof

I also call this waterproof. The server-safe version works, but users see a flash. Server renders light, client hydrates, then the effect runs and switches to dark:

function ThemeProvider({ children }) {<br>const [theme, setTheme] = useState('light')

useEffect(() => {<br>setTheme(localStorage.getItem('theme') || 'light')<br>}, [])

return div className={theme}>{children}div><br>Sidenote: Flash of wrong theme—useEffect runs after hydration<br>Inject a synchronous script that sets the correct value before browser paints and React hydrates. The DOM already has the right class when React takes over:

function ThemeProvider({ children }) {<br>return (<br><><br>div id="theme">{children}div><br>script dangerouslySetInnerHTML={{ __html: `<br>try {<br>const theme = localStorage.getItem('theme') || 'light'<br>document.getElementById('theme').className = theme<br>} catch (e) {}<br>`}} />

Sidenote: Inline script sets theme before browser paints<br>No mismatch, no flash.

#Make It Instance-Proof

The hydration-proof version targets a hardcoded id="theme". But what if someone uses two ThemeProviders?

function App() {<br>return (<br><><br>ThemeProvider>MainContent />ThemeProvider><br>AlwaysLightThemeContent /><br>ThemeProvider>Sidebar />ThemeProvider>

Sidenote: Multiple instances—both scripts target the same ID<br>Both scripts fight over the same element. Use useId to generate stable, unique IDs per instance:

function ThemeProvider({ children }) {<br>const id = useId()<br>return (<br><><br>div id={id}>{children}div><br>script dangerouslySetInnerHTML={{ __html: `<br>try {<br>const theme = localStorage.getItem('theme') || 'light'<br>document.getElementById('${id}').className = theme<br>} catch (e) {}<br>`}} />

Sidenote: useId generates unique IDs per instance<br>Now multiple instances coexist safely.

#Make It Concurrent-Proof

Now let’s make the theme server-driven. A Server Component that fetches user preferences:

async function ThemeProvider({ children }) {<br>const prefs = await db.preferences.get(userId)

return div className={prefs.theme}>{children}div><br>Sidenote: Server Component fetches preferences from database<br>Similar to before, render it in two places and you might get two identical database queries. Wrap the query in React.cache to deduplicate within a single request:

import { cache } from 'react'

const getPreferences = cache(<br>userId => db.preferences.get(userId)

async function ThemeProvider({ children }) {<br>const prefs = await getPreferences(userId)

return div className={prefs.theme}>{children}div><br>Sidenote: React cache() deduplicates concurrent calls<br>Same query, called from anywhere, hits the database once.

#Make It Composition-Proof

Sometimes you want to pass data to children as props, which traditionally meant using React.cloneElement:

function ThemeProvider({ children }) {<br>const [theme, setTheme] = useState('light')

return React.Children.map(children, (child) => {<br>return React.cloneElement(child, { theme })<br>})<br>Sidenote: Passes theme to children via cloneElement<br>But with React Server Components, React.lazy, or "use cache", children might be a Promise or an opaque reference—cloneElement won't work. Use context instead:

const ThemeContext = createContext('light')

function ThemeProvider({ children }) {<br>const [theme, setTheme] = useState('light')

return (<br>ThemeContext.Provider...

theme children make proof react themeprovider

Related Articles