yudhiesh
yudhiesh

Reputation: 6799

Too many re-renders caused by a custom React Hook

I am loading data in from a Neo4J database using use-neo4j and have created a custom hook. It makes a query to the database and then converts the data to a format I can use for plotting the data.

It is a lot of code but basically neo4jToJSON converts the data from Neo4J, then I set it to the state, lastly I return the state to be used in other components.

Note:

Before this I was just returning neo4jToJSON and had no issues. But the moment I incorporate state into the function and return the state it throws this error.

Edit:

I have tried the suggested answer in the updated code below but now the data just returns undefined.

import React, { useState, useEffect } from "react";
import { useReadCypher } from "use-neo4j";

const useFetchData = () => {
  const [jsonData, setJsonData] = useState();
  // Query to Neo4j database to get all the relevant data for all the users
  const getAllUserData = // a query string that is quite long so I didn't include it 
  // Pass query to use-neo4j to get the data
  const { result } = useReadCypher(getAllUserData);

  const filters = [
    "name",
    "longitude",
    "latitude",
    "agreeableness",
    "openness",
    "neuroticism",
    "extraversion",
    "conscientiousness",
    "angry",
    "fear",
    "joy",
    "sadness",
    "love",
    "country",
    "question1",
    "question2",
    "question3",
    "question4",
    "question5",
    "question6",
    "question7",
    "question8",
    "question9",
    "question10",
    "question11",
    "question12",
    "question13",
    "question14",
    "question15",
    "question16",
    "question17",
    "question18",
    "question19",
    "question20",
    "question21",
    "question22",
    "question23",
    "question24",
    "question25",
    "question26",
    "question27",
    "question28",
    "question29",
    "question30",
    "question31",
    "question32",
    "question33",
    "question34",
    "question35",
    "question36",
    "question37",
    "question38",
    "question39",
    "question40",
    "question41",
  ];
  
  useEffect(() => {
    if (result) {
      const { records } = result;

      const neo4jToJSON =
        records &&
        records.map(({ _fields }) => {
          // User name
          const screenName = _fields[0];
          // Country of the user
          const country = _fields[2][0];
          // Create the object with these values as the keys:
          //
          // "name",
          // "longitude",
          // "latitude",
          // "agreeableness",
          // "openness",
          // "neuroticism",
          // "extraversion",
          // "conscientiousness",
          // "angry",
          // "fear",
          // "joy",
          // "sadness",
          // "love",
          // "country"
          const userPersonality = filters.reduce((acc, curr, idx) => {
            if (idx === 0) {
              // set the property name to the screenName
              acc[curr] = screenName;
            } else if (idx >= 1 && idx <= 12) {
              // if the index is in the range of 1 to 12
              // it belongs to _fields[1]
              // set the properties of _fields[1] to their respective values
              // values come in the form of strings so apply parseFloat on them
              acc[curr] = parseFloat(_fields[1][idx - 1]);
            } else if (idx === 13) {
              // set the property country
              acc[curr] = country;
            }
            return acc;
          }, {});
          const finalResults = filters.reduce((acc, curr, idx) => {
            const questions = _fields[3];
            if (idx > 13) {
              // Access the first element of the array of arrays which contains
              // the questions by idx - 14
              // The current value starts from idx 14 of filters which is
              // question1
              // So if idx is equal to 14:
              // idx - 14 will give the question for the first question which is
              // at index 0
              //
              // Ternary operator here checks if the array value is not empty if
              // it is not empty then set the value of the current question to be
              // the array of questions
              // Else set the value to be an empty array which is falsy
              questions[idx - 14]
                ? (acc[curr] = questions[idx - 14])
                : (acc[curr] = []);
            }
            return acc;
          }, userPersonality);
          return finalResults;
        });
      setJsonData(neo4jToJSON);
    }
  }, []);
  return { jsonData };
};

export default useFetchData

Upvotes: 0

Views: 212

Answers (3)

Emile Bergeron
Emile Bergeron

Reputation: 17430

While useEffect is one of the way to avoid calling state setter functions inside of the render cycle, in your case, useState isn't needed at all!

Compute on every render

It's an anti-pattern to use state for something that can be synchronously computed within render.

const FILTERS = [
  "name",
  "longitude",
  "latitude",
  // ...etc.
];

const computeData = ({ records } = {}) => {
  return (
    records &&
    records.map(({ _fields }) => {
      const screenName = _fields[0];
      const country = _fields[2][0];
      const userPersonality = filters.reduce((acc, curr, idx) => {
        if (idx === 0) {
          acc[curr] = screenName;
        } else if (idx >= 1 && idx <= 12) {
          acc[curr] = parseFloat(_fields[1][idx - 1]);
        } else if (idx === 13) {
          acc[curr] = country;
        }
        return acc;
      }, {});
      const questions = _fields[3];
      const finalResults = filters.reduce((acc, curr, idx) => {
        if (idx > 13) {
          questions[idx - 14]
            ? (acc[curr] = questions[idx - 14])
            : (acc[curr] = []);
        }
        return acc;
      }, userPersonality);
      return finalResults;
    })
  );
};

export default function useFetchData() {
  // Query to Neo4j database to get all the relevant data for all the users
  const getAllUserData = "?whatever"; // a query string
  // Pass query to use-neo4j to get the data
  const { result } = useReadCypher(getAllUserData);

  // no need for state here, just compute on every render.
  return { jsonData: computeData(result) };
}

Optimize heavy computations with useMemo

If you figure that some computation only happens once when the data is received and mostly never changes, and/or it's an heavy computation that could lead to a bottleneck, consider using useMemo to memoize the computation result and only re-compute if the input changes.

import { useMemo } from "react";

//...computeData, filters, etc. here...

export default function useFetchData() {
  // Query to Neo4j database to get all the relevant data for all the users
  const getAllUserData = "?whatever"; // a query string
  // Pass query to use-neo4j to get the data
  const { result } = useReadCypher(getAllUserData);

  // no need for state here, just memoize the result.
  const jsonData = useMemo(() => computeData(result), [result]);

  return { jsonData };
}

Also notice that in each example, I've moved some stuff around, so that the hook logic is easier to grasp and to isolate the computation logic.

Upvotes: 1

Martin
Martin

Reputation: 6146

You are calling a state setter synchronously from your render function. Never do this. That's what triggers the re-render. And then triggers it again, and again, and again.

const useFetchData = () => {
  const [jsonData, setJsonData] = useState();
  const { result } = useReadCypher(getAllUserData);

  if (result) {
    setJsonData(something);
  }

If you need to set some state when result changes you can useEffect

const useFetchData = () => {
  const [jsonData, setJsonData] = useState();
  const { result } = useReadCypher(getAllUserData);
  useEffect(() => { if (result) setJsonData(something) }, [result]);

Upvotes: 1

Hemakumar
Hemakumar

Reputation: 639

Move your logic into useEffect function

useEffect(() => {
    effect
    return () => {
        cleanup
    }
}, [input])

Like this

        useEffect(() => {
 // Query to Neo4j database to get all the relevant data for all the users
  const getAllUserData = // a query string that is quite long so I didn't include it 
  // Pass query to use-neo4j to get the data
  const { result } = useReadCypher(getAllUserData);
    if (result) {
                // Convert the data from Neo4j which comes in the form of an Object to
                // another Object that has keys which are elements in the array filters and
                // values which are the _fields from the Object from Neo4j
            
                const { records } = result;
            
                const neo4jToJSON =
                  records &&
                  records.map(({ _fields }) => {
                    // User name
                    const screenName = _fields[0];
                    // Country of the user
                    const country = _fields[2][0];
                    const userPersonality = filters.reduce((acc, curr, idx) => {
                      if (idx === 0) {
                        // set the property name to the screenName
                        acc[curr] = screenName;
                      } else if (idx >= 1 && idx <= 12) {
                        // if the index is in the range of 1 to 12
                        // it belongs to _fields[1]
                        // set the properties of _fields[1] to their respective values
                        // values come in the form of strings so apply parseFloat on them
                        acc[curr] = parseFloat(_fields[1][idx - 1]);
                      } else if (idx === 13) {
                        // set the property country
                        acc[curr] = country;
                      }
                      return acc;
                    }, {});
                    const finalResults = filters.reduce((acc, curr, idx) => {
                      const questions = _fields[3];
                      if (idx > 13) {
                        // Access the first element of the array of arrays which contains
                        // the questions by idx - 14
                        // The current value starts from idx 14 of filters which is
                        // question1
                        // So if idx is equal to 14:
                        // idx - 14 will give the question for the first question which is
                        // at index 0
                        //
                        // Ternary operator here checks if the array value is not empty if
                        // it is not empty then set the value of the current question to be
                        // the array of questions
                        // Else set the value to be an empty array which is falsy
                        questions[idx - 14]
                          ? (acc[curr] = questions[idx - 14])
                          : (acc[curr] = []);
                      }
                      return acc;
                    }, userPersonality);
                    return finalResults;
                  });
                setJsonData(neo4jToJSON);
              }
    }

Upvotes: 0

Related Questions