Crypto Trends

Every Popular React Pattern You Should Know About

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 (
    
    {users.map(user => (
  • {user.name}
  • ))}
); } // 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 ; }

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  setCount(c => c + 1)} {...props} />;
  };
}

// Using the HOC:
function ClickButton({ count, increment, label }) {
  return ;
}
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 component that tracks the cursor’s position. Traditionally, it might store x, y in state and render something like

Mouse at (x, y)

. But what if we want to reuse the mouse-tracking logic with different UI? The Render Props pattern suggests creating a 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, encapsulates the logic (tracking the mouse), but delegates rendering to the outside.

Example: let’s implement a utility component 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 
    return
      {filtered.map(item => children(item))}
    ; } // Usage: n % 2 === 0}> {item =>
  • {item}
  • }

Here, 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

  • 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 for any kind of list — numbers, users, products — just by changing the rendering function. Another example: a 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 instead of a component — a function that would render JSX based on route parameters. Formik offered a 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 => ( {bar => ( ... )} )} — 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 ;
    }
    

    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 with multiple s, or a

  • Back to top button