In the world of React, the term "pattern" refers to a proven approach to solving a task, rather than a classical design pattern from the well-known book. Over the years, the React ecosystem has developed its own popular patterns—ways of organizing components and logic to make the code clear, maintainable, and reusable.
In this article, I’ll walk you through the most popular patterns, provide code examples, explore when each one can be useful, and highlight their pros and cons. We’ll talk about classic techniques like container components and HOCs, the evolution towards hooks, and take a look at the new patterns introduced in the latest versions of React.
1. Container & Presentational Components
Back when I was working with Vue at my first job, we introduced this pattern thanks to our team lead. Broadly speaking, we had so-called “smart” components and “dumb” components (or as I politely called them during demos — visual components). As you’ve probably guessed, the role of Container was played by the “smart” components, and Presentational — by the “dumb” ones. So, what’s the essence of this pattern? Ever heard the phrase “divide and conquer”? The Container & Presentational Components pattern is exactly about that: it separates logic (data and how it’s handled) from presentation (UI) into different components.
Presentational Components are responsible only for how something looks. They receive data via props
and render it — that’s it. Typically, they are pure functional components, often stateless (except for minor UI state like “is the dropdown open?”). They don’t care how the list of users was fetched — they simply expect something like props.users
and render it according to the design.
Container Components, on the other hand, know what to render and where to get it from, but not how it should look. They contain all the logic: they might fetch data, subscribe to a store or context, manage state, and render the presentational components, passing the processed data to them. A container might not have any of its own HTML at all, except what comes from the child presentational component. Its job is handling the data side of things.
Why is this approach useful? First, it improves separation of concerns (UI is separate from data), making the application easier to understand and maintain. Second, it enhances reusability: a single visual component can be reused with different data sources via different containers. Designers can tweak a component’s appearance in one place without touching the business logic. It also makes testing easier: you can test the container’s logic separately (without markup) and test the presentational component separately (with mocked data).
// Example: Presentational component for rendering a list of users
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// Container component that fetches and provides user data
function UserListContainer() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data));
}, []);
return <UserList users={users} />;
}
In this example, UserList
holds no state, doesn’t subscribe to a store or context, and simply renders a list. It doesn’t care how or where the user data is fetched — it just receives the users
prop and displays it. The container UserListContainer
, on the other hand, handles the data logic: it performs a fetch
, stores the result in useState
, and renders UserList
, passing the data down via props. Thanks to this separation, the UserList
component becomes easily reusable — whether you’re dealing with local data, Redux, or context — you just need to create a different container.
Of course, you don’t always need to split components into a container/presentation pair. This pattern is most useful as your app grows — when you start noticing props being passed down through multiple levels just as transit data, or when a component becomes too bloated with logic. That’s when you “extract” the logic into a container and keep the UI in a presentational component — and your code immediately becomes cleaner. It’s not a strict rule, but a helpful technique for refactoring when necessary.
It’s worth noting that with the advent of React Hooks, the boundary between logic and presentation has blurred somewhat. Now, you can extract logic into custom hooks and call them directly inside a component, rather than having to create a separate container class as was common before 2018. Still, the principle of “keeping logic separate from presentation” remains valuable. Even with hooks, you can structure code by separating concerns — for example, writing a useUsersData()
hook to fetch users and using it across multiple components instead of duplicating fetch logic.
Pros: clear separation of concerns, ability to reuse and swap parts independently (the UI component can be reused with different data sources), easier testing.
Cons: results in more files/components than might otherwise be necessary, which can feel excessive for small use cases. Sometimes, overly splitting components into “dumb” and “smart” can actually complicate the structure if the pattern is applied inappropriately. As the saying goes — use your head: not every button needs its own container.
2. Higher-Order Component (HOC)
When I first heard the term HOC, it sounded like something from mathematics. But in practice, it’s much more down-to-earth: an HOC is simply a function that takes a React component and returns a new component, wrapping the original one with additional functionality. In simpler terms, an HOC is a “wrapper.” We place one component inside another to get an enhanced version of the component passed into the HOC.
Why might you need this? Imagine you have several different components, and they all require the same functionality — like error handling or subscribing to external data. You could duplicate the logic in each component, but it’s far cleaner to write an HOC once and apply it wherever needed. A classic example is Redux’s connect
function: you write export default connect(mapState)(MyComponent)
, and your component receives props from the global state. connect
is an HOC that injects Redux data into the component without forcing you to rewrite it to be Redux-aware.
Creating your own HOC is also quite straightforward. Here’s a super simple example of an HOC that adds counter state to a component:
function withCounter(WrappedComponent) {
// Return a new wrapper component
return function WithCounter(props) {
const [count, setCount] = useState(0);
// Pass counter and increment function to the wrapped component
return <WrappedComponent count={count} increment={() => setCount(c => c + 1)} {...props} />;
};
}
// Using the HOC:
function ClickButton({ count, increment, label }) {
return <button onClick={increment}>{label}: {count}</button>;
}
const EnhancedButton = withCounter(ClickButton);
Here, withCounter
is an HOC — it returns a new functional component WithCounter
, which uses useState
internally and passes the state and increment function down to the WrappedComponent
. As a result, EnhancedButton
is an enhanced version of ClickButton
that can count clicks, even though the original ClickButton
had no idea about this behavior.
Pros: a single HOC can add functionality to many components at once — no need to duplicate logic. You can update the logic in one place (inside the HOC), and all wrapped components will receive the changes. HOCs can also be composed: for example, you could wrap a component with an HOC that adds theming, then another that adds logging, and so on. The final component ends up with multiple added capabilities.
Cons: this kind of magic comes at the cost of structural complexity. When many wrappers are involved, the React tree bloats and you get the “matryoshka doll” effect. In DevTools, you might see something like: Connect(withRouter(WithTheme(MyComponent)))
— and it becomes harder to tell what’s going on. Debugging these chains is no picnic either, as you often have to wade through multiple levels of abstraction. Also, HOCs typically pass props to the wrapped component, which can lead to naming conflicts (e.g., a prop.title
from the HOC might overwrite a title
prop you manually passed in). Another nuance — HOCs complicate typing in TypeScript (you need to define generics correctly for props), though that’s outside the scope of this article.
Over time, React developers have cooled a bit on HOCs. The official documentation even says: "Higher-order components are not commonly used in modern React code." In part, they’ve been replaced by hooks (which we’ll cover later). Still, HOCs haven’t disappeared: many third-party libraries — like Redux, Relay, and others — still offer HOC-based APIs. And in older projects, you’ll almost certainly encounter a few. So it’s still worth understanding this pattern. Just keep modern alternatives in mind, and use HOCs where they truly make sense.
3. Render Props Pattern
This next pattern is what I’d call an inverted HOC. Render Props is an approach where a component doesn’t render anything of its own, but instead takes a function (often via a render
prop or by using its children as a function) and calls it to determine what to render. In other words, we pass the component an instruction for what to render, and it decides when and with what data to invoke that instruction.
Imagine a <MouseTracker>
component that tracks the cursor’s position. Traditionally, it might store x, y
in state and render something like <p>Mouse at (x, y)</p>
. But what if we want to reuse the mouse-tracking logic with different UI? The Render Props pattern suggests creating a <Mouse>
component that doesn’t define its own JSX rigidly, but instead invokes a function passed via a prop (or as children
) and provides the coordinates to it. That function then decides what to render. This way, <Mouse>
encapsulates the logic (tracking the mouse), but delegates rendering to the outside.
Example: let’s implement a utility component <FilteredList items={...} filter={...}>
that displays a list based on the provided filter. Instead of hardcoding the list item markup, we’ll use a render prop via children
:
function FilteredList({ items, filter, children }) {
const filtered = items.filter(filter);
// Call the child function for each item, wrapping in <ul>
return <ul>{filtered.map(item => children(item))}</ul>;
}
// Usage:
<FilteredList items={[1,2,3,4,5]} filter={n => n % 2 === 0}>
{item => <li key={item}>{item}</li>}
</FilteredList>
Here, <FilteredList>
knows how to filter the array (items.filter(filter)
), but it does not know how to render each item. Instead, it calls the function we passed as a child (children
) for every item in the list. That function returns a <li>
for each item
. As a result, the filtering logic is encapsulated within FilteredList
, while the specific rendering of the list is defined externally. We could just as easily use this component for an array of objects — for example, rendering products — by simply passing a different child function.
The Render Props pattern greatly increases the flexibility of components. We can reuse <FilteredList>
for any kind of list — numbers, users, products — just by changing the rendering function. Another example: a <Mouse>
component could provide cursor coordinates, while the external code decides whether to display text, draw an image at the coordinates, or do something entirely different — no need to create multiple variations of the component for each use case.
Pros: Render Props allow a provider component (like FilteredList
in the example above) to be highly generic, while delegating the actual markup to the outside. Many libraries have adopted this pattern: for example, React Router (before version 6) let you pass a render
prop to <Route>
instead of a component — a function that would render JSX based on route parameters. Formik offered a <Formik>
component with a function-as-child to render the form. Downshift (an autocomplete library) is another classic example of the render props pattern.
Cons: the main downside is the extra noise in JSX. Code with nested functions can be hard to read. In our simple example, everything is tidy, but imagine you have multiple layers of such components: <Foo>{foo => ( <Bar>{bar => ( ... )}</Bar> )}</Foo>
— it’s easy to end up in “wrapper hell” with arrow functions embedded right in the markup. This also makes debugging more difficult when something breaks. Additionally, a new function is created on every render, which could impact performance if there are many such components (React does optimize function props via shallow comparison, but still). There’s also an implicit contract: the external code has to know what arguments the function receives. TypeScript helps, of course, but it’s not immediately obvious from just reading the code that children
, for example, is actually a function.
Like HOCs, the Render Props pattern is now used less frequently. Many use cases it solved are handled more elegantly with hooks, as the official documentation also notes. Still, it’s important to understand this pattern, as legacy code and some libraries still use it. If you see a component that takes a function as a prop (usually called render
or passed via children), now you know — it’s Render Props.
4. Hooks and Custom Hooks
We’ve already mentioned hooks a few times — now it’s time to talk about them as a pattern in their own right, one that has essentially replaced many of the earlier ones. Hooks were introduced in React 16.8 and instantly changed how components are written. Instead of classes with lifecycle methods, we now have functional components that use state (useState
), effects (useEffect
), and other features directly inside the function. Most importantly, we can write our own custom hooks using React’s built-in ones to reuse logic.
Why are hooks so popular? They allow you to reuse stateful logic and side effects without changing the structure of the components that use them. Previously, to share logic between two components, you had to use HOCs or Render Props — that is, add an extra wrapper component or callback function. Now, we can extract that logic into a custom hook like useSomething()
and call it from the components (or even other custom hooks) that need it. Hooks enable a more straightforward, readable coding style: instead of HOC magic happening behind the scenes, we explicitly call the hooks we need and get the data directly.
A custom hook is just a function whose name starts with use
(by convention, so React’s linter knows that it may contain hooks). For example, let’s rewrite our earlier withCounter
HOC as a custom hook:
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
return { count, increment };
}
// Using the hook in a component:
function ClickButton({ label }) {
const { count, increment } = useCounter();
return <button onClick={increment}>{label}: {count}</button>;
}
We get the same behavior (a click counter), but without any wrappers. The ClickButton
component simply calls useCounter()
and receives count
and increment
. Inside useCounter
, there could be any complex logic, side effects, or other hooks — the component using our hook doesn’t know and doesn’t need to know. The component code remains crystal clear: it just grabs what it needs from the hook and uses it.
The main advantage of custom hooks is logic reuse. For example, you can create a useFetch(url)
hook (a very common pattern) that handles calling an API and returns loading state, response data, and any error. You can then reuse this hook across different components and pages without duplicating the request logic.
Hooks also work really well together. You can call one hook inside another — for example, use useContext
or useReducer
inside your own useAuth
hook. This makes writing complex behavior easier, since you can build larger functionality from smaller, focused hooks like building blocks.
Pros: clarity and conciseness. A component using a hook isn’t wrapped in extra layers — its JSX stays clean and uncluttered by helper components or callback functions. Hooks replaced HOCs and Render Props for many use cases because they solve the same problems in a more natural, JavaScript-friendly way. Custom hooks are easy to test (they’re just functions). Hooks allow you to separate logic within a component into independent parts: for instance, a component can use both local useState
and several custom hooks at the same time — each handling its own concern. This modularity is much harder to achieve with multiple HOCs or render props.
Cons: if we can call it that — while hooks simplify many things, they come with their own set of rules that must be followed (they must be called in the same order, only inside components or other hooks, not inside conditions). Violating these rules results in warnings or errors from React. Another potential downside is that reused hooks create isolated state for each component. Usually that’s what we want, but if we need to share a single piece of state across components, hooks won’t help directly — the state will need to be lifted up (or moved into context or a store). But that’s a different concern altogether.
Overall, hooks are the primary tool used by React developers today. Most new APIs, frameworks, and libraries are built around them. So, if you come across a library written using HOCs or Render Props, chances are it already has — or will soon get — a hook-based version. Hooks made React component code more understandable and essentially removed the need for classes (almost all new React features are designed for functional components only).
5. Compound Components
The patterns we’ve covered so far focus on how components share logic or data. But there’s also an approach centered around composition, which enables building flexible UIs. Compound Components is a pattern where multiple components work together as a single unit, sharing internal state (usually via context). A user of such a "bundle" of components can freely combine its parts in JSX.
I first came across this pattern when building my own UI Kit as a side project. Think of an <Accordion>
with multiple <AccordionItem>
s, or a <Select>
with several <Option>
s. Compound Components allow us to write code where <Accordion>
doesn’t have to receive an array of items via props and render them internally — instead, we let the developer explicitly write out the items and their contents in JSX, using pre-defined subcomponents like <Accordion.Item>
, <Accordion.Header>
, and <Accordion.Panel>
.
You might be wondering, as I did at first: "How does the Accordion
know about its Item
s and organize them?" Internally — it’s done via React Context. Compound Components are usually implemented like this: the parent (container component) holds all the state (e.g. which item is expanded) and control methods (like toggle(index)
). It wraps its children
in a context provider and passes down that state and those functions. The child components (which are rendered somewhere inside children
) access this context and thus get access to the parent’s state. They "know" which container they belong to because they’re rendered inside it and receive its context.
Let’s look at a concrete example. We’ll create a simple compound component called Toggle
, which controls showing/hiding content on click. We’ll have a <Toggle>
container and its children <Toggle.On>
, <Toggle.Off>
, and <Toggle.Button>
.
const ToggleContext = createContext();
function Toggle({ children }) {
const [on, setOn] = useState(false);
const toggle = () => setOn(prev => !prev);
return (
<ToggleContext.Provider value={{ on, toggle }}>
{children}
</ToggleContext.Provider>
);
}
function ToggleOn({ children }) {
const { on } = useContext(ToggleContext);
return on ? <>{children}</> : null;
}
function ToggleOff({ children }) {
const { on } = useContext(ToggleContext);
return on ? null : <>{children}</>;
}
function ToggleButton({ children }) {
const { toggle } = useContext(ToggleContext);
return <button onClick={toggle}>{children}</button>;
}
// Usage:
<Toggle>
<ToggleOn>Now we see the hidden text</ToggleOn>
<ToggleOff>The text is hidden</ToggleOff>
<ToggleButton>Toggle</ToggleButton>
</Toggle>
Here, <Toggle>
manages the on
state (shown/hidden) and provides the value { on, toggle }
via context to all of its descendants. <ToggleOn>
and <ToggleOff>
read the on
state and conditionally render their children
. <ToggleButton>
accesses the toggle
function from context and calls it when clicked. The result is a clean and declarative interface: we place various UI parts inside <Toggle>
, and those components know when to display themselves and how to behave — we just describe the structure, without wiring props manually.
Note that <Toggle>
doesn’t care how many <ToggleOn>
or <ToggleOff>
components are inside, or what JSX is rendered in them. Everything relies solely on context state. That’s the power of composition: the user is given “building blocks” (separate components) to create whatever structure they need — rather than a single monolithic component with a dozen config props.
Pros: tremendous flexibility and expressiveness. A well-designed compound component feels like a tiny framework. For example, the @reach/ui library (a precursor to radix-ui) built many of its components this way — dialogs, menus, lists — all implemented via context and nested components. The API is intuitive for the user — just nest one component inside another. It also allows fine-tuning the resulting markup — you can wrap <ToggleButton>
in a <div>
with a class, for instance. It’s easier to maintain visual consistency too, since all pieces are coordinated by a single context, rather than spreading logic across unrelated components.
Cons: harder to implement correctly. You have to carefully design how the components interact, account for cases where some parts may be missing or repeated. If you misdesign the compound structure, you might run into bugs — for example, using <ToggleButton>
outside of <Toggle>
(i.e., outside its provider) would make useContext
return undefined
, causing an error. You either need to avoid this or add explicit checks and clear error messages (e.g., “ToggleButton must be used within a Toggle”).
Another concern is performance: when the context value changes, all context consumers re-render. In our example, on every toggle
call, <ToggleOn>
, <ToggleOff>
, and <ToggleButton>
will all re-render. That’s no big deal here, but if you had a dozen complex child components subscribing to context and updating frequently, you'd want to optimize — for instance, by splitting context or memoizing.
That said, the benefits usually outweigh the drawbacks. The Compound Components pattern helps you create intuitive and flexible APIs for your components. It embodies React’s philosophy: composition over inheritance. Instead of building one large component with tons of conditionals, you build a set of simple components that combine into complex behavior.
Compound Components are a fairly “advanced” pattern. In smaller apps, you might not need to implement it. But if you’re building a component library — as I did in my side project — or working on a complex widget, this pattern quickly becomes essential. Nearly all advanced React UI libraries (Material UI, Chakra, Radix, etc.) use context and composition under the hood to power their more complex components.
6. Server Components and Suspense: Modern React Capabilities
Finally, let’s talk about some of the newest features introduced in React 18 and React 19. These advancements focus on improving how we handle asynchrony, data, and server-side rendering. Most notably, we have React Suspense and Server Components. These concepts are still evolving within the developer community, but they’re worth understanding.
Suspense – Waiting Comfortably
Whenever a UI needs to fetch data, we’re faced with the task of showing a loading indicator until everything is ready. Previously, this involved writing manual logic — typically an isLoading
state and a conditional render of either a spinner or the content. With the introduction of React Suspense, the React team proposed a more declarative approach. Suspense is a special component that lets us pause the rendering of its child components until they’re ready, showing fallback UI in the meantime.
In simple terms, we wrap part of the component tree with <Suspense fallback={<Loader/>}> ... </Suspense>
, and if there’s a delay inside that area (such as code-splitting or data loading), React will automatically show the <Loader>
instead of the content. Once everything is ready, it will render the actual components. Suspense handles the coordination, freeing us from manually managing loading states.
Today, Suspense is widely used for lazy-loading components via React.lazy
and <Suspense>
. For example:
const Comments = React.lazy(() => import('./Comments'));
function ArticlePage() {
return (
<div>
{/* ... article content ... */}
<Suspense fallback={<div>Loading comments...</div>}>
<Comments postId={666} />
</Suspense>
</div>
);
}
In this example, the Comments
component will be loaded on demand (in a separate bundle). While the bundle is loading, the user sees a placeholder text: “Loading comments…”. Once the code has been fetched, React renders <Comments>
. All of this happens without any special loading logic inside ArticlePage
— Suspense takes care of it. Under the hood, React.lazy
throws a Promise during loading, and <Suspense>
catches it and displays the fallback UI.
Beyond lazy-loading code, Suspense is gradually being adopted for asynchronous data as well. In React 18, an experimental API was introduced that enables Suspense to work with data. For example, you can use a special use()
function to await a Promise directly inside a component (it’s not stable yet, but frameworks like Next.js 13 are already using it extensively). The idea is the same: a component that fetches data can choose to “suspend” — pause its execution until the data arrives. React detects this and shows the fallback, and once the data is available, it continues rendering. This allows us to write components that look synchronous on the surface, even though they contain asynchronous code — and thanks to Suspense, users don’t experience any incomplete or janky UI during the wait.
To be fair, using Suspense for data is still very much experimental. If you’re building a regular app using Vite or — heaven forbid — CRA, and not using a framework like Next.js, you can’t currently use Suspense for data loading out of the box. You’d need a third-party library (React Query, for example, doesn’t yet support Suspense by default, but plans to), or a compatible framework. That said, the direction is clear: React is moving toward a more declarative approach to async handling. Right now, you can already use Suspense for spinners and placeholders during code loading, and in the near future, the same approach will likely become standard for data as well.
To sum up Suspense: this pattern lets you elegantly organize your loading state logic. Instead of writing lots of conditional isLoading ? <Spinner> : <Content>
checks, we simply declare: “This part of the UI might be delayed — show this in the meantime.” It improves UX (users see skeletons or loaders instead of flashing unfinished content) and makes your code simpler. Be sure to follow Suspense’s development — it’s likely to become much more widely used soon.
Server Components – React Moves to the Server
Another revolutionary idea from the React team is React Server Components (RSC). This concept aims to combine the best of server-side rendering and client-side SPAs. The idea is simple: if part of your React components can run exclusively on the server — generating ready-to-ship HTML — then let them run there, without ever being sent to the client. These components never get included in the JS bundle and don’t include any interactivity — they’re purely for rendering content. The other part of the app remains client-side — the familiar React components that can handle events, maintain state, and so on. This separation is explicit: React distinguishes between server and client components.
So how does React know where to run a given component? There’s a new directive: "use client"
. If a component file starts with this string, it’s treated as a client component — it will be included in the JS bundle and run in the browser. If the directive is omitted, the component is considered a server component and is executed on the server (e.g., during page rendering with Node.js). Server components can contain asynchronous code — such as database queries or file system access — because they run in the server environment. However, they cannot use useState
, useEffect
, or other client-only hooks, since there’s no persistent state across requests, nor access to the DOM.
React 18 (and even more so React 19) allows frameworks to take full advantage of this feature. For example, in Next.js 13 and newer, with the new app/
router, components are server components by default unless you explicitly specify "use client"
. This means that most of the page can be rendered on the server, delivering fully prepared HTML to the client, while interactivity is handled by selectively using client components.
Benefits of Server Components: First and foremost — performance. Server components skip hydration — the client doesn’t need to re-run JavaScript to restore UI state. You get the benefits of SSR (fast first render, minimal client work) without the usual downsides of SSR (like the need to hydrate large HTML blocks). Secondly, security — sensitive logic stays on the server and never ends up in the bundle. You can fetch data directly on the server (e.g., from a database) without exposing API keys to the browser. Third, bundle size is significantly reduced, since the client never receives the code of server components — only the final HTML markup and necessary JS for the remaining client components.
What does this look like in practice? The clearest example: a blog. The post page can be implemented entirely as a server component — the post is fetched from the database and rendered to HTML on the server. The “like” button or comment form, however, are interactive — those are client components. As a result, when the user opens the page, they get a fully rendered article immediately (no loading spinner). JavaScript is loaded only for the interactive elements — the like button and comment form — and only those parts are hydrated and become interactive on the client. It’s SSR and SPA combined — orchestrated by React.
React enforces strict rules on how server and client components can interact. A server component can import and render other server or client components. But a client component cannot import a server component. In other words, the tree can look like: Server → Client → Client → ... etc. But not the other way around. In the blog example above, the server component for the page can render a <LikeButton />
(client component) inside itself. But if you tried to do import PostDetails from './PostDetails.server.jsx'
inside a client component, the build would fail and warn you this isn’t allowed. This enforces a clear architectural boundary: top-level components are server-rendered, interactive “leaves” are client-side.
Currently, Server Components are only available through frameworks. In a vanilla React app, using RSC manually would be extremely difficult. But if you're using Next.js, Remix, or working on fullstack React apps in general, then RSC is already available. In React 18, it’s more of an experimental feature for enthusiasts. Libraries are beginning to adapt — React Router v7 plans to support RSC, and Vite is also experimenting with it.
What do Server Components offer in the long run? Potentially, a major leap in performance and fullstack development ergonomics. It introduces a new design dimension to React: separating components by runtime — which ones should render on the server, and which on the client. This adds to the existing design questions (logic vs. UI, reusable vs. app-specific code), by introducing a new layer: where should this code execute — on the server or in the browser? Using RSC effectively can greatly boost app speed with minimal effort — React will handle what to load, when, and how to sync state across environments.
That said, there’s added mental complexity. You need to clearly understand the constraints (e.g., you can’t use useEffect
in a server component, or rely on persistent state across requests). But this is manageable — with experience and solid documentation, it becomes natural.
Conclusion
We’ve explored the key patterns in React — and even looked ahead at the future of React architecture. Container & Presentational Components bring structure by separating logic from presentation. HOCs and Render Props are older techniques for code reuse that have largely been replaced by modern hooks, but they still appear in many projects. Compound Components showcase the power of composition, providing an API for assembling flexible UIs from small parts. And Suspense and Server Components represent the near future — making asynchronous operations and rendering more efficient and declarative.
It’s important to remember that patterns aren’t unbreakable dogmas. Each case should be approached thoughtfully. Sometimes, it’s better to skip the pattern entirely than over-engineer your architecture for the sake of a “clean” solution. Avoid unnecessary complexity. That said, knowing these approaches expands your toolbox. When you face a problem, you’ll often find yourself thinking: “Ah, this is a good case for that pattern.” A seasoned developer sees multiple implementation options — and chooses the optimal one.
My personal advice: don’t just read about patterns — try them out. Write your own HOC, refactor a component from Render Props to a hook, implement a small set of Compound Components — you’ll feel their strengths and limitations firsthand. React keeps evolving, and new techniques keep emerging, but the foundational ideas — composition, separation of concerns, and explicit state management — remain. Master these tools, and your React apps will reward you with cleaner, more maintainable code!