React Advanced Patterns

Learn some advanced React patterns and how to use them.




What is React?

React is an open-source JavaScript library designed for building user interfaces, particularly for single-page applications where smooth and efficient updates are crucial. Developed and maintained by Facebook, React employs a declarative syntax that enables developers to describe the desired UI state, and it automatically updates the user interface when the underlying data changes.

What it brings to the table:

  • Organises UI into reusable components.

  • Simplifies the process of building and updating UI.

  • Enhances performance by minimizing DOM manipulations.

  • Ensures a unidirectional flow of data for predictable behavior.


React Components

React components are the fundamental building blocks of a React application, encapsulating a self-contained piece of user interface and its corresponding functionality. These components can be thought of as custom, reusable HTML elements that developers compose to create complex and interactive UIs. Each React component is designed to manage its own state, allowing for a modular and maintainable code structure.


Advanced React

Keep in mind, when I say "advanced react", we are not going into a deep dive of some super complex patterns or concepts, basic knowledge of react is needed to follow along.

Firstly let's look at the benefits that we can achieve when using the react patterns that we will discuss in this article:

  1. Enhanced Code Organization and Maintainability:
  2. Employing advanced coding techniques in React results in improved code organization. These approaches encourage modularity and reusability, making it simpler to manage and maintain extensive codebases. Components structured using these techniques are often more specialized, fostering a modular and maintainable codebase. This modular structure facilitates debugging, testing, and collaboration among team members.
  3. Increased Reusability and Composition:
  4. React's advanced coding techniques empower developers to craft reusable components and logic. For instance, certain approaches encourage the composition of smaller, specialized components into more intricate ones. By following these practices, developers can create components that encapsulate specific functionality, facilitating easy reuse across different sections of an application. This reusability minimizes redundancy and promotes a consistent and standardized approach to feature implementation.
  5. Optimized Performance:
  6. Several advanced coding techniques in React, along with built-in optimization features, contribute to improved performance. These techniques assist in preventing unnecessary re-renders of components, ensuring updates occur only when essential. Through the use of optimization strategies like preventing unnecessary re-renders, developers can enhance rendering performance. This is particularly crucial for applications with complex user interfaces or large datasets.
  7. Effective State Management and Side Effects:
  8. React's advanced coding techniques, including custom approaches for state management, offer a robust means of handling state and side effects. These techniques enable the encapsulation of complex logic, promoting a clean and efficient approach to state management. By leveraging these techniques, developers can manage state in a more predictable and scalable manner, particularly in scenarios where state logic becomes intricate. Additionally, custom approaches aid in encapsulating side effects, simplifying the code's reasoning and testing.


Render Props

Render Props is a design pattern in React that involves passing a function as a prop to a component, allowing the component to delegate the rendering of its content to the passed function.

It maybe sounds complicated, but it really isn't... Let's see it in a example:

                  function ToggleOn({on, children}) {
  return on ? children : null
}

function ToggleOff({on, children}) {
  return on ? null : children
}

function ToggleButton({on, toggle, ...props}) {
  return <Switch on={on} onClick={toggle} {...props} />
}                 
                

Above we have a set of components to work with. We will be using the Toggle example across multiple patterns so make sure you understand what these components do.

Now let's say that we want to use these component in our react application. The naive way would be:

                    function App() {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  return (
    <>
        <ToggleOn on={on}>The button is on</ToggleOn>
        <ToggleOff on={on}>The button is off</ToggleOff>
        <ToggleButton on={on} toggle={toggle} />
        <SomeLargeComponent />
    </>
  )
}                   
                

There is one obvious problem in the "naive" approach. Can you spot it ?

The problem is that the state is "global" in a sense and is exposed to all of the components, including "SomeLargeComponent". If you know how react works under the hood, you know that a change of "on" state will trigger a re-render of all components in App, including the "SomeLargeComponent". We would want to avoid that.

Another thing that would be nice if we separated the logic so we have a "presentation" component and a "container" component.

We can achieve this using render props:

                function Toggle({ render }) {
  const [on, setOn] = React.useState(false);
  const toggle = () => setOn(!on);

  return render({ on, toggle });
}

function App() {
  return (
    <Toggle
      render={({ on, toggle }) => (
        <>
          <ToggleOn on={on}>The button is on</ToggleOn>
          <ToggleOff on={on}>The button is off</ToggleOff>
          <ToggleButton on={on} toggle={toggle} />
        </>
      )}
    />
    <SomeLargeComponent />
  );
}                       
              

We created a Toggle component that will be entirely responsible for handling state internally. We provide the Toggle component a render prop called "render" which is a function that accepts state (or whatever we want) that is passed down from the Toggle component where the state and logic is being held.

We can also use the children prop as a render prop. In that case the code would look like this:

                function Toggle({ children }) {
const [on, setOn] = React.useState(false);
const toggle = () => setOn(!on);

return children({ on, toggle });
}

function App() {
  return (
    <Toggle
      {({ on, toggle }) => (
        <>
          <ToggleOn on={on}>The button is on<ToggleOn/>
          <ToggleOff on={on}>The button is off<ToggleOff/>
          <ToggleButton on={on} toggle={toggle} />
        </>
      )}
    />
    <SomeLargeComponent />
  );
}                   
              

This way we saved "SomeLargeComponent" from re-rendering, and solved the problem of logic separation.

The issues that we tried to solve with render props, have largely been replaced by React Hooks. As Hooks changed the way we can add reusability and data sharing to components, they can replace the render props pattern in many cases. Besides this, I also wanted to go over render props, because it is still being used and you will probably use, or have used, this pattern before.



Compound Components

Compound Components in React is a design pattern where a set of components work together to form a cohesive unit, allowing the composition of complex UI structures while maintaining a clear and intuitive API. Unlike typical components that operate independently, compound components are designed to be used together as a group to achieve a specific functionality or appearance. This pattern enhances code organization, promotes reusability, and provides a more declarative and intuitive interface for developers.

It will be more clear with an example, let's just remind our self what does the "naive" solution to the toggle problem looks like:

                function App() {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  return (
    <>
        <ToggleOn on={on}>The button is on<ToggleOn/>
        <ToggleOff on={on}>The button is off<ToggleOff/>
        <ToggleButton on={on} toggle={toggle} />
        <SomeLargeComponent/>
    </>
  )
}               
              

Let's see what the Compound Components solution would look like:

                function Toggle({children}) {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  return React.Children.map(children, (child, index) => {
    return React.cloneElement(child, {
      on,
      toggle,
    })
  })
}
                    
function App() {
  return (
    <>
      <Toggle>
        <ToggleOn>The button is on<ToggleOn/>
        <ToggleOff>The button is off<ToggleOff/>
        <ToggleButton />
      <Toggle/>
      <SomeLargeComponent/>
    </>
  )
}                    
              

We can see that we have something similar to the render props solution. The main difference in the App component is that we don't have prop drilling anymore (keep in mind we are still using the same set of toggle components), which is awesome. The state is being shared implicitly within the new defined Toggle component.

The key part of this pattern is this:

                React.Children.map(children, (child, index) => {
  return React.cloneElement(child, {
    on,
    toggle,
  })
})              
              

Here we are taking the children provided to the component, going through each child, cloning the child and finally the last parameter provided to the "cloneElement" function manually passing "props" to the new react element. This way all of the children get the props needed and they are provided from the Toggle component which again has all the logic and state inside it.

This looks cool, but it does have flaws. For example, if we wanted to have something like this:

                function App() {

  return (
    <>
      <Toggle>
        <ToggleOn>The button is on<ToggleOn/>
        <ToggleOff>The button is off<ToggleOff/>
        <span>Hello<span/>
        <ToggleButton />
      </>
      <SomeLargeComponent/>
    </>
  )
}               
              

We would get an error indicating that we can't clone a "span" element. Solution?

                function Toggle({children}) {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  return React.Children.map(children, (child, index) => {
    if (typeof child.type === 'string') return child
    return React.cloneElement(child, {
      on,
      toggle,
    })
  })
}                  
              

Adding a simple condition would solve this problem, because the type of a custom made component is always an object. By checking if the type is a string, we can just return the element as is and not clone it.

But what if we want something like this:

                function App() {

  return (
  <>
    <Toggle>
      <ToggleOn>The button is on<ToggleOn/>
      <ToggleOff>The button is off<ToggleOff/>
      <div>
        <span>Hello<span/>
      </div>
      <ToggleButton />
    </>
    <SomeLargeComponent />
  </>
  )
}                    
              

Unfortunately, using the previous solution will not work for this use case, and it is not that easy to make this code work using the previous solution. If we really wanted to, we could make some sort of recursive algorithm that would traverse the children, but that is definitely an overkill. What would be a better option is...

Flexible Compound Components

It is basically still the same pattern as compound components, the main difference is that the parent state is not being provided directly to children components, but rather via context (link do https://legacy.reactjs.org/docs/context). This provides a more dynamic and scalable approach to managing shared state within a component hierarchy.

There is quite a bit of refactoring that needs to be done. Let's start by creating our context:

                const ToggleContext = React.createContext()
              

Now let's create a hook to use our new context:

                function useToggle() {
  return React.useContext(ToggleContext)
}
              

In our Toggle component we need to wrap the children with a context provider. We pass the local state To the provider. When we do this, in simple terms, the provided state values get stored in the context instance. This enables that the children of the Toggle component have access to the values that were provided.

                function Toggle({children}) {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)

  return (
    <ToggleContext.Provider value={{on, toggle}}>
      {children}
    <ToggleContext.Provider/>
  )
}
              

Now we need to change our toggle components. They will no longer be accepting the needed state in props, but rather use the hook useToggle that we created to access the necessary state.

                function ToggleOn({children}) {
  const {on} = useToggle()
  return on ? children : null
}

function ToggleOff({children}) {
  const {on} = useToggle()
  return on ? null : children
}

function ToggleButton({...props}) {
  const {on, toggle} = useToggle()
  return <Switch on={on} onClick={toggle} {...props} />
}
              

Finally we can have as many nesting elements as we would like, and this code would work as expected.

                function App() {

  return (
    <>
      <Toggle>
        <ToggleOn>The button is on<ToggleOn/>
        <ToggleOff>The button is off<ToggleOff/>
        <div>
          <span>Hello<span/>
        </div>
      <ToggleButton />
      </>
      <SomeLargeComponent/>
    </>
  )
}     
              

HOCs (High Order Components)

This pattern enables component composition and code reuse by wrapping one or more components with a function. This function takes a component as an input and returns a new component with additional props, state, or behavior. HOCs are often used for cross-cutting concerns, such as authentication, logging, or data fetching, allowing us to encapsulate and share functionality across different parts of the application.

Let's say, that we always wanted to add a certain styling to multiple components in our application. Instead of creating a style object locally each time, we can simply create a HOC that adds the style objects to the component that we pass to it:

                function withStyles(Component) {
  return props => {
    const style = { padding: '0.2rem', margin: '1rem' }
    return <Component style={style} {...props} />
  }
}

const Button = () = <button>Click me!<button/>
const Text = () => <p>Hello World!<p/>

const StyledButton = withStyles(Button)
const StyledText = withStyles(Text)
              

As we se we created a function withStyles, that is in fact a HOC. Key word "with" is always used for HOCs. The HOC withStyles is accepting a component and returning an anonymous component (component that does not have a name). This enables us to have different side effects, props and behavior on the component that was wrapped with the HOC.

Let's see a "bigger" example, just to get a "feel" for what can this HOC pattern be used for:

                function withLoader(Element, url) {
  return (props) => {
    const [data, setData] = useState(null);
    
    useEffect(() => {
      async function getData() {
        const res = await fetch(url);
        const data = await res.json();
        setData(data);
      }

      getData();
    }, []);
    
    if (!data) {
      return <div>Loading...<div/>;
    }
    
    return <Element {...props} data={data} />;
  };
}
              

As we can see, we have a HOC withLoader. This HOC can arguably be called withFetch, but that is besides the point. The HOC accepts two arguments, the component and a URL. Inside the HOC we return a component in which we fetch data using the provided URL. We handle the loading condition inside it as well, and finally return the provided component with props and the newly fetched data.

It is very simple to use, which we may see in the next code snippet:

                function DogImages(props) {
  return props.data.message.map((dog, index) => (
    <img src={dog} alt="Dog" key={index} />
  ));
}

export default withLoader(
  DogImages,
  "https://dog.ceo/api/breed/labrador/images/random/6"
);

              

We can see in the above example that inside the DogImages wrapped component, we have the data fetched so we can list out the dog images.

There are other react patterns out there like: Controlled Components, Container and Presentational Components, State Reducer Pattern, Control Props Pattern and many more. Every pattern has its own use cases. There is no perfect pattern. These patterns that I covered in more detail are just some of the patterns, which i have personally found to be the most common in my career, and it's always nice to know your options and limitations when writing code.

Author: