Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 460
jonhobbs Avatar asked Dec 01 '18 20:12

jonhobbs


People also ask

Does useState always re-render?

The NoStateCounter component has a first initial render but it never re-renders because no component props or state ever changes. As a result, the count value changes when the button is clicked, but only when useState is used will the component re-render to show the current value of count .

Can we use multiple useState?

As we learned earlier, we can use multiple State or Effect Hooks in a single component: function Form() { // 1. Use the name state variable const [name, setName] = useState('Mary'); // 2.

How does React handle multiple state updates?

React State Batch Update These changes cause parts of the component to re-render, and possibly to its children. An interesting mechanism of React, which is not mentioned much, is the state batch updating. Instead of one by one, React does batch updates, reducing the number of component renders.

Does setState cause re-render?

Method 1 (by changing props): If we pass the state of the parent component as a prop to the child and call setState on the parent, it will cause the re-render of the child component as its props are changed. The below code demonstrates the same.


2 Answers

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>
like image 159
Yangshun Tay Avatar answered Sep 17 '22 09:09

Yangshun Tay


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

like image 24
Vahid Al Avatar answered Sep 17 '22 09:09

Vahid Al