236

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?

Yangshun Tay
  • 49,270
  • 33
  • 114
  • 141
jonhobbs
  • 26,684
  • 35
  • 115
  • 170
  • 4
    If you have depended states you should probably use `useReducer` – thinklinux Nov 18 '19 at 11:29
  • 1
    Wow! I only I just discovered this and it has completely blown my understanding about how react rendering works. I can't understand any advantage for working this way - it seems rather arbitrary that the behaviour in an async callback is different from in a normal event handler. BTW, in my tests it seems that the reconciliation (i.e. update of the real DOM) doesn't happen until after all the setState calls have been processed, so the intermediate render calls are wasted anyway. – Andy Jul 03 '20 at 09:33
  • 2
    "it seems rather arbitrary that the behaviour in an async callback is different from in a normal event handler" - It is not arbitrary but rather by implementation [1]. React batches all setState invocations done during a React event handler, and applies them just before exiting its own browser event handler. However, several setStates outside of event handlers (e.g. in network responses) will not be batched. So you would get two re-renders in that case. [1] https://github.com/facebook/react/issues/10231#issuecomment-316644950 – soumyadityac Feb 12 '21 at 17:47
  • 'but the "hooks way" seems to be to split the state into smaller units' -- this is a bit misleading, because the multiple re-renders only happen when the `setX` functions are called within an async callback. Sources: https://github.com/facebook/react/issues/14259#issuecomment-439632622, https://blog.logrocket.com/simplifying-state-management-in-react-apps-with-batched-updates/ – allieferr Nov 11 '21 at 02:08
  • you can use `useReducer` or `redux-toolkit` – Aman Ghanghoriya Oct 22 '22 at 12:47

9 Answers9

209

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/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/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/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Yangshun Tay
  • 49,270
  • 33
  • 114
  • 141
  • Thanks Yangshun, I guess merging the state myself is the answer but not ideal. I'm just not sure about your last sentence. If I have a datatable and it renders twice with the same data, aren't all the rows and their children being rendered and updated in the DOM twice? Not sure what you meant about batching but I come from an Angular 1.x background where everything is re-rendered all the time :) – jonhobbs Dec 01 '18 at 21:06
  • 2
    I agree it's not ideal, but it's still a reasonable way of doing it. You could write your own custom hook that does merging if you don't want to do the merging yourself. I updated the last sentence to be clearer. Are you familiar with how React works? If not, here are some links that might help you understand better - https://reactjs.org/docs/reconciliation.html, http://blog.vjeux.com/2013/javascript/react-performance.html. – Yangshun Tay Dec 01 '18 at 21:12
  • 3
    @jonhobbs I added an alternative answer which creates a `useMergeState` hook so that you can merge state automatically. – Yangshun Tay Dec 01 '18 at 21:22
  • Nice solution, I use it in a bit modified way, see https://stackoverflow.com/a/55102681/121143 – mschayna Mar 11 '19 at 13:17
  • 4
    Curious under what circumstances this may be true?: "_React may batch the `setState` calls and update the browser DOM with the final new state._" – ecoe Sep 25 '19 at 13:23
  • 1
    Nice.. i thought about combining the state, or using a separate payload state, and having the other states read from the payload object. Very good post though. 10/10 will read again – Anthony Moon Beam Toorie Aug 03 '20 at 04:11
116

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

Vahid Al
  • 1,581
  • 1
  • 12
  • 24
  • 2
    `Expected 0 arguments, but got 1.ts(2554)` is an error I receive trying to use useReducer that way. More precisely `setState`. How do you fix that? The file is js, but we still use ts checks. – ZenVentzi Feb 01 '20 at 18:38
  • I think it's still the same idea that two states merged. But in some cases, you can't merge the states. – user1888955 Aug 15 '20 at 21:49
  • 1
    I think this is an awesome answer in combination with the batching update in React hooks posted below (https://github.com/facebook/react/issues/14259). Luckily, my use case was simple and I was able to simply switch over to useReducer from useState, but in other cases (such as a few listed in the GitHub issue) it's not so easy. – cjones26 Feb 09 '21 at 19:52
  • 3
    For those using typescript `useReducer>>(...)` where `MyState` is the type of your state object. – P. Galbraith May 05 '21 at 01:09
  • 1
    Thanks mate this really helped me out. Also thank you @P.Galbraith for the TypeScript solution :) – Johan Jarvi May 17 '21 at 03:29
  • 1
    I was looking to implement this solution, but how can we access previous state just like the this.setState function of class based component? like so: this.setState(prevState => {}) – user2420374 Sep 06 '21 at 02:24
  • @user2420374 added a new update regarding your question, hope it's not too late! – Vahid Al Sep 16 '21 at 12:57
  • 1
    @VahidAl, I ended up implementing setState with the spread operator, but I'll opt for this implementation from now on, thanks for the update! – user2420374 Oct 08 '21 at 21:27
93

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.

Sureshraj
  • 969
  • 6
  • 4
  • 9
    This is such a helpful answer, as it solves the problem in probably the majority of cases without having to do anything. Thanks! – davnicwil Feb 18 '20 at 14:23
  • 15
    Update: Starting in React 18 (opt-in feature) all updates will be automatically batched, no matter where they originate from. https://github.com/reactwg/react-18/discussions/21 – Sureshraj Jun 09 '21 at 09:01
  • 1
    Sureshraj **should become** the most up-voted answer. If you project allows, just go `npm install react@beta react-dom@beta` (or v18+). change the index file a bit (look at link), all problem resolved. You don't need to code anything. https://blog.saeloun.com/2021/07/15/react-18-adds-new-root-api.html plus, the v18 compiles your project MUCH faster than v17!! – Tom Jan 23 '22 at 09:39
21

This will do:

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

// later
setState({
    ...state,
    username: 'John'
});
Mehdi Dehghani
  • 10,970
  • 6
  • 59
  • 64
12

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.

ford04
  • 66,267
  • 20
  • 199
  • 171
  • 1
    Upvoted this! This is one of the most helpful features I found in React API. Found it when I tried to store a function as a state (I know it's weird to store functions, but I had to for some reason I don't remember) and it didn't work as expected because of this reserved call signature – elquimista Apr 27 '20 at 09:26
6

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

Mohamed Ramrami
  • 12,026
  • 4
  • 33
  • 49
  • Nearly a year later this feature still seems to be totally undocumented and still marked as unstable :-( – Andy Jul 03 '20 at 09:13
1

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];
};
mschayna
  • 1,300
  • 2
  • 16
  • 32
  • Cool! I would only wrap `setMergedState` with `useMemo(() => setMergedState, [])`, because, as the docs say, `setState` doesn't change between re-renders: `React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.`, this way the setState function is not recreated on re-renders. – tonix Dec 27 '19 at 17:11
0

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];
};
-4

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

JeanAlesi
  • 478
  • 3
  • 17