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

jon*_*bbs 48 javascript reactjs react-hooks

我第一次尝试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 };
};
Run Code Online (Sandbox Code Playgroud)

在普通的类组件中,你只需要调用一次来更新可以是一个复杂对象的状态,但是"钩子方式"似乎是将状态分成更小的单元,其副作用似乎是多个在单独更新时呈现.任何想法如何缓解这个?

Yan*_*Tay 50

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

注意:setState类组件不同,setState返回的from 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'));
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

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

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'));
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

  • 我同意这并不理想,但这仍然是一种合理的做法。如果您不想自己进行合并,您可以编写自己的自定义挂钩来执行合并。我更新了最后一句话以使其更清晰。你熟悉 React 的工作原理吗?如果没有,这里有一些链接可能会帮助您更好地理解 - https://reactjs.org/docs/reconciliation.html、http://blog.vjeux.com/2013/javascript/react-performance.html。 (2认同)
  • @jonhobbs 我添加了一个替代答案,它创建了一个 `useMergeState` 钩子,以便您可以自动合并状态。 (2认同)
  • 好奇在什么情况下这可能是真的?:“_React 可能会批处理 `setState` 调用并使用最终的新状态更新浏览器 DOM。_” (2认同)

Vah*_* Al 16

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

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

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

setState({loading: false, data: test.data.results})
Run Code Online (Sandbox Code Playgroud)

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

setState({loading: false})
Run Code Online (Sandbox Code Playgroud)

太好了,哈?!

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

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
}
Run Code Online (Sandbox Code Playgroud)

  • “预期 0 个参数,但得到 1.ts(2554)” 是我尝试以这种方式使用 useReducer 时收到的错误。更准确地说是“setState”。你如何解决这个问题?文件是js,但是我们还是用ts检查。 (2认同)
  • 对于那些使用打字稿 `useReducer&lt;Reducer&lt;MyState, Partial&lt;MyState&gt;&gt;&gt;(...)` 的人,其中 `MyState` 是状态对象的类型。 (2认同)

Meh*_*ani 11

这将:

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

// later
setState({
    ...state,
    username: 'John'
});
Run Code Online (Sandbox Code Playgroud)


Sur*_*raj 10

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

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

  • 这是一个非常有用的答案,因为它可能在大多数情况下解决了问题,而无需执行任何操作。谢谢! (7认同)
  • 更新:从 React 18(选择加入功能)开始,所有更新都将自动批量处理,无论它们来自何处。https://github.com/reactwg/react-18/discussions/21 (3认同)

for*_*d04 7

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

setState(prevState => {
  return {...prevState, loading, data};
});
Run Code Online (Sandbox Code Playgroud)

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

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

setState({ loading: true, data }); // ups... loading, but we already set data
Run Code Online (Sandbox Code Playgroud)

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

setState(prevState => {
  return {...prevState, loading, data};
});
Run Code Online (Sandbox Code Playgroud)
setState({ loading: true, data }); // ups... loading, but we already set data
Run Code Online (Sandbox Code Playgroud)

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


Moh*_*ami 6

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

ReactDOM.unstable_batchedUpdates(() => { ... })
Run Code Online (Sandbox Code Playgroud)

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

看这个例子