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

IT技术 javascript reactjs react-hooks
2021-02-15 03:55:32

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

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

6个回答

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

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

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

太棒了,再次感谢。我对使用 React 越来越熟悉,但可以学到很多关于它的内部原理。我给他们读一读。
2021-04-17 03:55:32
我同意这并不理想,但它仍然是一种合理的做法。如果您不想自己进行合并,您可以编写自己的自定义挂钩来进行合并。我更新了最后一句话以更清楚。你熟悉 React 的工作原理吗?如果没有,这里有一些链接可以帮助您更好地理解 - reactjs.org/docs/reconciliation.htmlblog.vjeux.com/2013/javascript/react-performance.html
2021-04-22 03:55:32
很好..我考虑组合状态,或使用单独的有效载荷状态,并从有效载荷对象中读取其他状态。不过很好的帖子。10/10 将再次阅读
2021-05-02 03:55:32
@jonhobbs 我添加了一个替代答案,它创建了一个useMergeState钩子,以便您可以自动合并状态。
2021-05-05 03:55:32
好奇在什么情况下这可能是真的?:“ React 可能会批量setState调用并使用最终的新状态更新浏览器 DOM。
2021-05-12 03:55:32

这也有另一个解决方案,使用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})

正如您可能在我们的 new 中注意到的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
}

typescript支持感谢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要求一种方法来访问我们的prevState内部setState,所以这里有一种方法可以实现这个目标:

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

对于那些使用typescript的人来说,你的状态对象的类型useReducer<Reducer<MyState, Partial<MyState>>>(...)在哪里MyState
2021-04-17 03:55:32
我想实现这个解决方案,但是我们如何像基于类的组件的 this.setState 函数一样访问以前的状态?像这样:this.setState(prevState => {})
2021-04-27 03:55:32
Expected 0 arguments, but got 1.ts(2554)是我在尝试以这种方式使用 useReducer 时收到的错误。更准确地说setState你如何解决这个问题?该文件是js,但我们仍然使用ts检查。
2021-05-03 03:55:32
我认为这是一个很棒的答案,结合下面发布的 React 钩子中的批处理更新(github.com/facebook/react/issues/14259)。幸运的是,我的用例很简单,我可以简单地从 useState 切换到 useReducer,但在其他情况下(例如 GitHub 问题中列出的一些),这并不容易。
2021-05-08 03:55:32
谢谢伙计,这真的帮助了我。也感谢@P.Galbraith 的 TypeScript 解决方案:)
2021-05-08 03:55:32

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

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

这是一个非常有用的答案,因为它可能在大多数情况下解决了问题而无需做任何事情。谢谢!
2021-04-16 03:55:32
更新:从 React 18(选择加入功能)开始,所有更新都将自动批处理,无论它们来自何处。github.com/reactwg/react-18/discussions/21
2021-05-09 03:55:32

这将:

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

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

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

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

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

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

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

点赞这个!这是我在 React API 中发现的最有用的功能之一。当我尝试将函数存储为状态时发现它(我知道存储函数很奇怪,但由于某种原因我不记得了)并且由于这个保留的调用签名它没有按预期工作
2021-04-18 03:55:32