Sneak peek into React 18 useDeferredValue hook

By default, React always renders a consistent UI. Usually, when we update the state, we expect to see changes on the screen without any delay. It makes the app feel responsive, giving us a better user experience.

However, sometimes it might be helpful to intentionally introduce an inconsistency.

Consider an example of switching tabs. While navigating from the Home tab to the Profile tab, we might see a blank page with a loader. It is because the profile page data is not loaded.

This becomes very frustrating, isn’t it?

It would have been better to stay on the Home page instead of seeing the blank page.

Before React 18, implementing this pattern was difficult. Thanks to the new concurrent features like startTransition/useTransition, useDeferredValue, etc in React 18, which have made it possible!

Let us check out the newly added hook useDeferredValue in Concurrent React.

useDeferredValue hook

The useDeferredValue is a hook that will return a deferred version of the passed value. It takes in the state value and a timeout in milliseconds.

import { useDeferredValue } from 'react';

const deferredValue = useDeferredValue(value, {
  timeoutMs: 5000
});

It is commonly used, to keep the UI responsive when we have something that renders immediately based on user input and something that needs to wait for a data fetch.

Sticking with our example of searching a photo from a large list discussed in our previous post on startTransition API, we will see how we can improve the user experience with the useDeferredValue hook.

When we type the photo name in the search input, we expect the typed character to appear on the screen without any delay.

But, because of the expensive search operation, the input becomes sluggish, as seen in the image below.

Let us jump into the code.

// SearchPhotos.jsx

import React, { useState } from "react";
import PhotoCard from "./PhotoCard";
import {
  Container, TextField
} from "@material-ui/core";
import { makeStyles } from "@material-ui/styles";

const SearchPhotos = () => {
  const useStyles = makeStyles({
    container: {
      marginTop: '100px',
    }
  });
  const [title, setPhotoTitle] = useState("");
  const classes = useStyles();

  const onChange = (e) => {
    // Urgent update
    setPhotoTitle(e.target.value);
  };

  return (
    <Container className={classes.container}>
      <TextField label="Search by photo title" onChange={onChange} value={title}/>
      <PhotoCard searchParam={title} />
    </Container>
  );
};

export default SearchPhotos;

//PhotoCard.jsx

import React, { useEffect, useState } from "react";
import photosJson from "./photos.json";
import PhotoListCard  from './PhotoListCard'

const PhotoCard = React.memo(({ searchParam }) => {
  const [photos, setPhotos] = useState();

  const fetchPhotos = (title) => {
    return new Promise((res) => {
      setTimeout(() => {
        if (!title) {
          return res(photosJson);
        }
        return res(photosJson.filter((photo) => photo.title.includes(title)));
      }, 500);
    });
  };

  useEffect(() => {
    fetchPhotos(searchParam).then((res) => {
      // Non-urgent update
      setPhotos(res);
    });
  }, [searchParam]);

  const photoData = photos?.map((p) => ({
    key: p.id,
    name: p.title,
    thumbnailUrl: p.thumbnailUrl
  }));

  return <PhotoListCard data={photoData}  />;
});

export default PhotoCard;

We need to separate high-priority updates( setPhotoTitle(e.target.value) ) from low-priority updates( setPhotos(res) ) to speed up the sluggish inputs. The useDeferredValue allows us to do so.

The input should be treated as a high priority task and searching a photo can be reduced to low priority.

Here is the modified code with the useDeferredValue hook.

import React, { useState, useDeferredValue } from "react";

const SearchPhotos = () => {
  const useStyles = makeStyles({
    container: {
      marginTop: '100px',
    }
  });
  const [title, setPhotoTitle] = useState("");
  const classes = useStyles();

  const onChange = (e) => {
    setPhotoTitle( e.target.value);
  };
  const deferredValue = useDeferredValue(title, {
    timeoutMs: 5000
  });

  return (
    <Container className={classes.container}>
      <TextField id="standard-basic" label="Search by photo title" onChange={onChange} value={title}/>
      <PhotoCard searchParam={deferredValue}  isStale={deferredValue !== title} />
    </Container>
  );
};

In this example, we are deferring the searchParam and showing the stale results till the new search result is loaded.

We have also added isStale={deferredValue !== title} to differentiate between old results and new results. The old results are in grey, while the new results are black.

  <PhotoCard searchParam={deferredValue}  isStale={deferredValue !== title} />

How is useDefferedValue different from debouncing or throttling?

Debouncing has a fixed artificial delay, no matter how fast our computer is. However, the useDeferredValue value only lags, if the rendering takes a while. There is no minimal lag imposed by React. The lag adjusts to the user’s device. On fast machines, the lag would be less, or non-existent, and on slow devices, it would be more noticeable. In both cases, the app would remain responsive.

NOTE:

The useDeferredValue hook is experimental and is subject to change.

Read more about the useDeferredValue hook on Concurrent UI Patterns documentation and WG discussion.

Need help on your Ruby on Rails or React project?

Join Our Newsletter