201

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

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

4

9 回答 9

178

您可以将loadingstate 和datastate 组合成一个 state 对象,然后您可以进行一次setState调用,并且只会进行一次渲染。

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

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

于 2018-12-01T21:03:12.257 回答
97

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

厉害了哈?!

所以让我们把所有的部分放在一起:

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询问了一种访问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

于 2019-06-10T20:32:00.017 回答
80

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

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

于 2019-02-02T18:56:16.663 回答
14

这将做:

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

// later
setState({
    ...state,
    username: 'John'
});
于 2019-07-14T06:39:42.410 回答
9

要从类组件复制this.setState合并行为,React 文档建议useState使用with object spread的功能形式-不需要useReducer

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

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

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

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

您甚至可以 通过 1.)在您的状态中明确显示状态 - 、、等来更好地确保一致的状态,以及 2.) 使用将状态逻辑封装在 reducer 中: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 
  )
}

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)。也将回调传递给useEffectcannot be async

于 2020-04-12T18:54:42.653 回答
6

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

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

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

看这个例子

于 2019-09-09T10:29:17.950 回答
1

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

凉爽的!对于那些打算使用这个钩子的人来说,可以用一种比较健壮的方式来编写它来使用函数作为参数,例如:

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];
};
于 2019-03-11T13:14:50.250 回答
0

除了Yangshun Tay回答,你最好记住setMergedState函数,所以每次渲染都会返回相同的引用而不是新函数。setMergedState如果 TypeScript linter 强制您作为依赖项传递到父组件中useCallbackuseEffect父组件中,这可能至关重要。

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];
};
于 2021-12-15T14:23:25.203 回答
-3

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

于 2020-10-28T23:19:06.520 回答