Even a small theme preference can produce confusing behavior: a flash on startup, a lost setting after refresh or components disagreeing about the active theme. The React/Next.js portfolio experiment in my archive is a useful context for separating current in-memory state from a persisted browser preference.
Keep the initial render independent of browser storage
The sample starts with dark and reads localStorage after mounting. That avoids accessing window during server rendering and lets the initial server and client output share one default. It belongs in a React client-component context, not in a server authentication flow.
import {useEffect,useState} from 'react';
export function Preference(){
const [theme,setTheme]=useState('dark');
useEffect(()=>{
try{const saved=localStorage.getItem('theme');
if(saved==='light'||saved==='dark')setTheme(saved);
}catch{/* Browser storage may be unavailable. */}
},[]);
function toggle(){
const next=theme==='dark'?'light':'dark';
setTheme(next);
try{localStorage.setItem('theme',next);}catch{}
}
return <button onClick={toggle}>{theme}</button>;
}
Only light and dark are accepted from storage. An unavailable or restricted storage API does not stop the button working because reads and writes are guarded. State changes are still applied even if persistence fails; the immediate UI is not held hostage by browser storage.
Use one Provider for a shared preference
Preference above owns local state. If several components need the theme, duplicating useState in each creates separate sources. The following independent Context example establishes one owner. In Next.js it must live under a client boundary.
import {createContext, useContext, useEffect, useState} from 'react';
import type {ReactNode} from 'react';
type Theme = 'dark' | 'light';
const ThemeContext = createContext<{
theme: Theme;
toggle: () => void;
} | null>(null);
export function ThemeProvider({children}: {children: ReactNode}) {
const [theme, setTheme] = useState<Theme>('dark');
const [loaded, setLoaded] = useState(false);
useEffect(() => {
try {
const saved = localStorage.getItem('portfolio.theme');
if (saved === 'light' || saved === 'dark') setTheme(saved);
} catch {}
setLoaded(true);
}, []);
useEffect(() => {
if (!loaded) return;
try { localStorage.setItem('portfolio.theme', theme); } catch {}
}, [theme, loaded]);
const toggle = () => setTheme(t => t === 'dark' ? 'light' : 'dark');
return <ThemeContext.Provider value={{theme, toggle}}>
{children}
</ThemeContext.Provider>;
}
export function useTheme() {
const value = useContext(ThemeContext);
if (!value) throw new Error('ThemeProvider is required');
return value;
}
The loaded flag prevents the persistence effect from writing the default over the saved preference before initialization finishes. The state updater stays pure; storage writes are not placed inside it. Consumers read one React state through useTheme. Context does not make localStorage reactive automatically.
Initialization timing is a visible design choice
A saved light theme can appear after the initial dark render. A server-readable cookie or a controlled early theme application are alternative designs when that transition matters. Hiding a hydration warning does not reconcile different initial output. Decide whether the small transition is acceptable for the product.
React Strict Mode can exercise effect setup and cleanup again during development. Repeating a deterministic storage read or write is different from leaking an event subscription, which needs cleanup. The storage event can synchronize changes from other documents; updates in the current component tree still flow through Context state.
A preference is not authorization
Users can modify browser storage. That is acceptable for a theme and unacceptable as proof of admin permission. A local isAdmin=true must not grant server access. Prefixing keys helps avoid collisions between applications sharing an origin, but does not make their contents trustworthy.
Test a saved light value, unknown input, storage exceptions and a consumer outside the Provider. This Context is intended to give one genuinely shared preference one owner, not to become an indiscriminate replacement for every kind of application state.
React useContext; Effect lifecycle and server-rendering boundaries.