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


122

我第一次尝试使用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 };
};

在普通的类组件中,您只需调用一次即可更新状态,该状态可以是一个复杂的对象,但“钩子”似乎是将状态拆分为较小的单元,其副作用似乎是多次分别更新时呈现。有什么想法可以减轻这种情况吗?


如果您有依赖状态,则可能应该使用useReducer
thinklinux

哇!我只是发现了这一点,而它完全使我对React渲染如何工作的理解。我无法理解以这种方式工作的任何优势-异步回调中的行为与普通事件处理程序中的行为似乎相当随意。顺便说一句,在我的测试中,似乎直到所有setState调用都已处理完毕后才进行协调(即,对真实DOM进行更新),因此中间渲染调用仍然被浪费了。
安迪

Answers:


121

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

注意:setStatein类组件不同,setStatereturn fromuseState不会合并具有现有状态的对象,而是会完全替换该对象。如果要进行合并,则需要读取以前的状态,然后自己将其与新值合并。请参考文档

在您确定性能问题之前,我不必担心过度调用渲染。呈现(在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/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>

替代方法-编写您自己的状态合并钩子

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>


1
我同意这并不理想,但这仍然是一种合理的方法。如果您不想自己进行合并,则可以编写自己的自定义挂钩来合并。我更新了最后一句话,使之更清楚。您是否熟悉React的工作原理?如果不是,这里有一些链接,可以帮助你更好的理解- reactjs.org/docs/reconciliation.htmlblog.vjeux.com/2013/javascript/react-performance.html
Yangshun Tay

2
@jonhobbs我添加了一个替代答案,该答案创建了一个useMergeState挂钩,以便您可以自动合并状态。
Yangshun Tay

1
棒极了,再次感谢。我对使用React越来越熟悉,但是可以了解很多有关它的内部知识。我会给他们看书。
jonhobbs

2
好奇在什么情况下这可能是正确的?:“ React可能会批量setState调用并用最终的新状态更新浏览器DOM。
ecoe

1
很好..我考虑过组合状态,或使用单独的有效负载状态,并让其他状态从有效负载对象中读取。很好的职位。10/10将再次阅读
Anthony Moon Beam Toorie

61

这也有使用的另一种解决方案useReducer!首先,我们定义新的setState

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

之后,我们就可以像老式的旧类一样简单地使用它this.setState,而无需this

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

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

setState({loading: false})

太好了,哈?!

因此,让我们将所有内容放在一起:

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
}

Expected 0 arguments, but got 1.ts(2554)我尝试以这种方式使用useReducer时收到错误。更精确地说setState。您如何解决?该文件是js,但我们仍然使用ts检查。
ZenVentzi '20


我认为两个州合并仍然是同一主意。但是在某些情况下,您无法合并状态。
user1888955

45

react-hooks中的批处理更新 https://github.com/facebook/react/issues/14259

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


5
这是一个很有帮助的答案,因为它可以在大多数情况下解决此问题,而无需执行任何操作。谢谢!
davnicwil



4

要复制this.setState从类零件的合并行为,作出反应的文档推荐使用的函数形式useState与传播对象-无需useReducer

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

这两个状态现在合并为一个,这将节省您的渲染周期。

一个状态对象还有另一个优点:loadingdata从属状态。将状态放在一起时,无效的状态更改会变得更加明显:

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

你甚至可以更好地确保一致的状态 通过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 
  )
}

PS:务必请对前缀定制钩用useuseData而不是getData)。还传递了回调到useEffect不能为async


1
投票了!这是我在React API中发现的最有用的功能之一。当我尝试将函数存储为状态时发现了它(我知道存储函数很奇怪,但是由于某种原因我不记得了),并且由于此保留的调用签名而无法正常工作
elquimista

1

一些补充答案https://stackoverflow.com/a/53575023/121143

凉!对于那些打算使用此钩子的人,可以以某种健壮的方式编写它,以将函数用作参数,例如:

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

更新:优化版本,当传入的部分状态未更改时,状态不会被修改。

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

凉!我只换setMergedStateuseMemo(() => setMergedState, []),因为,作为文档说,setState不会再呈现之间切换: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.,这种方式的setState函数未在重新重新呈现。
tonix

0

您还可以useEffect用来检测状态更改,并相应地更新其他状态值


你能详细说明吗?他已经在使用useEffect
bluenote10
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.