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

2022-08-30 01:04:09

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

在普通的类组件中,你会进行一次调用来更新状态,这可能是一个复杂的对象,但“钩子方式”似乎是将状态拆分为更小的单元,其副作用似乎是在单独更新时多次重新呈现。任何想法如何缓解这种情况?


答案 1

您可以将状态和状态组合到一个状态对象中,然后可以执行一次调用,并且只有一个呈现。loadingdatasetState

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

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

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>

答案 2

这还有另一个解决方案使用!首先,我们定义我们的新.useReducersetState

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

在那之后,我们可以简单地使用它,就像旧的类一样,只是没有!this.setStatethis

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

正如您可能在我们的新项目中注意到的那样(就像我们之前一样),我们不需要一起更新所有状态!例如,我可以像这样改变我们的一个状态(它不会改变其他状态!setStatethis.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
}

类型脚本支持。感谢P. Galbraith回答了这个解决方案,那些使用typescript的人可以使用这个:

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

其中 是状态对象的类型。MyState

例如,在我们的例子中,它将是这样的:

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: ''}
)

以前的状态支持。在评论中,user2420374要求一种方法来访问我们的内部,所以这里有一种方法可以实现这一目标:prevStatesetState

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检查传递的参数是函数(这意味着您正在尝试访问 prevState)还是普通对象。你可以在这里找到 Alex Grande 的 isFunction 的这个实现


注意。对于那些想经常使用这个答案的人,我决定把它变成一个图书馆。你可以在这里找到它:

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

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