DEV Community

OmriMor
OmriMor

Posted on

Clean up your network rendering logic with a custom hook

Introduction

Tired of writing if (loading) return <Loader/>; if (error) return <ErrorMessage/>; if (empty) return <Empty/>; return <Component/> over and over?

Suspense for Data Fetching will take care of this for us down the road. In the meantime, I have a short hook to offer as an alternative.

The problem

Your component needs to fetch some data from an API endpoint.

While the fetch request is in progress, you want your user to have a good experience so you show them a loader. Once the request is resolved, you replace the loader with the actual data.

We also want to handle the edge cases: empty and error state.

Seeing this pattern over and over in our codebase handled a little bit differently each time by each team member got us thinking.

The two main issues we wanted to address:

  • Remove the need for repetition across our codebase - keep it DRY.

  • The order could be different from one place to the other, creating inconsistent UX:

// Example A
if (loading) return <Loader/>;
if (error) return <ErrorMessage/>;
if (empty) return <Empty/>;
return <Component/>;

// Example B
if (empty) return <Empty/>;
if (loading) return <Loader/>;
if (error) return <ErrorMessage/>;
return <Component/>;

There's a crucial difference between the above examples. If you have an error in the second example but also an empty state, your user will never be aware of the error and instead just see the empty state. We believe the first pattern to be correct and would like to avoid accidentally building the wrong pattern again.

The solution

We're going to handle this with a custom Hook.

Our hook will show the appropriate component based on the current state of the data:

  • If the request is pending: show a loader
  • If the response is empty: show an empty state component
  • If the response failed: show an error state component

The code

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

function useNetworkStateHelper({
  loading = false,
  error = false,
  isEmpty = false,
  LoadingComponent = DefaultLoader,
  ErrorComponent = DefaultError,
  EmptyComponent = DefaultEmpty,
}) {
  const [isBusy, setIsBusy] = useState(loading || error || isEmpty);

  let showIfBusy;

  useEffect(() => {
    setIsBusy(loading || error || isEmpty);
    if (loading) showIfBusy = <LoadingComponent />;
    else if (error) showIfBusy = <ErrorComponent />;
    else if (isEmpty) showIfBusy = <EmptyComponent />;
  }, [loading, error, isEmpty]);

  return { isBusy, showIfBusy };
}

export default useNetworkStateHelper;

See longer version with prop-types and default components here

Let's break it down.

We set up two variables - isBusy & showIfBusy.
The first is a boolean indicating the current state.
The second will hold the current component to render based on the state.

  const [isBusy, setIsBusy] = useState(loading || error || isEmpty);
  let showIfBusy;

Next, inside our useEffect we set the corresponding component based on current state (loading, error or empty). Note that this is the correct order as defined earlier.

  useEffect(() => {
    setIsBusy(loading || error || isEmpty);
    if (loading) setShowIfBusy(<LoadingComponent />);
    else if (error) setShowIfBusy(<ErrorComponent />);
    else if (isEmpty) setShowIfBusy(<EmptyComponent />);
  }, [loading, error, isEmpty]);

Finally, return the two variables we set up in the beginning.

return { isBusy, showIfBusy };

Let's look at an example of usage.
Let's say we have a component to show a list of movies - MovieList.

Using our hook we can simply return the expected JSX from our component and let it handle the rest.

import useNetworkStateHelper from './useNetworkStateHelper';

function MovieList({ isLoading, hasError, movies }) {
  const { isBusy, showIfBusy } = useNetworkStateHelper({
    loading: isLoading,
    error: hasError,
    isEmpty: movies.length === 0,
  });

  if (isBusy) return showIfBusy;

  return movies.map(movie => <div key={movie.id}>{movie.name}</div>);
}

export default MovieList;

See an example with custom components here

How do you handle data fetching and rendering in your application? I'd love to hear.

Thanks for reading! I’m Omri, front-end engineer at Healthy.io, a digital health startup. If you think I skipped something important or have any comments, I'd love to fix them.

Top comments (0)