Beyond Basic React
Writing React components is easy. Writing React components that scale, that other developers understand instantly, and that don't become maintenance nightmares—that's an art. These patterns have transformed how I build applications.
The biggest shift for me was moving from thinking of components as UI boxes to thinking of them as APIs. Once a component is treated as an API surface, composition, predictability, and documentation become much more important than cleverness.
1. Compound Components
This pattern changed everything for me. Instead of passing dozens of props to configure a component, you compose it from smaller pieces. Think of how HTML's <select> and <option> work together.
The Power
- Flexible composition without prop drilling
- Clear, declarative APIs
- Each piece is independently testable
- Users control the layout
I use this pattern for Tabs, Accordions, Dropdown menus, and any component where users need control over structure.
Example: Tabs API
1function Tabs({ children }: { children: React.ReactNode }) {
2 return <div className="tabs">{children}</div>;
3}
4
5Tabs.List = function List({ children }: { children: React.ReactNode }) {
6 return <div className="tabs-list">{children}</div>;
7};
8
9Tabs.Panel = function Panel({ children }: { children: React.ReactNode }) {
10 return <div className="tabs-panel">{children}</div>;
11};
12
13// Usage
14<Tabs>
15 <Tabs.List>
16 <button>Overview</button>
17 <button>Details</button>
18 </Tabs.List>
19 <Tabs.Panel>Content here</Tabs.Panel>
20</Tabs>;2. Custom Hooks: Extract Everything
If you're not writing custom hooks, you're missing React's most powerful feature. Any stateful logic that could be reused should become a hook.
My Rules for Hooks
- One responsibility per hook
- Return tuples for simple state, objects for complex
- Handle loading and error states consistently
- Document the expected behavior
Hooks I Write for Every Project
- useLocalStorage: Persistent state that syncs across tabs
- useDebounce: Delay expensive operations
- useMediaQuery: Responsive logic in JavaScript
- useOnClickOutside: Close modals and dropdowns properly
Example: A simple reusable hook
1function useDebounce<T>(value: T, delay = 300) {
2 const [debouncedValue, setDebouncedValue] = React.useState(value);
3
4 React.useEffect(() => {
5 const timer = setTimeout(() => setDebouncedValue(value), delay);
6 return () => clearTimeout(timer);
7 }, [value, delay]);
8
9 return debouncedValue;
10}3. State Machines for Complex UI
When your component has more than three states, reach for a state machine. They make impossible states impossible and complex flows understandable.
A login form isn't just "loading" or "not loading". It's idle, validating, submitting, succeeded, or failed. Each state has specific allowed transitions. State machines encode this explicitly.
1type AuthState =
2 | { status: "idle" }
3 | { status: "submitting" }
4 | { status: "success" }
5 | { status: "error"; message: string };
6
7function authReducer(state: AuthState, action: { type: string }) {
8 switch (action.type) {
9 case "submit":
10 return { status: "submitting" };
11 case "success":
12 return { status: "success" };
13 case "error":
14 return { status: "error", message: "Login failed" };
15 default:
16 return state;
17 }
18}Even if you do not use a formal state machine library, the discipline of explicit states and transitions keeps your UI honest.
4. Render Props (Still Useful)
Hooks have replaced most render prop use cases, but the pattern still shines for components that need to share DOM-related state—like mouse position or scroll position.
The lesson here is not that older patterns are bad. The lesson is that the simplest abstraction that solves the problem is usually the right one.
5. Provider Pattern for Global State
Context API gets a bad rap for performance, but used correctly—with split providers and memoization—it's perfect for truly global state like themes, auth, and feature flags.
Example: Split read/write contexts
1const ThemeContext = React.createContext("light");
2const ThemeDispatchContext = React.createContext<(theme: string) => void>(() => {});
3
4function ThemeProvider({ children }: { children: React.ReactNode }) {
5 const [theme, setTheme] = React.useState("light");
6
7 return (
8 <ThemeContext.Provider value={theme}>
9 <ThemeDispatchContext.Provider value={setTheme}>
10 {children}
11 </ThemeDispatchContext.Provider>
12 </ThemeContext.Provider>
13 );
14}Splitting context this way avoids unnecessary re-renders for consumers that only need one side of the state.
6. Composition Over Configuration
One of the most important React ideas is that composition usually beats giant prop lists. If a component is getting difficult to configure, consider exposing smaller building blocks instead of more options.
When to Reach for Each Pattern
- Compound Components: when structure should be flexible
- Custom Hooks: when logic should be reusable
- State Machines: when state transitions get complicated
- Context Providers: when state must be shared globally
- Render Props: when you need a reusable stateful wrapper
The Meta-Pattern
The real pattern is knowing which pattern to use. Start simple. Add complexity only when the code demands it. The best pattern is the one that makes your specific problem disappear.
Senior React work is less about memorizing a long list of patterns and more about choosing the right trade-off for the current problem.
Continuous Learning
React patterns evolve. Hooks replaced class lifecycle methods. Server Components are changing how we think about data fetching. Stay curious, keep learning, and always question if there's a better way.
The best React code is not the most clever code. It is the code that is easy for the next developer to change without fear.

Prabhath Madhushan
Full Stack Developer | Software Engineer
A passionate developer building scalable web applications with modern technologies. Always learning, always creating.
