Javascript 从组件中的 useState 多次调用状态更新程序会导致多次重新渲染

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/53574614/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 05:08:27  来源:igfitidea点击:

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

javascriptreactjsreact-hooks

提问by jonhobbs

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.

我第一次尝试 React 钩子,一切似乎都很好,直到我意识到当我获取数据并更新两个不同的状态变量(数据和加载标志)时,我的组件(数据表)被渲染两次,即使两次调用状态更新器发生在同一个函数中。这是我的 api 函数,它将两个变量返回给我的组件。

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?

在普通的类组件中,您只需调用一次即可更新可能是复杂对象的状态,但“钩子方式”似乎是将状态拆分为更小的单元,其副作用似乎是多次重新当它们单独更新时呈现。任何想法如何减轻这种情况?

回答by Yangshun Tay

You could combine the loadingstate and datastate into one state object and then you could do one setStatecall and there will only be one render.

您可以将loading状态和data状态合并为一个状态对象,然后您可以进行一次setState调用,并且只会有一次渲染。

Note:Unlike the setStatein class components, the setStatereturned from useStatedoesn'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.

注意:setState类内组件不同,从setState返回的useState对象不会与现有状态合并,而是完全替换对象。如果要进行合并,则需要阅读以前的状态并将其与新值合并。请参阅文档

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 setStatecalls and update the browser DOM with the final new state.

在您确定存在性能问题之前,我不会太担心过度调用渲染。渲染(在 React 上下文中)和将虚拟 DOM 更新提交到真实 DOM 是不同的事情。这里的渲染指的是生成虚拟 DOM,而不是更新浏览器 DOM。React 可能会批量setState调用并使用最终的新状态更新浏览器 DOM。

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>

回答by Vahid Al

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

这也有另一个解决方案使用useReducer! 首先我们定义我们的新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!

之后我们就可以像旧类一样简单地使用它this.setState,只是没有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!):

正如您可能在我们的 new 中注意到的setState(就像我们之前使用的一样this.setState),我们不需要一起更新所有状态!例如,我可以像这样改变我们的状态之一(它不会改变其他状态!):

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
}

回答by Sureshraj

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

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.

如果从基于 React 的事件中触发状态更新,例如按钮单击或输入更改,React 当前将批量状态更新。如果在 React 事件处理程序之外触发更新,则不会批量更新,例如异步调用。

回答by Mehdi Dehghani

This will do:

这将:

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

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

回答by Mohamed Ramrami

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 :

如果您使用第三方钩子并且无法将状态合并为一个对象或使用useReducer,则解决方案是使用:

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

Recommended by Dan Abramov here

丹·阿布拉莫夫在这里推荐

See this example

看这个例子

回答by ford04

To replicate this.setStatemerge behavior from class components, React docs recommendto use the functional form of useStatewith object spread - no needfor useReducer:

要复制this.setState从类零件的合并行为,作出反应的文档推荐使用的函数形式useState与传播对象-无需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: loadingand dataare dependentstates. Invalid state changes get more apparent, when state is put together:

一个状态对象还有另一个优点:loading并且data依赖状态。当状态放在一起时,无效的状态更改变得更加明显:

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

You can even better ensure consistent statesby 1.) making the status - loading, success, error, etc. - explicitin your state and 2.) using useReducerto encapsulate state logic in a reducer:

你甚至可以更好地确保一致的状态通过1)使状态- ,,等-明确使用你的状态和2)在减速封装状态逻辑:loadingsuccesserroruseReducer

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 prefixcustom Hooks with use(useDatainstead of getData). Also passed callback to useEffectcannot be async.

PS:务必请对前缀定制钩用useuseData而不是getData)。还通过回调来useEffect不能是async

回答by mschayna

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

回答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];
};