Homebrew React Hooks: useCurrent
Tags: react, webdev, javascript, hook
Published: 2019-06-16 15:37:54

**You might probably have heard of React Suspense.**

In 2018 Dan Abramov presented this new feature which is yet to be released. As part of [the talk](https://www.youtube.com/watch?v=6g3g0Q_XVb4) he showcased how suspense can make data fetching easier.

I was really fascinated by his words about loading indicators and how removing them can lead to better user experience.

Some of you might now think: **Why should I show a blank page to my users?**

Well, he actually meant not completely removing them, but only showing them when they are necessary.

Let's say you have a request that only takes 100 milliseconds. You would show a loading spinner for a very short time frame. The user might not even have enough time to realize he did just enter a loading state. He might just notice the page flashing.

On the other hand, if you do not have a loading indicator at all and the request takes a few seconds (e.g. due to bad internet connection) and all the user sees is a blank page, the situation becomes even worse.

Such "janky" behavior can confuse the user and make him assume that the page is buggy or broken, in the worst case he could even become angry (and we all know that only a happy user is a loyal customer).

**In order to provide the best user experience we need to solve both problems at the same time!**


So there are two big questions to answer, the first of them being:

## What do we render while the request for new content is still pending?

Correct! We render **The old content**™️

Of course, in case we do not have any old content we still need to show a loading spinner.

<br/>

Here is the second question:

## What do we render when the request for new content takes forever?

Yes! We show a loading spinner.
<br/>


Although suspense might be the silver bullet to solve this problem in the future, I asked myself: **Can we achieve the same user experience with hooks today?**
<br/>
<br/>


### Let's take a look on existing data fetching libraries

In the following examples, I am going to use a hook that simulates a network request for the data fetching part. It uses `setTimeout` internally.

```jsx
const {data, loading} = useFakeFetch("/endpoint")
```

The code should look familiar to folks that have worked with `react-apollo-hooks` or `react-fetch-hook` before.

Almost all of those data fetching hooks work the same, once a hook parameter changes, the data is re-fetched with the new parameters.

Try clicking the `change endpoint` button in the following example.

{% codesandbox use-current-example-1-tqupx %}

**Did you notice that immediately after clicking the button data becomes null?**

This is exactly the kind of behavior that we want to change!

**So where do we start?**

We could, of course, change the existing data fetching hook. 

However, my first rule regarding hooks is **the power of hooks lies in the composability of multiple hooks**.

So instead of altering the existing hook we will now step by step build a new hook that is compatible with every data fetching hook that returns a `data` and `loading` value.

So let's start by storing the old `data` until the new `data` is available.

**The perfect use-case for `useState` combined with `useEffect`**

```jsx
const {data, loading} = useFakeFetch("/endpoint")
const [currentData, setCurrentData] = React.useState(data);

React.useEffect(() => {
 // condition for updating the stored data
 if (loading === false && data !== currentData) {
   setCurrentData(data)
 }
}, [loading, data, setCurrentData, currentData]);

// use currentData instead of data
```

Check out the following example:

{% codesandbox use-current-example-2-v8nnt %}

Did you notice the new value `currentData` which now holds the old value until the new data was fetched?

Half of the problem is already solved!

In the next step, we will ensure that the loading indicator should only be shown after a certain threshold has been exceeded (aka the request is taking longer than expected).

**Reintroducing our old friends `setTimeout` and `clearTimeout`**

```jsx
const {data, loading} = useFakeFetch(endpoint);
const [currentData, setCurrentData] = React.useState(data);

React.useEffect(() => {
  if (loading === false && data !== currentData) {
    setCurrentData(data);
  }
}, [loading, data, setCurrentData, currentData]);

// NEW STUFF STARTS HERE
const previousLoadingRef = React.useRef(loading);
const [
  shouldShowLoadingIndicator,
  setShouldShowLoadingIndicator
] = React.useState(loading);

React.useEffect(() => {
  let timeout = undefined;
  // only update in case loading has changed between renders
  if (previousLoadingRef.current !== loading) {
    if (loading) {
      // should show loading indicator if request time
      // exceeds one second
      timeout = setTimeout(() => {
        setShouldShowLoadingIndicator(true);
      }, 1000);
    } else {
      setShouldShowLoadingIndicator(false);
    }
  }
  previousLoadingRef.current = loading;
  
  // cancel the timeout in case the data is available 
  // before one second has passed
  return () => timeout && clearTimeout(timeout);
}, [loading, setShouldShowLoadingIndicator]);

// use currentData instead of data
// use shouldShowLoadingIndicator instead of loading
```

In this example, we now have a long request, after one second we want to show a loading indicator!

{% codesandbox use-current-example-3-iv6yu %}

Almost done! Now we have a working proof of concept implementation. Let's convert that code into a reusable hook:

```jsx
const useCurrent = (data, loading, showLoadingIndicatorThereshold = 300) => {
  const [currentData, setCurrentData] = React.useState(data);
  const previousLoadingRef = React.useRef(loading);
  const [
    shouldShowLoadingIndicator,
    setShouldShowLoadingIndicator
  ] = React.useState(loading);

  React.useEffect(() => {
    if (loading === false && data !== currentData) {
      setCurrentData(data);
    }
  }, [loading, data, setCurrentData, currentData]);

  React.useEffect(() => {
    let timeout = undefined;
    if (previousLoadingRef.current !== loading) {
      if (loading) {
        timeout = setTimeout(() => {
          setShouldShowLoadingIndicator(true);
        }, showLoadingIndicatorThereshold);
      } else {
        setShouldShowLoadingIndicator(false);
      }
    }
    previousLoadingRef.current = loading;
    return () => timeout && clearTimeout(timeout);
  }, [loading, setShouldShowLoadingIndicator, showLoadingIndicatorThereshold]);

  return [shouldShowLoadingIndicator, currentData];
};
```

Here's a usage example example

```jsx
const { data, loading } = useFakeFetch(endpoint);
const [shouldShowLoadingIndicator, currentData] = useCurrent(
  data,
  loading,
  300
);
```

And of course, there is also a live example

{% codesandbox use-current-example-4-uq6t5 %}


**We are done!** This hook is now officially compatible with the following libraries:

- `react-apollo` (with the HOC API)
- `react-fetch-hook`
- `react-apollo-hooks`
- *`insert every hook data fetching library here`*

Let's make the web a better place for users by using this hook until React Suspense finally lands!


## Bonus: Decrease amount of rerenders (possible performance optimization)

It is further possible to even decrease the amount of rerenders.

Open the console of the following example, wit until the initial data is loaded, then click the button `Change endpoint` button once.

{% codesandbox use-current-example-4-uq6t5 %}

The last output should be `render 8`. Which means 8 renders occurred during the whole process. We can reduce the total amount of necessary renders for this procedure to 6, having 2 less rerenders.

Before we optimize the example I want to mention, that we probably do not need to do such an optimization. However, when we have a large component tree and use this hook is used on top of it, you might feel the impact of the two additional rerenders!

Let's start with the `currentData` state. We do not need an actual `useState` hook for storing it. That's because of every time `data` or `shouldShowLoadingIndicator` changes a rerender is already triggered.

We can, therefore, store `currentData` using `useRef`. As a result, we can spare also the `useEffect` for updating `currentData`.

Furthermore, `shouldShowLoadingIndicator` is only changed under two conditions:

- timeout was not cancelled (request duration was exceeded)
- during `useEffect` when `loading` has changed and is now false

The latter is actually unnecessary, we can refactor `shouldShowLoadingIndicatorR` to a `useRef` as well. That's because `shouldShowLoadingIndicator` is automatically false when `loading` is false, which means we can update `shouldShowLoadingIndicator` at the same time `currentData` is updated.

But how do we update `shouldShowLoadingIndicator` to `true`? Mutating a ref is not triggering any rerenders. The solution is the introduction a new state which soles purpose is to trigger a rerender.

Let's take a look at the optimized version:

```jsx
const useCurrent = (data, loading, showLoadingIndicatorThereshold = 300) => {
  const currentDataRef = React.useRef(data);
  const previousLoadingRef = React.useRef(loading);
  const shouldShowLoadingIndicatorRef = React.useRef(loading);
  const [, triggerStateUpdate] = React.useState(0);

  // those values should always reflect the input when loading is false 
  if (!loading) {
    currentDataRef.current = data;
    shouldShowLoadingIndicatorRef.current = false;
  }

  React.useEffect(() => {
    let timeout = undefined;
    // only when loading has changed from false to true
    if (previousLoadingRef.current !== loading && loading) {
      timeout = setTimeout(() => {
        // mutate ref
        shouldShowLoadingIndicatorRef.current = true;
        // trigger rerender
        triggerStateUpdate(i => i + 1);
      }, showLoadingIndicatorThereshold);
    }
    previousLoadingRef.current = loading;
    return () => timeout && clearTimeout(timeout);
  }, [loading, triggerStateUpdate, showLoadingIndicatorThereshold]);

  return [shouldShowLoadingIndicatorRef.current, currentDataRef.current];
};
```

Again, open the console in the following example! Wait until the initial loading has happened, click the button and observer the console logs.


{% codesandbox use-current-example-5-41sbh %}

We have successfully removed two unnecessary rerenders!


**I hope you enjoyed reading this blog post!**

I wanna thank @mfpiccolo and @sseraphini which helped my by proofreading this article!

This is only the first of many blog posts about utility hooks and other topics (including react and GraphQL).

Stay updated by following me here on DEV or [Twitter](http://twitter.com/n1rual) and [Github](http://github.com/n1ru4l).