我第一次尝试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!首先,我们定义新的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})

Awesome,哈? !

所以让我们把所有的片段放在一起:

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要求一种方法来访问setState中的prevState,所以这里有一种方法来实现这个目标:

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

其他回答

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

Awesome,哈? !

所以让我们把所有的片段放在一起:

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要求一种方法来访问setState中的prevState,所以这里有一种方法来实现这个目标:

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

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

为了复制这个。setState类组件的合并行为, React文档建议使用useState的函数形式来扩展对象,不需要useReducer:

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

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

使用一个状态对象还有另一个优点:加载和数据是相互依赖的状态。无效的状态变化变得更加明显,当把状态放在一起:

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

你甚至可以通过以下方法更好地确保状态的一致性:1.)在你的状态中显式地显示状态——加载、成功、错误等;2.)使用useReducer在一个reducer中封装状态逻辑:

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:请确保在自定义Hooks前加上use (useData而不是getData)。同时传递给useEffect的回调不能是异步的。

如果你正在使用第三方钩子,并且不能将状态合并到一个对象或使用useReducer,那么解决方案是使用:

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

此处由丹·阿布拉莫夫推荐

请看这个例子

你可以将加载状态和数据状态合并到一个状态对象中,然后你可以做一个setState调用,只会有一个呈现。

注意:与类组件中的setState不同,useState返回的setState不会将对象与现有状态合并,它会完全替换对象。如果希望进行合并,则需要读取之前的状态,并自己将其与新值合并。参考文档。

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