Multiple calls to state updater from useState in component causes multiple re-renders

JavascriptReactjsReact Hooks

Javascript Problem Overview


I'm trying React hooks for the first time and all seemed good until I realised that when I get data and update two different state variables (data and loading flag), my component (a data table) is rendered twice, even though both calls to the state updater are happening in the same function. Here is my api function which is returning both variables to my component.

const getData = url => {

    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(async () => {

        const test = await api.get('/people')

        if(test.ok){
            setLoading(false);
            setData(test.data.results);
        }
        
    }, []);

    return { data, loading };
};

In a normal class component you'd make a single call to update the state which can be a complex object but the "hooks way" seems to be to split the state into smaller units, a side effect of which seems to be multiple re-renders when they are updated separately. Any ideas how to mitigate this?

Javascript Solutions


Solution 1 - Javascript

You could combine the loading state and data state into one state object and then you could do one setState call and there will only be one render.

Note: Unlike the setState in class components, the setState returned from useState doesn't merge objects with existing state, it replaces the object entirely. If you want to do a merge, you would need to read the previous state and merge it with the new values yourself. Refer to the docs.

I wouldn't worry too much about calling renders excessively until you have determined you have a performance problem. Rendering (in the React context) and committing the virtual DOM updates to the real DOM are different matters. The rendering here is referring to generating virtual DOMs, and not about updating the browser DOM. React may batch the setState calls and update the browser DOM with the final new state.

const {useState, useEffect} = React;

function App() {
  const [userRequest, setUserRequest] = useState({
    loading: false,
    user: null,
  });

  useEffect(() => {
    // Note that this replaces the entire object and deletes user key!
    setUserRequest({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(results => results.json())
      .then(data => {
        setUserRequest({
          loading: false,
          user: data.results[0],
        });
      });
  }, []);

  const { loading, user } = userRequest;

  return (
    <div>
      {loading && 'Loading...'}
      {user && user.name.first}
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('#app'));

<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>

Alternative - write your own state merger hook

const {useState, useEffect} = React;

function useMergeState(initialState) {
  const [state, setState] = useState(initialState);
  const setMergedState = newState => 
    setState(prevState => Object.assign({}, prevState, newState)
  );
  return [state, setMergedState];
}

function App() {
  const [userRequest, setUserRequest] = useMergeState({
    loading: false,
    user: null,
  });

  useEffect(() => {
    setUserRequest({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(results => results.json())
      .then(data => {
        setUserRequest({
          loading: false,
          user: data.results[0],
        });
      });
  }, []);

  const { loading, user } = userRequest;

  return (
    <div>
      {loading && 'Loading...'}
      {user && user.name.first}
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('#app'));

<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>

Solution 2 - Javascript

This also has another solution using useReducer! first we define our new setState.

const [state, setState] = useReducer(
  (state, newState) => ({...state, ...newState}),
  {loading: true, data: null, something: ''}
)

after that we can simply use it like the good old classes this.setState, only without the this!

setState({loading: false, data: test.data.results})

As you may noticed in our new setState (just like as what we previously had with this.setState), we don't need to update all the states together! for example I can change one of our states like this (and it doesn't alter other states!):

setState({loading: false})

Awesome, Ha?!

So let's put all the pieces together:

import {useReducer} from 'react'

const getData = url => {
  const [state, setState] = useReducer(
    (state, newState) => ({...state, ...newState}),
    {loading: true, data: null}
  )

  useEffect(async () => {
    const test = await api.get('/people')
    if(test.ok){
      setState({loading: false, data: test.data.results})
    }
  }, [])

  return state
}

Typescript Support. Thanks to P. Galbraith who replied this solution, Those using typescript can use this:

useReducer<Reducer<MyState, Partial<MyState>>>(...)

where MyState is the type of your state object.

e.g. In our case it'll be like this:

interface MyState {
   loading: boolean;
   data: any;
   something: string;
}

const [state, setState] = useReducer<Reducer<MyState, Partial<MyState>>>(
  (state, newState) => ({...state, ...newState}),
  {loading: true, data: null, something: ''}
)

Previous State Support. In comments user2420374 asked for a way to have access to the prevState inside our setState, so here's a way to achieve this goal:

const [state, setState] = useReducer(
    (state, newState) => {
        newWithPrevState = isFunction(newState) ? newState(state) : newState
        return (
            {...state, ...newWithPrevState}
        )
     },
     initialState
)

// And then use it like this...
setState(prevState => {...})

isFunction checks whether the passed argument is a function (which means you're trying to access the prevState) or a plain object. You can find this implementation of isFunction by Alex Grande here.


Notice. For those who want to use this answer a lot, I decided to turn it into a library. You can find it here:

Github: https://github.com/thevahidal/react-use-setstate

NPM: https://www.npmjs.com/package/react-use-setstate

Solution 3 - Javascript

Batching update in react-hooks https://github.com/facebook/react/issues/14259

> React currently will batch state updates if they're triggered from within a React-based event, like a button click or input change. It will not batch updates if they're triggered outside of a React event handler, like an async call.

Solution 4 - Javascript

This will do:

const [state, setState] = useState({ username: '', password: ''});

// later
setState({
	...state,
	username: 'John'
});

Solution 5 - Javascript

To replicate this.setState merge behavior from class components, React docs recommend to use the functional form of useState with object spread - no need for useReducer:

setState(prevState => {
  return {...prevState, loading, data};
});

The two states are now consolidated into one, which will save you a render cycle.

There is another advantage with one state object: loading and data are dependent states. Invalid state changes get more apparent, when state is put together:

setState({ loading: true, data }); // ups... loading, but we already set data

You can even better ensure consistent states by 1.) making the status - loading, success, error, etc. - explicit in your state and 2.) using useReducer to encapsulate state logic in a reducer:

const useData = () => {
  const [state, dispatch] = useReducer(reducer, /*...*/);

  useEffect(() => {
    api.get('/people').then(test => {
      if (test.ok) dispatch(["success", test.data.results]);
    });
  }, []);
};

const reducer = (state, [status, payload]) => {
  if (status === "success") return { ...state, data: payload, status };
  // keep state consistent, e.g. reset data, if loading
  else if (status === "loading") return { ...state, data: undefined, status };
  return state;
};

const App = () => {
  const { data, status } = useData();
  return status === "loading" ? <div> Loading... </div> : (
    // success, display data 
  )
}

const useData = () => {
  const [state, dispatch] = useReducer(reducer, {
    data: undefined,
    status: "loading"
  });

  useEffect(() => {
    fetchData_fakeApi().then(test => {
      if (test.ok) dispatch(["success", test.data.results]);
    });
  }, []);

  return state;
};

const reducer = (state, [status, payload]) => {
  if (status === "success") return { ...state, data: payload, status };
  // e.g. make sure to reset data, when loading.
  else if (status === "loading") return { ...state, data: undefined, status };
  else return state;
};

const App = () => {
  const { data, status } = useData();
  const count = useRenderCount();
  const countStr = `Re-rendered ${count.current} times`;

  return status === "loading" ? (
    <div> Loading (3 sec)... {countStr} </div>
  ) : (
    <div>
      Finished. Data: {JSON.stringify(data)}, {countStr}
    </div>
  );
}

//
// helpers
//

const useRenderCount = () => {
  const renderCount = useRef(0);
  useEffect(() => {
    renderCount.current += 1;
  });
  return renderCount;
};

const fetchData_fakeApi = () =>
  new Promise(resolve =>
    setTimeout(() => resolve({ ok: true, data: { results: [1, 2, 3] } }), 3000)
  );

ReactDOM.render(<App />, document.getElementById("root"));

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>

PS: Make sure to prefix custom Hooks with use (useData instead of getData). Also passed callback to useEffect cannot be async.

Solution 6 - Javascript

If you are using third-party hooks and can't merge the state into one object or use useReducer, then the solution is to use :

ReactDOM.unstable_batchedUpdates(() => { ... })

Recommended by Dan Abramov here

See this example

Solution 7 - Javascript

A little addition to answer https://stackoverflow.com/a/53575023/121143

Cool! For those who are planning to use this hook, it could be written in a bit robust way to work with function as argument, such as this:

const useMergedState = initial => {
  const [state, setState] = React.useState(initial);
  const setMergedState = newState =>
    typeof newState == "function"
      ? setState(prevState => ({ ...prevState, ...newState(prevState) }))
      : setState(prevState => ({ ...prevState, ...newState }));
  return [state, setMergedState];
};

Update: optimized version, state won't be modified when incoming partial state was not changed.

const shallowPartialCompare = (obj, partialObj) =>
  Object.keys(partialObj).every(
    key =>
      obj.hasOwnProperty(key) &&
      obj[key] === partialObj[key]
  );

const useMergedState = initial => {
  const [state, setState] = React.useState(initial);
  const setMergedState = newIncomingState =>
    setState(prevState => {
      const newState =
        typeof newIncomingState == "function"
          ? newIncomingState(prevState)
          : newIncomingState;
      return shallowPartialCompare(prevState, newState)
        ? prevState
        : { ...prevState, ...newState };
    });
  return [state, setMergedState];
};

Solution 8 - Javascript

In addition to Yangshun Tay's answer you'll better to memoize setMergedState function, so it will return the same reference each render instead of new function. This can be crucial if TypeScript linter forces you to pass setMergedState as a dependency in useCallback or useEffect in parent component.

import {useCallback, useState} from "react";

export const useMergeState = <T>(initialState: T): [T, (newState: Partial<T>) => void] => {
    const [state, setState] = useState(initialState);
    const setMergedState = useCallback((newState: Partial<T>) =>
        setState(prevState => ({
            ...prevState,
            ...newState
        })), [setState]);
    return [state, setMergedState];
};

Solution 9 - Javascript

You can also use useEffect to detect a state change, and update other state values accordingly

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionjonhobbsView Question on Stackoverflow
Solution 1 - JavascriptYangshun TayView Answer on Stackoverflow
Solution 2 - JavascriptVahid AlView Answer on Stackoverflow
Solution 3 - JavascriptSureshrajView Answer on Stackoverflow
Solution 4 - JavascriptMehdi DehghaniView Answer on Stackoverflow
Solution 5 - Javascriptford04View Answer on Stackoverflow
Solution 6 - JavascriptMohamed RamramiView Answer on Stackoverflow
Solution 7 - JavascriptmschaynaView Answer on Stackoverflow
Solution 8 - JavascriptKonstantin GadyrkaView Answer on Stackoverflow
Solution 9 - JavascriptJeanAlesiView Answer on Stackoverflow