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

时间:2018-12-01 20:06:59

标签: 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 };
};

在普通的类组件中,您只需调用一次即可更新状态,该状态可以是一个复杂的对象,但是“挂钩”似乎是将状态分成较小的单元,其副作用似乎是分别重新更新时会重新渲染多个。有什么想法可以减轻这种情况吗?

8 个答案:

答案 0 :(得分:15)

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

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

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

答案 1 :(得分:4)

要从类组件复制[remote "<YOUR-REMOTE-NAME>"] url = https://<YOUR-USERNAME>:<YOUR-TOKEN>@github.com/<etc your repo url> 合并行为, 反应文档recommend使用this.setState的功能形式以及对象分布-useState的{​​{3}}:

useReducer

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

一个状态对象还有另一个优点:setState(prevState => { return {...prevState, loading, data}; }); loading 依赖 状态。将状态放在一起时,无效的状态更改会变得更加明显:

data

您甚至可以通过以下方法来no need更好:1)设置状态-setState({ loading: true, data }); // ups... loading, but we already set data loadingsuccess等- 显式 和2))使用error将状态逻辑封装在化简器中:

useReducer

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"));

PS:确保使用<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>use而非useDataensure consistent states自定义挂钩。还传递给getData的回调不能是useEffect

答案 2 :(得分:3)

在的配料更新反应钩 https://github.com/facebook/react/issues/14259

  

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

答案 3 :(得分:3)

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

const [state, setState] = useReducer(
  (state, newState) => ({...state, ...newState}),
  {loading: true, data: null}
)

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

setState({loading: false, data: test.data.results})

所以我们在一起:

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
}

答案 4 :(得分:0)

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

答案 5 :(得分:0)

这可以做到:

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

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

答案 6 :(得分:0)

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

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

丹·阿布拉莫夫here推荐

查看此example

答案 7 :(得分:0)

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