DEV Community

Cover image for React Design Patterns
Necati Özmen for Refine

Posted on • Originally published at refine.dev

React Design Patterns


refine repo

Author: Peter Osah

Introduction:

React developers can save time and effort by using design patterns, which provide a quick approach to addressing problems using tested-and-trusted solutions. They enable cohesive modules with lower coupling, which in turn helps React developers create maintainable, scalable, and efficient applications. In this article, we will explore React design patterns and examine how they might improve the development of React applications.

Container and presentation patterns

The Container and presentation pattern is a pattern that aims to separate the presentation logic from the business logic in a react code, thereby making it modular, testable, and one that follows the separations of concern principle.
Mostly in react applications, there arise cases where we are required to fetch data from a backend/store or to compute a logic and represent the resultant of that computation on a react component. In these cases, the container and presentation pattern shines as it can be used to categorize the components into two namely:

  • The container component, which acts as the component responsible for the data fetching or computation.
  • the presentation component, whose job is to render the fetched data or computed value on the UI(user interface).

An example of Container and presentation pattern is shown below:

import React, { useEffect } from 'react';
import CharacterList from './CharacterList';

const StarWarsCharactersContainer:React.FC = () => {
    const [characters, setCharacters] = useState<Character>([])
    const [isLoading, setIsLoading] = useState<boolean>(false);
    const [error, setError] = useState<boolean>(false);

    const getCharacters = async () => {
        setIsLoading(true);
        try {
            const response = await fetch("https://akabab.github.io/starwars-api/api/all.json");
            const data = await response.json();
            setIsLoading(false);
            if (!data) return;
            setCharacters(data);
        } catch(err) {
            setError(true);
        } finally {
            setIsLoading(true);
        }
    };

    useEffect(() => {
        getCharacters();
    }, []);

    return <CharacterList loading={loading} error={error} characters={characters} />;
};

export default StarWarsCharactersContainer;
Enter fullscreen mode Exit fullscreen mode
// the component is responsible for displaying the characters

import React from 'react';
import { Character } from './types';

interface CharacterListProps {
    loading: boolean;
    error: boolean;
    users: Character[];
}

const CharacterList: React.FC<CharacterListProps> = ({ loading, error, characters }) => {

    if (loading && !error) return <div>Loading...</div>;
    if (!loading && error) return <div>error occured.unable to load characters</div>;
    if (!characters) return null;

    return (
        <ul>
            {characters.map((user) => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    );
};

export default CharacterList;
Enter fullscreen mode Exit fullscreen mode

Component composition with Hooks

Hooks are a brand-new feature that debuted in React 16.8. Since then, they have played a crucial role in developing react applications. Hooks are basic functions that grant functional components access to state and lifecycle methods (which were previously exclusively available to class components). Hooks, on the other hand, can be specifically designed to meet a component requirement and have additional use cases.

We can now isolate all stateful logic—a type of logic that needs reactive state variable(s)—and compose or use it in the components using custom hooks. As a result, the code is more modularized and testable because the hooks are loosely tied to the component and can therefore be tested separately.

An example of component composition with hooks is shown below:

// creating a custom hook that fetches star wars characters

export const useFetchStarWarsCharacters = () => {

    const [characters, setCharacters] = useState<Character>([])
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(false);
    const controller = new AbortController()

    const getCharacters = async () => {
        setIsLoading(true);
        try {
            const response = await fetch(
                "https://akabab.github.io/starwars-api/api/all.json", 
                {
                    method: "GET", 
                    credentials: "include",
                    mode: "cors",
                    headers: {
                        'Content-Type': 'application/json',
                        'Access-Control-Allow-Origin': '*'
                    },
                    signal: controller.signal
                }
            );
            const data = await response.json();
            setIsLoading(false);
            if (!data) return;
            setCharacters(data);
        } catch(err) {
            setError(true);
        } finally {
            setIsLoading(true);
        }
    };

    useEffect(() => {
        getCharacters();
        return () => {
            controller.abort();
        }
    }, []);

    return [
        characters,
        isLoading,
        error
    ];
};
Enter fullscreen mode Exit fullscreen mode

After creating the custom hook, we will import it into our StarWarsCharactersContainer component and use it;

// importing the custom hook to a component and fetch the characters 

import React from 'react';
import { Character } from './types';
import { useFetchStarWarsCharacters } from './useFetchStarWarsCharacters';

const StarWarsCharactersContainer:React.FC = () => {

    const [ characters, isLoading, error ] = useFetchStarWarsCharacters();

    return <CharacterList loading={loading} error={error} characters={characters} />;
};

export default StarWarsCharactersContainer;

Enter fullscreen mode Exit fullscreen mode


State management with Reducers

Most often, handling many states in a component leads to issues with many ungrouped states, which can be burdensome and challenging to handle. The reducer pattern can be a helpful option in this situation. We can categorize states using reducers into certain actions that, when executed, can change the grouped states.

This pattern allows the developer who uses it to control the component's and/or hook's state management, letting them manage state changes when events are sent.

An example of using the reducer pattern is shown below:

Image description

In the above code, the component dispatches two actions:

  • The 'login' action type triggers a state change that affects three state values namely loggedIn, user, token.
  • The 'logout' action simply resets the state to its initial value.

Data management with Providers

The provider pattern is very useful for data management as it utilizes the context API to pass data through the application's component tree. This pattern is an effective solution to prop drilling, which has been a common concern in react development.

To implement the provider pattern, we will first create a Provider Component. A Provider is a higher-order component that the Context object provides to us. We can construct a Context object by utilizing the createContext method provided by React.

export const ThemeContext = React.createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = React.useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

After creating the provider, we will enclose components dependent on the data from context API with the created Provider Component.

To obtain the data from context API, we call useContext hook, which accepts a context as a parameter(in this case, ThemeContext).

import { useContext } from 'react';
import { ThemeProvider, ThemeContext } from "../context";


const HeaderSection = () => {
  <ThemeProvider>
    <TopNav />
  </ThemeProvider>;
};


const TopNav = () => {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <div style={{ backgroundColor: theme === "light" ? "#fff" : "#000 " }}>
      ...
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Component enhancement with HOCs (higher-order components)

A higher-order component takes in a component as an argument and returns a supercharged component injected with additional data or functionality. The possibility of HOCs in React is due to React preference of composition over inheritance.

The Higher-Order Component (HOC) pattern offers a mechanism to increase or modify a component's functionality, facilitating component reuse and code sharing.

An example of the HOC pattern is shown below:

import React from 'react'

const higherOrderComponent = Component => {
  return class HOC extends React.Component {
    state = {
      name: 'John Doe'
    }

    render() {
      return <Component name={this.state.name {...this.props} />
    }
 }


const AvatarComponent = (props) => {
  return (
    <div class="flex items-center justify-between">
      <div class="rounded-full bg-red p-4">
          {props.name}
      </div>
      <div>
          <p>I am a {props.description}.</p>
      </div>
    </div>
  )
}


const SampleHOC = higherOrderComponent(AvatarComponent);


const App = () => {
  return (
    <div>
      <SampleHOC description="Frontend Engineer" />
    </div>
  )
}

export default App;
Enter fullscreen mode Exit fullscreen mode

In the code above, the is supplied props by the higherOrderComponent, which it will utilize internally.

Compound Components

The Compound Components Pattern is a React design pattern for managing parent components that are made up of child components.

The principle behind this pattern is to break down the parent component into smaller components and then manage the interactions between these smaller components with either props, context or other react data management techniques.

This pattern comes in handy when there is a need to create reusable, versatile components made up of smaller components. It enables developers to create sophisticated UI components that can be readily customized and extended while maintaining a clear and simple code structure.

An example of a use case of the compound components pattern is shown below:

import React, { createContext, useState } from 'react';

const ToggleContext = createContext();

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

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

Toggle.On = function ToggleOn({ children }) {
  const { on } = useContext(ToggleContext);
  return on ? children : null;
}

Toggle.Off = function ToggleOff({ children }) {
  const { on } = useContext(ToggleContext);
  return on ? null : children;
}

Toggle.Button = function ToggleButton(props) {
  const { on, toggle } = useContext(ToggleContext);
  return <button onClick={toggle} {...props} />;
}

function App() {
  return (
    <Toggle>
      <Toggle.On>The button is on</Toggle.On>
      <Toggle.Off>The button is off</Toggle.Off>
      <Toggle.Button>Toggle</Toggle.Button>
    </Toggle>
  );
}
Enter fullscreen mode Exit fullscreen mode

Prop combination

This entails creating a single object out of several related props and passing it as a single prop to the component.

This pattern allows us to clean up our code and make it simpler to manage the props, making it especially helpful when we want to pass a lot of related properties to a component.

import React from 'react';

function P(props) {
  const { color, size, children, ...rest } = props;
  return (
    <p style={{ color, fontSize: size }} {...rest}>
      { children }
    </p>
  );
}

function App() {
  const paragraphProps = {
    color: "red",
    size: "20px",
    lineHeight: "22px"
  };
  return <P {...paragraphProps}>This is a P</P>;
}
Enter fullscreen mode Exit fullscreen mode

Controlled inputs

The Controlled Input pattern can be used to handle input fields. This pattern involves using an event handler to update the component state if the value of an input field changes, as well as storing the current value of the input field in the component state.

Because React controls the state and behavior of the component, this pattern makes code more predictable and readable than the uncontrolled inputs pattern, which does not use the component's state and instead controls it directly through the DOM (Document object model).

An example of a use case of a controlled inputs pattern is shown below:

import React, { useState } from 'react';

function ControlledInput() {
  const [inputValue, setInputValue] = useState('');

  const handleChange = (event) => {
    setInputValue(event.target.value);
  };

  return (
    <input type="text" value={inputValue} onChange={handleChange} />
  );
}
Enter fullscreen mode Exit fullscreen mode

Manage custom components with fowardRefs

A higher-order component called a ForwardRef takes another component as input and outputs a new component that passes the original component's ref. By doing this, the child component's ref, which can be used to retrieve the underlying DOM node or component instance, is made accessible to the parent component.

When creating a custom component that interacts with a third-party library or another custom component within your application, it is highly helpful to include the ForwardRef pattern in your workflow. By granting access to the library's DOM node or another component's DOM instance, it helps transfer control of such components to you.

An example of a use case of the forwardRef pattern is shown below:

import React from "react";

const CustomInput = React.forwardRef((props, ref) => (
  <input type="text" {...props} ref={ref} />
));

const ParentComponent = () => {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <CustomInput ref={inputRef} />;
};
Enter fullscreen mode Exit fullscreen mode

In the code above, we triggered the focus of another component <CustomInput/> from our component <ParentComponent/> using forwardRefs.

Conclusion

We discussed React design patterns in this article, including Higher-Order Components, Container-Presentational Component Patterns, Compound Components, Controlled Components, and many more. You can enhance code quality, promote team collaboration, and make your apps more scalable, flexible, and maintainable by incorporating these design patterns and best practices into your React projects.

Top comments (9)

Collapse
 
brense profile image
Rense Bakker

Tbh lately I've been using uncontrolled inputs more, since in most cases it's fine to grab the input values from the form data, the way html forms were intended to be used. You can still bind to the onChange handler to handle validation if needed, although it becomes harder (you have to use a ref) to do validation that relies on the values of other inputs in the form.

Collapse
 
shanos profile image
Stefan Schulz

another hugh advantage of using uncontrolled components is that it doesnt trigger a rerender of the component, cause there is no state which is changed... on hugh UI's it can make a big impact in performance related stuff...

great article btw.

Collapse
 
micka91 profile image
Micka91

Saying that it's an advantage to use uncontrolled components is not entirely accurate in my opinion. For me, if you encounter this kind of issue, it's because the components are not properly isolated, and the use of memo, useMemo, useCallback, useTransition, useDeferredValue, etc., is probably not being used correctly on expensive component. Having re-renders is not a problem as long as it's non-blocking for the user, and the data is not altered. It's the principle of React.

Thread Thread
 
brense profile image
Rense Bakker

The act of a user typing is not necessarily a state change unless you want to actually do something with whatever the user is typing. If that's not the case (and most of the time it isn't) I think its much nicer to the users CPU to let it cool off for a little bit 😜 if you update the state on each character typed, you're triggering a render on each key stroke which means the CPU has to do a bunch of work. Yes, if you build your app well, rendering shouldn't be a huge load on the CPU, but it's still a lot worse than not producing any extra CPU load at all.

Collapse
 
dsaga profile image
Dusan Petkovic

Thanks @necatiozmen, good article to refresh one's memory, I especially started using the Compound Pattern of recent, it kind of easy to group related components, and also make the sub components available for usage without importing all of them.

But also customizing how the child components will behave by wrapping them with the main component is handy.

{# changing the style by adding a prop #}
<Parent style="filled">  
   <Parent.child></Parent.child>
</Parent

<Parent style="outlined">  
   <Parent.child></Parent.child>
</Parent

Enter fullscreen mode Exit fullscreen mode
Collapse
 
jodoesgit profile image
Jo

Thank you for these, I shall now squirrel them away.

Collapse
 
necatiozmen profile image
Necati Özmen

🐿️🐿️🐿️

Collapse
 
lalami profile image
Salah Eddine Lalami

Thanks for sharing ,
here another project you can learn Design Pattern it's Mern-stack React.js Redux Node.js :
github.com/idurar/idurar-erp-crm

open source erp

Collapse
 
hassan_dev91 profile image
Hassan

Thanks a lot