Search code examples
reactjsreact-hooks

React.useCallback : it doesn't prevent continuos re-rendering


I'm trying to use React.useCallback functions within React.useEffect in order to avoid continuous re-rendering.

My objective is, once selected a sorting option, keep the list sorted even when new elements are added. But without continuous-rendering. This is why I tried to use React.useCallback

But what I've done so far, doesn't prevent continuous re-rendering...

What's wrong with these React.useCallback functions?

 const memoizedIncrByName = React.useCallback(() => {
    let new_data = [...data].sort((a, b) => {
      if (b.name < a.name) return 1;
      if (b.name > a.name) return -1;
      return 0;
    });
    setData(new_data);
  }, [data]);

  const memoizedIncrByEmail = React.useCallback(() => {
    let new_data = [...data].sort((a, b) => {
      if (b.email < a.email) return 1;
      if (b.email > a.email) return -1;
      return 0;
    });
    setData(new_data);
  }, [data]);

  React.useEffect(() => {
    console.log("SelectedOption:", selectedSortingOption);
    if (selectedSortingOption !== null) {
      if (selectedSortingOption.value === "name") {
        memoizedIncrByName();
      } else if (selectedSortingOption.value === "email") {
        memoizedIncrByEmail();
      }
    }
  }, [memoizedIncrByName, memoizedIncrByEmail, selectedSortingOption]);



    <Select
          defaultValue={selectedSortingOption}
          onChange={SetSelectedSortingOption}
          options={sortingOptions}
        />

data sample:

let new_users = [
  {
    id: 5,
    name: "Chelsey Dietrich",
    username: "Kamren",
    email: "[email protected]",
    address: {
      street: "Skiles Walks",
      suite: "Suite 351",
      city: "Roscoeview",
      zipcode: "33263",
      geo: {
        lat: "-31.8129",
        lng: "62.5342"
      }
    },
    phone: "(254)954-1289",
    website: "demarco.info",
    company: {
      name: "Keebler LLC",
      catchPhrase: "User-centric fault-tolerant solution",
      bs: "revolutionize end-to-end systems"
    }
  },
  {
    id: 6,
    name: "Mrs. Dennis Schulist",
    username: "Leopoldo_Corkery",
    email: "[email protected]",
    address: {
      street: "Norberto Crossing",
      suite: "Apt. 950",
      city: "South Christy",
      zipcode: "23505-1337",
      geo: {
        lat: "-71.4197",
        lng: "71.7478"
      }
    },
    phone: "1-477-935-8478 x6430",
    website: "ola.org",
    company: {
      name: "Considine-Lockman",
      catchPhrase: "Synchronised bottom-line interface",
      bs: "e-enable innovative applications"
    }
  },

Update 1)

Following the wise suggestions made by Nicholas Tower, I tried in this way:

  const sortedData = React.useMemo(() => {
    if (selectedSortingOption !== null) {
      if (selectedSortingOption.value === "name") {
         return [...data].sort((a, b) => {
          if (b.name < a.name) return 1;
          if (b.name > a.name) return -1;
          return 0;
        });
      } else if (selectedSortingOption.value === "email") {
        return [...data].sort((a, b) => {
          if (b.email < a.email) return 1;
          if (b.email > a.email) return -1;
          return 0;
        })
      } else {
        return data;
      }
    } else {
      return data;
    }
  }, [data, selectedSortingOption]);

Now it does not continuously re-render anymore, but, surprisingly, when I add a new item in the data, updating data, it does not re-render. And to make the new data appears, I have to change the selectedSortingOption. So, I guess, there is something else to fix

This is the function called when clicking the button AddEmployee :

const [newEmployee, setNewEmployee] = React.useState([]);

  const addSingleEmployee = () => {
    if (new_users.length === 0) {
      return;
    }
    let employee = new_users[0];

    let employeeArray = [];
    employeeArray.push(employee);
    setNewEmployee(employeeArray);

    new_users.shift();
    newData = data;
    newData.push(employee);
    setData(newData);
  };

Solution

  •  const memoizedIncrByName = React.useCallback(() => {
        let new_data = [...data].sort((a, b) => {
          if (b.name < a.name) return 1;
          if (b.name > a.name) return -1;
          return 0;
        });
        setData(new_data);
      }, [data]);
    

    Every time the data changes, the memoization breaks. But every time you call the function, you change the data, thus breaking the memoization. And since your useEffect runs every time memoizedIncrByName or memoizedIncrByEmail changes, you get into a loop where you render, create a new memoizedIncrByName, run the effect, call memoizedIncrByName, which renders again and repeats the process.

    If you want to keep the useEffect, i would recommend moving the functions inside of the effect and make the effect only depend on the sorting option:

    React.useEffect(() => {
      const incrByName = () => {
        let new_data = [...data].sort((a, b) => {
          if (b.name < a.name) return 1;
          if (b.name > a.name) return -1;
          return 0;
        });
        setData(new_data);
      };
      const incrByEmail = () => {
        let new_data = [...data].sort((a, b) => {
          if (b.email < a.email) return 1;
          if (b.email > a.email) return -1;
          return 0;
        });
        setData(new_data);
      }
    
      if (selectedSortingOption !== null) {
          if (selectedSortingOption.value === "name") {
            incrByName();
          } else if (selectedSortingOption.value === "email") {
            incrByEmail();
          }
        }
    }, [selectedSortingOption]);
    

    But i would actually recommend dropping the useEffect entirely. When you do the use effect approach, you end up having to render twice any time you want to change the sort: once to update the sort state, and then again when the useEffect updates the data state. Instead you can have the sorted list be a value that's calculated during rendering. For performance, you can wrap the calculation in a useMemo to skip the computation if nothing has changed:

    const [data, setData] = useState(/* the full unsorted array */);
    const [selectedSortingOption, setSelectedSortingOption] = useState(null);
    
    const sortedData = useMemo(() => {
      if (selectedSortingOption.value === "name") {
         return [...data].sort((a, b) => {
          if (b.name < a.name) return 1;
          if (b.name > a.name) return -1;
          return 0;
        });
      } else if (selectedSortingOption.value === "email") {
        return [...data].sort((a, b) => {
          if (b.email < a.email) return 1;
          if (b.email > a.email) return -1;
          return 0;
        })
      } else {
        return data;
      } 
    }, [data, selectedSortingOption]);
    
    // Use sortedData below here
    

    EDIT: for the addSingleEmployee function, you are mutating the state. Make sure to make a new array. So instead of this:1

    newData = data;
    newData.push(employee);
    setData(newData);
    

    Do this:

    const newData = [...data];
    newData.push(employee);
    setData(newData);