How to use useEffect hook properly with array dependency. I passed state from redux store and still my component renders infinitely

14,727

Solution 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.

Solution 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);
    });
  }, []);
...
})
Share:
14,727
shubham choudhary
Author by

shubham choudhary

Updated on June 09, 2022

Comments

  • shubham choudhary
    shubham choudhary about 2 years

    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.

  • shubham choudhary
    shubham choudhary about 5 years
    yeah, redux action updates the shopUsers when component mount mounts for first time. so let's consider shopUsers initially set to null and when component renders. useEffect gets executed. It hits the API and sets the shopUsers to data with users which is array of objects. So now shopUsers changed it re-renders, and tried to hit api again, and gets shopUsers again, but data is now same as previous. so, from next time onwards it should not trigger re-render. I have one doubt. does useEffect performs a shallow comparison on array and objects?
  • Khaled Osman
    Khaled Osman about 5 years
    I don't get what you're trying to do, it feels weird to me.. why would you want to update a variable that it, itself is used to trigger a call that updates itself? I don't understand the usecase, but most commonly what you want to do is have a parent component that makes the API call on componentDidMount, or a useEffect(fn, []) with empty array as second argument to be triggered once, and pass the result as props to the child component to render them. And then whenever you want to make the API call again it should be in an event handler in the parent component that will update the props.
  • shubham choudhary
    shubham choudhary about 5 years
    Thanks,looks like your solution can work for me. Can you explain few things first here, the parameters we pass the array as dependency, does react performs shallow comparition just like isEqual does? if not, then don't you think it should secondly, is this useRef thing introduced with redux hooks only or was it previously also there?
  • Khaled Osman
    Khaled Osman about 5 years
    you can listen to some event or pass a callback prop thats triggered when the user navigates back to trigger the API call to fetch data again, that's much cleaner than making an API call every time the component updates or a prop gets changed.
  • Colin Ricardo
    Colin Ricardo about 5 years
    isEqual uses deep comparison. useRef is a hook provided by React, yes. It's used similarly to React.createRef().
  • shubham choudhary
    shubham choudhary about 5 years
    Yeah, sorry I was talking about deep comparison :)
  • Colin Ricardo
    Colin Ricardo about 5 years
    As of now React only does shallow comparisons, but that might change in the future!
  • shubham choudhary
    shubham choudhary about 5 years
    You are right callback can be a solution.But, I mostly avoid using callbacks. It makes login flow unpredictable. In one case I have a third screen in app which once updates navigates back to same page. So I will need to pass callback from one screen to another and then to third. and passing data as params using react navigation takes lot work. I am too lazy for that
  • shubham choudhary
    shubham choudhary about 5 years
    You were right. I was being weird. Now I got this now.
  • Khaled Osman
    Khaled Osman about 5 years
    if you don't want to pass the same props to multiple components I think you can use Context for that to render something different based on some shared variable