React 18 features: batching, useTransition and useDeferredValue

Reading Time: 5 minutes

A Fresh Look For React

I love React applications, but there is always the problem that in applications with larger computation, heavy logic or bulky loading process, it starts to feel "slugish" and the user may percive that is not responding so well to their interaction.

With the introduction of React 18 not so long ago, there are new features that aim to reduces the stress on loading and give the user that sensation of responsiveness when interacting with the page.

This is a birds eye view of some of the features that caugh my attention on this new version, and how you too can put them to practice, so your apps run smoothly as positive.

Batching

First we need to explain the concept of batching:

Batching is when React groups multiple state updates into a single re-render for better performance.

Basically its a management of the state updates for the React events, but what happend with "non react events"; Well for state updates that involve third party methods like API calls or promise based query, the batching is not implemented.

Take a look a this example:

Prior to React 18, the batching was not implemented by defaut in non native events, causing undesired rendering.

No Batching

const App = () => {
  const [counter, setCounter] = useState(0);
  const [active, setActive] = useState(false);

  const handleCounterIncrease = () => {
    setTimeout(() => {
      setCounter(counter + 1);
      setActive(!active);
    }, 0);
  };

  const handleCounterDecrease = async () => {
    await Promise.resolve();

    setCounter(counter - 1);
    setActive(!active);
  };

  console.log('component rendering');

  return (
    <div>
      <button type="button" onClick={handleCounterIncrease}>
        Increase
      </button>
      <button type="button" onClick={handleCounterDecrease}>
        Decrease
      </button>

      <div>Counter: {counter}</div>
      <div>Active: {active.toString()} </div>
    </div>
  );
};

Basically If you run this code, click one of the buttons, and take a look in the console we will see the next output:

non-batching

You can see that our component its re-renderind twice, that is cause its setting two separete states inside a promise based event.

Automatic Batching

With this new feture implemented, now by default updates inside of timeouts, promises, native event handlers or any other event are batched.

However in React 18 environments this will only trigger one render.

  const handleCounterIncrease = () => {
    setTimeout(() => {
      setCounter(counter + 1);
      setActive(!active);
    }, 0);
  };

  const handleCounterDecrease = async () => {
    await Promise.resolve();

    setCounter(counter - 1);
    setActive(!active);
  };

const handleCounterIncrease = () => {
    setCounter(counter + 1);
    setActive(!active);
};

auto-batching

This is an awesome feature for performance standpoints because it avoids unnecessary re-renders and adds the great benefit of preventing 'half' renderings when just one state variable was updated.

Transitions

This is a new concept introduced in React 18 from which we can identify two types of updates in our app, The oficial documenation qualifies them as follows

Urgent updates: reflect direct interaction, like typing, clicking, pressing, and so on.
Transition updates: transition the UI from one view to another.

From an user perspective point, you are waiting to feel your applications as close to reality as posible, meaning that if you press a key in your keyboard, you spect to see the character pop in the screen as soon as you pressed it. Otherwise this will feel slow or wrong.

So Whats The Difference?

That being said, urgent updates are ment to function this way, to give priority on the state update chain to be the first ones to change and show a new value.

Transitions in the other hand are usually the action of getting the result based on the interaction on the page (like a list of values after typing , or a collections of images), that in our brain, going and fetch for a response is much more slower, than a click for example, so is expected that this action will take longer.

How React Thinks

For example, imagine you enter a store page, and try to serch an specific item in the search box, our store has over 2,000,000 items available to filter from, the expected behavior is that as soon you type something the character shows in the searchbox, and the suggestions appear latter or instantly depending on the concurrency and other variables.

Based on how react works, when you update multiple states, it will try to bundle them in one single call and them make them all at once, before doing the re-rendering.

Before React 18, having the same category of "urgency", for all of the state updates, resulted in slow pages, and giving the impression of not responding to the input.

useTransition to The Rescue

This new version for React offers the option of giving a specific setState the urgency based on your needs , simple import the new hook, and grap around the set state that you want to delegate in the callback like:

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

const [isPending, startTransition] = useTransition();
const [inputValue, setInputValue] = useState('')
const [response, setResponse] = useState([])

const handleInput = (e) =>{
    setInputValue(e.target.value)
    startTransition(() => {
       const a= []
       for (let i = 0; i < 2000000; i++) {
        a.push(e.target.value)
     } 
      setResponse(a)
    });
  }

.... rest of the code

      <input type="text" onChange={handleInput} value={inputValue} placeholder="typehere"/>
      {isPending ? <p>loading</p> 
        : response.map((item, index)=>(<p key={index}>{item}</p>))
      }

Now, the call to setResponse will be consider a Transition and has lower priority, also it gives iImplicit urgen tag to the other setState.

In practice this will result in a first render of the input in the searchbox, as the user is typing, and Afterwards it will render all the list based on the input, giving a more responsive sensation to the interaction.

UseDeferred Value

Another great addition to this version is the introudction of useDeferredValue, another hook that allows to improve waiting time and for the user and make your pages smooth.

How it Works

This hook implements logic similar to debuncing or throttling, basically the delay the execution of a funtion and reduces the event from firing multiple times.

For example, if we continue with the shop analogy, when you type something in the search box, debouncing will wait a few miliseconds expecting for another user interaction, if it does not receive any, then will fire the event

So for useDeferredValue you dont have to specify a set amount of time to wait for the action to be ran. Instead react automaticalle asumes that the value inside the hook will ve Deferred, so it will wait until the application isnt busy with anything else to go ahead and run the changes.

How To Use It

To use it we need to call the hook, and pass the value we want to be deferred, like so:

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

  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );

On our first render the deferred value will be the same as the first input. Later on while you type, React will first attempt a re-render with the old value, and then try another re-render in background with the new value.

This will cause the input to update inmmediately, and wait a bit for the result of the search to show. Another plus factor is that it will have a memory of the last value provided, so if you type that character again it will populate with the response and not the loading state in this case.

This proves to be handy to avoid wainting times on user based actions and server actions

0 Shares:
You May Also Like
Read More

An introduction to Rust

Reading Time: 4 minutes At MagmaLabs we always keep up to date by trying and using new technologies because of our client's…