-1

In my React Project I have one component that has 2 child components

  1. Search Form
  2. Employee Projects List (loads only once)

I would like to load employee projects only once on page load. Then every time search form values change I would like to filter the table data. The problem is whenever I change search inputs, it causes too many re-renders error

Too many re-renders. React limits the number of renders to prevent an infinite loop.

I have checked other solutions, but nothing helped me to figure out this. Please checkout the sample in CodeSandBox

Pavan Jadda
  • 4,306
  • 9
  • 47
  • 79
  • Usually means an error in your architecture. For example if you use a useSelector hook and it always returns an new instance. or if you "sprea ...{}" your objects in your reducers which effectively creates a new instance. In your sample projec the biggest problem is that you create a new instance of "data" everytime you change it. and useEffect should be used with caution – Jonathan Alfaro Jun 18 '21 at 22:08
  • @JonathanAlfaro What do you suggest for this kind of requirements? Because this is pretty much common in enterprise apps – Pavan Jadda Jun 18 '21 at 22:21
  • I build and maintain really complex react apps in which rendering is crucial... This is how I do it. Each component is responsible for its own state. I dont use local state or useEffect. In stead I use a redux store maybe saga and/or DMV. I dont pass reference types as properties only value types. I use useSelector hook that only return value types and use one selector per property instead of returning full objects. In my reducers I never "spread ...{}" objects instead I assign individual properties. I use React Dev Tools with the rendering viewer turned on when I test – Jonathan Alfaro Jun 18 '21 at 22:38
  • That way I can see what is rendering. With this few tips (more or less) I keep apps that have hundreds or perhaps thousands of components rendered on a single screen working like a swiss clock. – Jonathan Alfaro Jun 18 '21 at 22:40
  • Use smaller compoents that render smaller pieces and that do not depend on the parent for data (props) – Jonathan Alfaro Jun 18 '21 at 22:40
  • Also for objects that are lists instead of a DataGrid or other prebaked.... I create a "EmpoyeeRow" components which takes a primary key so be able to retrieve the data it needs to render from the redux store. I render my own lists instead of relying on someone elses "grid" – Jonathan Alfaro Jun 18 '21 at 22:45
  • @JonathanAlfaro Unless you're using Immer or ReduxToolkit or some other immutable library that abstracts away the copying I just don't see how "In my reducers I never "spread ...{}" objects instead I assign individual properties." would even work for you in React. React reconciliation works via shallow reference equality checks ***because*** you return ***new*** values or object references when updating state. Why should `useEffect` be used with caution? Care yes, but caution? – Drew Reese Jun 19 '21 at 00:08
  • @DrewReese React does a "==" check which means a reference check for refererence types and an equality check for value types.... So if you ever use spreads or immer or anyother library that "recreates" the references it will trigger a re-render of any component that returns "references(objects)" from a useSelector. So the key is to always return actual values NOT references from useSelector that way react only re-renders when the value of a property has changed. – Jonathan Alfaro Jun 19 '21 at 03:55
  • @DrewReese I am the technical lead for team that works with highly complex react apps that render thousands of components in a single screen. Rendering is something we do with scalpel precission because if we ever do it wrong the apps would crash from the sheer number of components that would re-render. – Jonathan Alfaro Jun 19 '21 at 03:59
  • @DrewReese furthermore. Our components look like this.... if the component lets say handles a "Customer" object. We would have a useSelector for the FirstName that returns a string, another useSelector for the LastName and another for the Age... We would NEVER have a single useSelector for the whole Customer object. This means that our Customer.tsx component would ONLY re-render if the value of one those properties changed... this means that simple "spread ...{}" in a reducer would not trigger a re-render if the property values did not change – Jonathan Alfaro Jun 19 '21 at 04:02
  • @DrewReese because of this we never need an immutability library like immer. We just make sure to never use reference types in our props or as return of any useSelector. This has another "side effect" which is beneficial. If any component is taking care of "too many" fields then it needs to be broken down into small components. this means that our screens render very small portions at any given time. If we followed the recommendations that are "standard" in blogs and other internet sources about "immutability" our apps would be impossible and would not exist. – Jonathan Alfaro Jun 19 '21 at 04:07
  • @JonathanAlfaro See [Equality Comparison and Sameness](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness) and [React Reconciliation](https://reactjs.org/docs/reconciliation.html) and [shallowEqual](https://github.com/facebook/react/blob/0e100ed00fb52cfd107db1d1081ef18fe4b9167f/packages/shared/shallowEqual.js#L18-L50). React uses strict equality. It seems you have a strong opinion and understanding of OP's issue though; can you provide a technical answer that provides value to the community instead of talking about what you do at work? – Drew Reese Jun 19 '21 at 04:42
  • @DrewReese I understand React Reconciliation. And that is exactly why we never return objects from useSelectors or use "spread ...{}" in reducers. Precisely to avoid re-renders. So unless you want your entire screen to constantly render on every property change you need to be granular about changes and not use "spread". – Jonathan Alfaro Jun 19 '21 at 06:23
  • @DrewReese As for the help to the OP. I already pointed out how he can fix his issue by doing the following: 1 do not use local state but rather a redux store. 2 forget about useEffect unless you truly understand the dependencies parameter. 3 do not pass around objects in props like he is doing with the "data" array being passed to the "DataGrid". 4 He needs to make smaller components like "EmployeeRow.tsx" which uses a key to pull the values of properties using useSelector. So my original recommendation has not changed. – Jonathan Alfaro Jun 19 '21 at 06:24
  • @DrewReese there is one thing I want to clarify Hooks do not use strict equality "===" they use Object.is algorithm which is slightly different. shallowEquals is a deprecated add-on. The Hooks Documentation: https://reactjs.org/docs/hooks-reference.html – Jonathan Alfaro Jun 19 '21 at 06:49
  • @JonathanAlfaro Can't we do it without Redux? I mean do we need to use Redux for such small use case? – Pavan Jadda Jun 20 '21 at 03:37
  • 1
    The real problem here is to "share" state. The moment you "share" state in a hierarchy of components basically you tell react "render the whole tree every time there is a change". Setting up a REDUX store is not complicated and actually it is probably the most common way to use React. Reducers and data stores are an integral part of any react project. Specially business apps. – Jonathan Alfaro Jun 21 '21 at 01:02
  • 1
    Thanks @JonathanAlfaro for the suggestions. Up on further research and based on your suggestion I incorporated Redux into my App. It works fine now without any issues and in fact it made state management easy across the app. – Pavan Jadda Jul 03 '21 at 01:06

1 Answers1

1

It re-renders because you are passing through props the search form state, and when you change that state (typing in the search form) the component re-renders and execute all the if statement you have inside it, so it run's the setData function triggering another re-render because you change the internal state of that component and then start an infinite loop of renders because every time it enters to the if statements it runs the setData function with a different array argument (remember JavaScript treats all none basic types by references so every time data.filter() executes, it may return same array a similar array but its other reference, so for JavaScript its not the same array). To fix this you can put all if statements inside other useEffect hook like this:

useEffect(() => {
    // If statemenst
    if (
    props.formik.values.projectName !== undefined &&
    props.formik.values.projectName !== null &&
    props.formik.values.projectName !== ""
  ) {
    setData(data.filter((employeeProject) => employeeProject.projectName));
  }

  if (
    props.formik.values.projectType !== undefined &&
    props.formik.values.projectType !== null &&
    props.formik.values.projectType !== ""
  ) {
    setData(data.filter((employeeProject) => employeeProject.projectType));
  }
}, [props.formik.values.projectName, props.formik.values.projectType])

Aldo this code fix the error, will not work as intended because, it will filter the list, and when you get an empty list there is no way it fill again, so the grid will remain empty until you refresh the page manually, so you should filter the list on the JSX without changing the state/list like:

export default function EmployeeProjects(props: any) {
  const [data, setData] = useState<EmployeeProject[]>([]);

  useEffect(() => {
    let employeeProjects: EmployeeProject[] = [];
    employeeProjects.push(new EmployeeProject(1, "React", "Frontend"));
    employeeProjects.push(new EmployeeProject(2, "Angular", "Frontend"));
    employeeProjects.push(new EmployeeProject(3, "Vue", "Frontend"));
    employeeProjects.push(new EmployeeProject(4, "Java", "Backend"));
    employeeProjects.push(new EmployeeProject(5, "C#", "Backend"));
    employeeProjects.push(new EmployeeProject(6, "Python", "Backend"));
    setData(employeeProjects);
  }, []);
  return (
    <div style={{ marginTop: "20px" }}>
      <DataGrid
        autoHeight={true}
        columns={[
          { field: "id", headerName: "ID", flex: 200 },
          { field: "projectName", headerName: "Project Name", flex: 200 },
          { field: "projectType", headerName: "Project Type", flex: 200 }
        ]}
        rows={data.filter(
          (employeeProject) =>
            employeeProject.projectName.includes(
              props.formik.values.projectName
            ) ||
            employeeProject.projectName.includes(
              props.formik.values.projectType
            )
        )}
      />
    </div>
  );
}