React hook - lazy loading pattern

Lazy loading some data, that you also want to save into your app data store is a common enough task to warrent abstracting to a common pattern.

TLDR

  1. Use a consitent convention for naming hooks following this pattern, for example `use<dataName>Data` so useFilterData, useActivityData etc.
  2. Always return an object from the hook with data, loading and error properties. This will give them a predictable API.
  3. The hook should take any arguments that are relvant to the specific data, So we might pass in the current tab to the useTabData hook.
  4. The hook should return data from the store by default, and only make async calls if that data doesn't exist (and then update the store after fetching it).
  5. Optional: Along side data, loading and error; a forth property onUpdate, which is a callback, can be returned for when the data also needs to be saved in the db

The goal of this pattern is to make interfacing with data from endpoints have a consistent feel as well as taking advantage of lazy loading content, and not re-fetching if the data alreay exists in your data store. Here an minimal example app I made to demo the pattern.

So how would we write a hook using this pattern, lets say the end goal is ta make a custom hook called `useTabData`

const {
  data,
  loading,
  error
} = useTabData(tab)

It will fetch data depending on what tab the user has open, following this pattern we should always return an object with data, loading and error so our component can deal with these situations. And now the logic internal to this hook is basically going to check if the data is in the store and only make a async request if it's not there. Here's an example.

import { useState, useEffect } from "react";
import { useStore } from "./store";

export const useTabData = tab => {
  const [state, dispatch] = useStore();
  const coolData = state && state.coolData && state.coolData[tab];
  const shouldFetchData = tab && !(coolData && coolData.length);

  useEffect(() => {
    if (shouldFetchData) fetchData(tab);
  }, [tab, shouldFetchData]);
  return {
    data: coolData || [],
    loading: isLoading,
    error: ""
  };
};

Basically we are getting our state from our store, and if there is no data for the current tab, we'll skip running `fetchData` inside the useEffect. Of course returning data, loading and error. Filling this in a little more by defining fetchData.

export const useTabData = tab => {
  const [isLoading, setIsLoading] = useState(true);
  const [state, dispatch] = useStore();
  const coolData = state && state.coolData && state.coolData[tab];
  const shouldFetchData = tab && !(coolData && coolData.length);

  const fetchData = async curretTab => {
    setIsLoading(true);
    const promise = new Promise(resolve => {
      setTimeout(
        () => resolve(tab === "sweet"
          ? ["a", "b", "c", "d"]
          : ["x", "y", "z"]),
        3000
      );
    });
    const data = await promise;
    dispatch({
      type: "UPDATE_COOL_DATA",
      payload: {
        type: currentTab,
        data
      }
    });
    setIsLoading(false);
  };

  useEffect(() => {
    if (shouldFetchData) fetchData(tab);
  }, [tab, shouldFetchData]);
  return {/* data, loading, error etc*/};
};

So were using another bit of state at the top of our hook for isLoading, setting that to true at the stare of a call to fetchData, then we make our async call (mocked here with a timeout), once the data comes back we'll update the store and set isLoading to false. All is well with our frondend 🤗.

The above example can be seen here, Or codesandbox (styles arn't working in the sandbox 🤷‍♀️).
The Repo can be found here, (note, that it's writen in typescript).
Here's a blog post detailing a react hooks data-store similar to the one that I'm using in the example. (though the type of store doesn't matter)

Some other points

  • Note that in the example the data being returned is coming directly from the store (assigned to a varible inbetween). It might be tempting to put the data from your async call into a local useState, but I would highly discourage it as duplicating state like this could lead to some nasty bugs.
  • Also adding a onUpdate callback, for data that can be save to the backend from the fronend. This callback would obviously be making async calls and so It also should return a similar format of data, isLoading, error (maybe status instead of data, if you're not interested in the data comming back).
  • We would implement the returned error in a real example.
  • You might get stale data if you only ever return from the store. For most data this isn't likely to be a huge problem, but some extra logic to invalidate/timeout old data could be implemented.
  • If you find you're writing a lot of hook like this for you app, you migh want to abstract some of this into a hook factory in order to reduce duplication (i.e. the code that checks if the data is already in the store). I might come back and write more details about this. 🤞
Did you know you can get Electronic-Mail sent to your inter-web inbox, for FREE !?!?
You should stay up-to-date with my work by signing up below.