Code Reusability Patterns in React

When building React applications, being able to share logic is an important part of building good software so as not to always repeat yourself. Usually, we can achieve code reusability by using utility functions. However, when you want to share functionality or state logic between React components, plain javascript utility functions are sometimes not sufficient. This can be achieved using some well-established code reusability patterns in React, which includes

  1. Higher-order Components
  2. Render Props
  3. Custom Hooks

We will consider how we can implement these patterns for a particular scenario. Let's consider a scenario, where we would like to display a spinner while fetching data from various API endpoints. To solve this problem we could decide to check if the data is still being fetched and display a spinner if the data is still being fetched, then display the actual data once it's loaded.

import React, { useEffect, useState } from 'react';
import loader from "../loader.svg";

export default function DogHoundList() {
    const [ loading, setLoading ] = useState(true);
    const [ dogs, setDogs ] = useState([]);

    useEffect(() => {
        const url = 'https://dog.ceo/api/breed/hound/images/random/15';
        fetch(url)
            .then((resp) => resp.json())
            .then(function(data) {
                setDogs(data.message);
                setLoading(false);
            })
            .catch(function(error) {
                console.log(error);
                setLoading(false);
            });
    }, []);

    return (
        <div>
            {loading ? (
                <div><img src={loader} alt="loader"/></div>
            ) : (
                <div>
                    {dogs.map((dog,index) => (
                        <div key={dog} >
                            <img src={dog} alt={`${dog}-${index}`} />
                        </div>
                    ))}
                </div>
            )}
        </div>
    );
}

We could do these for one or two components, but imagine if we had up to 15 different API endpoints that different components had to load data from. At some point, we would notice that we are just repeating the data fetching logic and would need a way to share this logic. So if in the future we decide to change from using "fetch" to "axios", or change from using promises to async/ await, or even change our spinner to a linear progress bar, we wouldn't have to go to all the 15 different components to make this change, but can rather make this change from a single component. Using the different patterns mentioned above, we will see how we can share/reuse this logic across several components.

Higher-order components: A higher-order component is a function that takes a component and returns a new component ( a concept that already existed outside of the React ecosystem). when passing a component to another function, the receiving function can take the function passed to it and decide what to do with the function, based on some data or information in it(the receiving function) and/or output a modified component, which will be a combination of the function passed to it and additional data it provides. The Idea is that the higher-order function houses the logic we want to share while still allowing the house-mates (the different components we can pass to it) to be able to perform their different functions.

export default function withFetchingData(WrappedComponent, url) {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                isLoading: true,
                data: null,
                error: null
            };
        }

        componentDidMount() {
            fetch(url)
                .then((resp) => resp.json())
                .then((result) =>
                    this.setState({
                        data: result.message,
                        isLoading: false
                    })
                )
                .catch((error) =>
                    this.setState({
                        error,
                        isLoading: false
                    })
                );
        }

        render() {
            return this.state.isLoading ? (
                <img src={loader} alt="loader" />
            ) : (
                <WrappedComponent data={this.state.data} {...this.props} />
            );
        }
    };
}

Notice that we returned a class component in our Higher-order component, we could have returned a stateful functional component and fetch our data with useEffect, but if we are going to use Hooks, it then makes sense that we simply just implement a data fetching custom hook rather than convoluting our HOC by combining it with a hook, we simply just implement this functionality with a custom hook. We can now use our withDataFetching HOC to wrap other components that need to fetch data from an API. The withDataFetching HOC receives the component we would like to render and the URL to fetch the data from, handles the data fetching (in a way that's reusable), and provides the wrappedComponent with the expected data.

import React from 'react';
import withFetchingData from '../components/withFetchingData';

const url = 'https://dog.ceo/api/breed/hound/images/random/15';

function Dogs({ data }) {

    return (
        <div>
            {data.map((dog, index) => (
                <div key={dog} >
                    <img src={dog} alt={`${dog}-${index}`} />
                </div>
            ))}
        </div>
    );
}

export default withFetchingData(Dogs,url)

Render Props: A render prop uses a prop value, which is a function to decide what it renders. similar to the house-mate analogy, the component which receives "render as a prop" houses the logic we want to share while still allowing the house-mates (the functions being passed into the component as a prop called render(must not be called render)) to be able to perform different functions based on the data it's going to pass to the render props function when calling it.

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

function FetchData(props) {
    const [ state, setState ] = useState({ isLoading: true, data: null, error: null });

    useEffect(() => {
        fetch(props.url)
            .then((resp) => resp.json())
            .then((result) =>
                setState({
                    data: result.message,
                    isLoading: false
                })
            )
            .catch((error) =>
                setState({
                    error,
                    isLoading: false
                })
            );
    }, []);
    return <div>{props.render(state.data)}</div>;
}
export default FetchData(props)

In the FetchData render props component, we simply pass url props to it which is the url it fetches data from, but instead of returning the usual JSX, we are calling a render function that will be passed to FetchData as props and passing the data we fetched to this render props function. And this is how we use the FetchData Render props

import React from 'react';
import './App.css';
import DogHoundList from './pages/DogHoundList';
import FetchData from "./components/FetchData";

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <FetchData render={(data) => <DogHoundList data={data}/>} 
                   url='https://dog.ceo/api/breed/hound/images/random/15' />
      </header>
    </div>
  );
}

export default App;

We pass the url we want to fetch data from, as a prop to the FetchData component after it fetches the data, it calls the render prop function passed to it, with the data gotten from the API call. The component being returned from this render props function gets access to the data passed to it and can now do with as it pleases. If for instance, we want to fetch a list of another breed of dogs, let's say Chow breed all we need to do is to pass url that fetches Chow breed to the FetchData component and return a DogChowList component as the return value of the render props. Take note of our Fetchdata component under this render props section, we will come back to it.

<FetchData render={(data) => <DogChowList data={data}/>} url='https://dog.ceo/api/breed/chow/images/random/15' />

Custom Hook: According to the React docs, a custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks. A custom hook allows us to share logic across our functional components by extracting the common logic b/w components, so as to avoid repetition. In our example thus far, we can create a custom hook that handles our data fetching. our useFetch Implementation will look like this

import { useState, useEffect}  from 'react'

const useFetch = (url) => {
    const [ state, setState ] = useState({ isLoading: true, data: null, error: null });

    useEffect(() => {
        fetch(url)
            .then((resp) => resp.json())
            .then((result) =>
                setState({
                    data: result.message,
                    isLoading: false
                })
            )
            .catch((error) =>
                setState({
                    error,
                    isLoading: false
                })
            );
    }, []);
    return state;
  };

export default useFetch

Notice that our useFetch hook returns a state object containing isLoading, data, and error. After fetching data with the useFetch custom hook, we can now decide what to render based on the state returned from this hook, which can be used across several components.

function DogsHoundList() {
    const url = 'https://dog.ceo/api/breed/hound/images/random/15';
    const {data, isLoading} = useFetch(url)

    return (
        <div>
            { isLoading ?  <img src={loader} alt="loader" /> : data.map((dog, index) => (
                <div key={dog} >
                    <img src={dog} alt={`${dog}-${index}`} />
                </div>
            ))}
        </div>
    );
}

However, notice that we would have to perform the isLoading check to decide what to render in every component we use the useFetch hook, in order for us not to perform this check and still use the useFetch hook, we could simply just replace the data fetching logic in the FetchData component under the Render prop section of this article, using the useFetch hook. therefore

import React, { useEffect, useState } from 'react';
import loader from '../loader.svg';
import useFetch from './useFetch';

export default function FetchData(props) {
    const state = useFetch(props.url)
    return <div>{state.isLoading ? <img src={loader} alt="loader" /> : props.render(state.data)}</div>;
}

We now have a useFetch hooks that handle our data fetching and we can still use our FetchData render prop component the way we used it before. It's important to note that Custom hooks can replace HOCs and render props and present a simpler pattern for extracting logic, but can have a use case where you can use these patterns together.

Conclusion

We've seen several patterns that could be used in sharing logic across components in React. With the Addition of hooks, the custom hooks patterns have become the go-to-option for sharing logic across React components in 2020. There are however scenarios where you may still find the HOC and render props pattern useful.