After years of building React applications, I've noticed that a handful of patterns come up again and again. Mastering these will make you a significantly better React developer.
1. Container / Presentational Pattern
Separate data-fetching logic from rendering:
// Container: handles data
function UserContainer() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => { fetchUser().then(setUser); }, []);
if (!user) return <Spinner />;
return <UserCard user={user} />;
}
// Presentational: pure rendering
function UserCard({ user }: { user: User }) {
return <div>{user.name}</div>;
}
2. Custom Hooks
Extract stateful logic into reusable hooks:
function useWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const update = () =>
setSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener("resize", update);
update();
return () => window.removeEventListener("resize", update);
}, []);
return size;
}
3. Compound Components
Build flexible component APIs:
function Tabs({ children }: { children: React.ReactNode }) {
const [active, setActive] = useState(0);
return (
<TabContext.Provider value={{ active, setActive }}>
{children}
</TabContext.Provider>
);
}
Tabs.List = TabList;
Tabs.Panel = TabPanel;
Usage: <Tabs><Tabs.List /><Tabs.Panel /></Tabs>
4. Render Props
Pass rendering control to the parent:
function MouseTracker({ render }: { render: (pos: Position) => React.ReactNode }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
return (
<div onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })}>
{render(pos)}
</div>
);
}
5. Error Boundaries
Gracefully handle component errors:
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
render() {
if (this.state.hasError) return <div>Something went wrong.</div>;
return this.props.children;
}
}
6. Lazy Loading with Suspense
const HeavyChart = React.lazy(() => import("./HeavyChart"));
function Dashboard() {
return (
<Suspense fallback={<Spinner />}>
<HeavyChart />
</Suspense>
);
}
7. Controlled vs Uncontrolled Inputs
Prefer controlled inputs for form validation; use uncontrolled only when needed for performance.
8. Memoization
Use React.memo, useMemo, and useCallback wisely โ but only when you have a measured performance problem.
9. Context + useReducer for State Management
For medium-complexity state, skip Redux and reach for useReducer with Context:
const [state, dispatch] = useReducer(reducer, initialState);
10. Portals
Render modals and tooltips outside the main DOM hierarchy:
ReactDOM.createPortal(<Modal />, document.body)
These patterns aren't rules โ they're tools. Use them when they solve a real problem, and don't over-engineer. Happy coding! ๐