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

在一个普通的类组件中,你只需要一次调用就可以更新一个复杂的对象的状态,但“钩子方式”似乎是将状态分割成更小的单元,其副作用似乎是当它们分别更新时需要多次重新渲染。有什么办法可以缓解这种情况吗?


当前回答

这样做:

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

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

其他回答

除了Yangshun Tay的答案,你最好记住setMergedState函数,这样它每次渲染都会返回相同的引用,而不是新函数。如果TypeScript linter强迫你在useCallback或父组件useEffect中传递setMergedState作为依赖项,这可能是至关重要的。

import {useCallback, useState} from "react";

export const useMergeState = <T>(initialState: T): [T, (newState: Partial<T>) => void] => {
    const [state, setState] = useState(initialState);
    const setMergedState = useCallback((newState: Partial<T>) =>
        setState(prevState => ({
            ...prevState,
            ...newState
        })), [setState]);
    return [state, setMergedState];
};

这也有另一个解决方案使用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,那么解决方案是使用:

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

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

请看这个例子

这样做:

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

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

补充一点回答https://stackoverflow.com/a/53575023/121143

太酷了!对于那些计划使用这个钩子的人来说,它可以用一种有点健壮的方式来使用function作为参数,比如:

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