How to use useEffect hook properly with array depe

2020-07-18 07:42发布

问题:

I am using useEffect hook and getting a list of users data with fetch call using function getStoreUsers which dispatches an action on response and stores shopUsers(which is an array) inside the redux store.

In array dependency, I am writing [shopUsers]. I don't know why it is causing infinite rendering.

Here is how I am using useEffect hook:

useEffect(() => {
    const { getStoreUsers, shopUsers } = props;
    setLoading(true);
    getStoreUsers().then(() => {
      setLoading(false);
    }).catch(() => {
      setLoading(false);
    });
  }, [shopUsers]);

I want to re-render component only when data inside shopUsers array changes.

If I write shopUsers.length inside array dependency. It stops to re-render.

But, let's suppose I have have a page which opens up when the user clicks on a userList and updates user data on next page. After the update I want the user to go back to the same component which is not unmounted previously. So, In this case array length remains the same, but data inside in of array index is updated. So shopUsers.length won't work in that case.

回答1:

You can make a custom hook to do what you want:

In this example, we replace the last element in the array, and see the output in the console.

import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
import { isEqual } from "lodash";

const usePrevious = value => {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};

const App = () => {
  const [arr, setArr] = useState([2, 4, 5]);
  const prevArr = usePrevious(arr);

  useEffect(() => {
    if (!isEqual(arr, prevArr)) {
      console.log(`array changed from ${prevArr} to ${arr}`);
    } 
  }, [prevArr]);

  const change = () => {
    const temp = [...arr];
    temp.pop();
    temp.push(6);
    setArr(temp);
  };

  return (
      <button onClick={change}>change last array element</button>
  )
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Live example here.



回答2:

Your effect is triggered based on the "shopUsers" prop, which itself triggers a redux action that updates the "shopUsers" prop and thats why it keeps infinitely firing.

I think what you want to optimize is the rendering of your component itself, since you're already using redux, I'm assuming your props/state are immutable, so you can use React.memo to re-render your component only when one of its props change.

Also you should define your state/props variable outside of your hooks since they're used in the scope of the entire function like so.

In your case, if you pass an empty array as a second param to memo, then it will only fire on ComponentDidMount, if you pass null/undefined or dont pass anything, it will be fired on ComponentDidMount + ComponentDidUpdate, if you want to optimise it that even when props change/component updates the hook doesn't fire unless a specific variable changes then you can add some variable as your second argument

React.memo(function(props){
  const [isLoading, setLoading] = useState(false);
  const { getStoreUsers, shopUsers } = props;
  useEffect(() => {
    setLoading(true);
    getStoreUsers().then(() => {
      setLoading(false);
    }).catch((err) => {
      setLoading(false);
    });
  }, []);
...
})