13

Suppose I have the following code: (which is too verbose)

function usePolicyFormRequirements(policy) {
  const [addresses, setAddresses] = React.useState([]);
  const [pools, setPools] = React.useState([]);
  const [schedules, setSchedules] = React.useState([]);
  const [services, setServices] = React.useState([]);
  const [tunnels, setTunnels] = React.useState([]);
  const [zones, setZones] = React.useState([]);
  const [groups, setGroups] = React.useState([]);
  const [advancedServices, setAdvancedServices] = React.useState([]);
  const [profiles, setProfiles] = React.useState([]);

  React.useEffect(() => {
    policiesService
      .getPolicyFormRequirements(policy)
      .then(
        ({
          addresses,
          pools,
          schedules,
          services,
          tunnels,
          zones,
          groups,
          advancedServices,
          profiles,
        }) => {
          setAddresses(addresses);
          setPools(pools);
          setSchedules(schedules);
          setServices(services);
          setTunnels(tunnels);
          setZones(zones);
          setGroups(groups);
          setAdvancedServices(advancedServices);
          setProfiles(profiles);
        }
      );
  }, [policy]);

  return {
    addresses,
    pools,
    schedules,
    services,
    tunnels,
    zones,
    groups,
    advancedServices,
    profiles,
  };
}

When I use this custom Hook inside of my function component, after getPolicyFormRequirements resolves, my function component re-renders 9 times (the count of all entities that I call setState on)

I know the solution to this particular use case would be to aggregate them into one state and call setState on it once, but as I remember (correct me, if I'm wrong) on event handlers (e.g. onClick) if you call multiple consecutive setStates, only one re-render occurs after event handler finishes executing.

Isn't there any way I could tell React, or React would know itself, that, after this setState another setState is coming along, so skip re-render until you find a second to breath.

I'm not looking for performance-optimization tips, I'm looking to know the answer to the above (Bold) question!

Or do you think I am thinking wrong?

Thanks!

--------------


UPDATE How I checked my component rendered 9 times?

export default function PolicyForm({ onSubmit, policy }) {
  const [formState, setFormState, formIsValid] = usePgForm();
  const {
    addresses,
    pools,
    schedules,
    services,
    tunnels,
    zones,
    groups,
    advancedServices,
    profiles,
    actions,
    rejects,
    differentiatedServices,
    packetTypes,
  } = usePolicyFormRequirements(policy);

  console.log(' --- re-rendering'); // count of this
  return <></>;
}
Ardeshir Izadi
  • 955
  • 1
  • 7
  • 25
  • With a full blown `class Thing extends React.Component` you can have a `shouldComponentUpdate`. – colburton Dec 03 '19 at 18:40
  • If they're updated all the time together then perhaps you should consider a more-unified state object. – Dave Newton Dec 03 '19 at 18:41
  • show us an example of how you checked that your component renders 9 times. – Dennis Vash Dec 03 '19 at 18:41
  • @DennisVash ``` export default function PolicyForm({ onSubmit, policy }) { const { addresses, pools, schedules, services, tunnels, zones, groups, advancedServices, profiles, } = usePolicyFormRequirements(policy); console.log(' --- re-rendering'); return <>>``` – Ardeshir Izadi Dec 03 '19 at 18:44
  • use sandbox https://codesandbox.io/s/new or add a WORKING snippet (Ctrl+M) – Dennis Vash Dec 03 '19 at 18:45
  • @DennisVash https://codesandbox.io/s/nice-euler-gwqxo – Ardeshir Izadi Dec 04 '19 at 06:17
  • 1
    There is a way to force the batching of updates. Take a look at [this article](https://blog.logrocket.com/simplifying-state-management-in-react-apps-with-batched-updates/) to see how it works. I am using it and it cuts down the renders to one. – Andrew Einhorn Jan 15 '21 at 10:57
  • Thanks @AndrewEinhorn. That's so useful. – Ardeshir Izadi Jan 18 '21 at 08:32

6 Answers6

10

I thought I'd post this answer here since it hasn't already been mentioned.

There is a way to force the batching of state updates. See this article for an explanation. Below is a fully functional component that only renders once, regardless of whether the setValues function is async or not.

import React, { useState, useEffect} from 'react'
import {unstable_batchedUpdates} from 'react-dom'

export default function SingleRender() {

    const [A, setA] = useState(0)
    const [B, setB] = useState(0)
    const [C, setC] = useState(0)

    const setValues = () => {
        unstable_batchedUpdates(() => {
            setA(5)
            setB(6)
            setC(7)
        })
    }

    useEffect(() => {
        setValues()
    }, [])

    return (
        <div>
            <h2>{A}</h2>
            <h2>{B}</h2>
            <h2>{C}</h2>
        </div>
    )
}

While the name "unstable" might be concerning, the React team has previously recommended the use of this API where appropriate, and I have found it very useful to cut down on the number of renders without clogging up my code.

Andrew Einhorn
  • 741
  • 8
  • 23
6

If the state changes are triggered asynchronously, React will not batch your multiple state updates. For eg, in your case since you are calling setState after resolving policiesService.getPolicyFormRequirements(policy), react won't be batching it.

Instead if it is just the following way, React would have batched the setState calls and in this case there would be only 1 re-render.

React.useEffect(() => {
   setAddresses(addresses);
   setPools(pools);
   setSchedules(schedules);
   setServices(services);
   setTunnels(tunnels);
   setZones(zones);
   setGroups(groups);
   setAdvancedServices(advancedServices);
   setProfiles(profiles);
}, [])

I have found the below codesandbox example online which demonstrates the above two behaviour.

https://codesandbox.io/s/402pn5l989

If you look at the console, when you hit the button “with promise”, it will first show a aa and b b, then a aa and b bb.

In this case, it will not render aa - bb right away, each state change triggers a new render, there is no batching.

However, when you click the button “without promise”, the console will show a aa and b bb right away. So in this case, React does batch the state changes and does one render for both together.

Sarath P S
  • 131
  • 5
  • Thanks! That's what I was looking for. – Ardeshir Izadi Dec 04 '19 at 06:20
  • I am thinking of some way to tell React, behave a function as a batcher. for example, after my promise resolved, I could tell `React.batch(() => {// my setStates})` Do you think I am thinking wrong about this? – Ardeshir Izadi Dec 04 '19 at 06:22
  • 2
    There is a way where you can make use of `ReactDOM` 's unstable_batchedUpdates to tell react to batch all your setState calls. In `useEffect` after resolving your method, you can do something like this `ReactDOM.unstable_batchedUpdates(() => { setAddresses(addresses); setPools(pools); setSchedules(schedules); setServices(services); setTunnels(tunnels); setZones(zones); setGroups(groups); setAdvancedServices(advancedServices); setProfiles(profiles); });` – Sarath P S Dec 04 '19 at 13:17
  • Thank you , you're deserved thousand likes :) – VinceNguyen Jun 04 '21 at 13:43
4

Isn't there any way I could tell React, or React would know itself, that, after this setState another setState is coming along, so skip re-render until you find a second to breath.

You can't, React batches (as for React 17) state updates only on event handlers and lifecycle methods, therefore batching in promise like it your case is not possible.

To solve it, you need to reduce the hook state to a single source.

From React 18 you have automatic batching even in promises.

Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
4

You can merge all states into one

function usePolicyFormRequirements(policy) {
  const [values, setValues] = useState({
    addresses: [],
    pools: [],
    schedules: [],
    services: [],
    tunnels: [],
    zones: [],
    groups: [],
    advancedServices: [],
    profiles: [],
  });
  
  React.useEffect(() => {
    policiesService
      .getPolicyFormRequirements(policy)
      .then(newValues) => setValues({ ...newValues }));
  }, [policy]);

  return values;
}
Tolotra Raharison
  • 3,034
  • 1
  • 10
  • 15
4

By the way, I just found out React 18 adds automatic update-batching out of the box. Read more: https://github.com/reactwg/react-18/discussions/21

Ardeshir Izadi
  • 955
  • 1
  • 7
  • 25
3

REACT 18 UPDATE

With React 18, all state updates occurring together are automatically batched into a single render. This means it is okay to split the state into as many separate variables as you like.

Source: React 18 Batching

Azzam Michel
  • 573
  • 5
  • 8