1

In my react functional component, I have a function that is executed to toggle the checkboxes as selected or unselected. It works fine.

The function (inside Parent component) goes like this:

const ParentComponent = () => {

const { state, dispatch } = useContext(FilterContext);

const onChange = useCallback((value) => {
(item) => {
  const { name, selected } = item;
 
  // this "name" comes from another sibling component on the mount, and then from child component (sideAccordian) on click

  const selectedFilters = state.identifiers_checkbox.selected_checkboxes.find(
    (obj) => obj === name
  );

  const selected_checkboxes = selectedFilters
    ? state.identifiers_checkbox.selected_checkboxes.filter(
        (obj) => obj !== name
      )
    : [...state.identifiers_checkbox.selected_checkboxes, name];

  const toggled_checkboxes = {
    selected_checkboxes,
    options_checkbox: [
      ...state.identifiers_checkbox.options_checkbox.map((filter) => {
        return {
          ...filter,
          options: filter.options.map((option, i) => {
            return name === option.name
              ? {
                  ...option,
                  selected,
                }
              : option;
          }),
        };
      }),
    ],
  };

  dispatch({
    type: "update-checkboxes",
    payload: {
      checkboxSelected: true,
      selected_checkboxes: toggled_checkboxes.selected_checkboxes,
      options_checkbox: toggled_checkboxes.options_checkbox,
    },
  });
},
[
  dispatch,
  state.identifiers_checkbox.options_checkbox,
  state.identifiers_checkbox.selected_checkboxes,
]);

  useEffect(() => {
    if (info.prevui === "intervention") {
      if (state.identifiers_checkbox.options_checkbox.length > 1) {
        onChange({ name: info.abbr, selected: 1 });
      }
    }
  }, [
    info.abbr,
    info.prevui,
    dispatch,
    state.identifiers_checkbox.options_checkbox.length,
    // onChange,
  ]);


return (
       <div>
        {
            state.identifiers_checkbox.options_checkbox.length > 1 ? (
             <SideAccordian
               filterstate={state.identifiers_checkbox}
               onChange={onChange}
             />
             ) : (
                <h6 style={{ textAlign: "center" }}>No Options Available</h6>
             )
         }
        </div>
    )
}

Child Component

export default function SideAccordian(props) {
    const [options, setOptions] = useState([]);
    const classes = useStyles();

    useEffect(() => {
        setOptions(props.filterprop);
    }, [props]);

    return (
        <div className={classes.root}>
            {
                options.map((ele, id) => (
                    <Accordion key={id} defaultExpanded={true}>
                        <AccordionSummary
                            key={ele.key}
                            expandIcon={<ExpandMoreIcon />}
                            aria-controls="panel1a-content"
                            id="panel1a-header"
                        >
                        </AccordionSummary>
                        {ele.options.map((item, i) => {
                            return (
                                <AccordionDetails key={i}>
                                    <IndeterminateInput
                                        key={i}
                                        id={i}
                                        name={item.name}
                                        value={item.label}
                                        selected={item.selected}
                                        onChange={props.onChange}
                                    />
                                </AccordionDetails>
                            )
                        })}
                    </Accordion>
                ))
            }
        </div>
    );
}

Additionally, I need to call onChange() function as the component mounts & then automatically mark any of the checkboxes as selected according to the name of the checkbox passed as an argument.

The handler function onChange() works fine on clicking the checkbox, but when trying to invoke it on mount on the basis of a particular condition, it doesn't work.

As soon as the component mounts:

If onChange() is included in the dependency array, it throws Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array or one of the dependencies changes on every render.

If removed from the dependency array, it works but keeps throwing error each Hook useEffect has a missing dependency: 'onChange'. Either include it or remove the dependency array.

Since, I need to execute this function only once on render also i.e. (when info.prevui === intervention' & info.prevui does not change every time ), this error behavior of useEffect() is out of my understanding.

Can anyone please help out to understand and provide any working hint/solution for the same?

Thanks

program_bumble_bee
  • 439
  • 2
  • 25
  • 57
  • the error indicates that state is set in an infinite loop. maybe you can try implementing componentDidUpdate function? – Akino Archilles Oct 06 '20 at 16:25
  • @AkinoArchilles Since I am using functional component, using componentDidUpdate() has the same effect as hook useEffect() – program_bumble_bee Oct 06 '20 at 16:32
  • 2
    If `handleCheckboxChange` dispatches an action to update state that is a dependency in the effect which invokes `handleCheckboxChange` then that is likely the cause of your looping. You mention you want the effect to run when the component mounts, it does, but since you've added dependencies it will run any time a dependency updates. Does issue repro if you use an empty dependency array (`[]`)? – Drew Reese Oct 06 '20 at 16:42
  • @DrewReese. Nope, if I pass empty dependency array [ ], the looping stops. But then as usual, the warning msgs occur: `React Hook useEffect has missing dependencies: 'handleCheckboxChange' and 'info.abbr'. Either include them or remove the dependency array react-hooks/exhaustive-deps`. And also, the checkbox which I need to get selected, e.g., `label1` (originally it is being passed from state 'info.abbr'), does not get `checked: true` – program_bumble_bee Oct 06 '20 at 16:52
  • 1
    You *can* ignore warnings if you *truly* want an effect to run only once on mount. It sounds like the data you want though isn't immediately available when the component mounts, but rather comes a render cycle (or 2 or 3) later. – Drew Reese Oct 06 '20 at 16:56
  • @DrewReese Ignoring the warnings is legitimate in case of react hooks? I mean, under what conditions, should this be followed? – program_bumble_bee Oct 06 '20 at 16:58
  • 1
    Completely, they are *just* warnings, afterall, but like any warning, you should use caution when deciding to ignore it. The most typical use-case I consider is the `componentDidMount` condition where you know with certainty you only want a hook to run once total during the life of the component and all data is available at *that* time. The linter can't possibly know your use-case so it is a very general blanket check. – Drew Reese Oct 06 '20 at 17:01
  • Your hint did work. I changed like this: `useEffect(() => { if (info.prevui === 'intervention') { if (state.identifiers_checkbox.options_checkbox.length > 1) { handleCheckboxChange(info.abbr); } } }, [info.abbr, state.identifiers_checkbox.options_checkbox.length, info.prevui]);`. The functionality I required did work, but now I am just left with a warning : `React Hook useEffect has a missing dependency: 'handleCheckboxChange'. Either includes it or remove the dependency array`, which I think I can ignore now :D – program_bumble_bee Oct 06 '20 at 17:04
  • If your IDE doesn't suggest it, `// eslint-disable-next-line react-hooks/exhaustive-deps` a line above the dependency array will stifle the warning, but again, caution should be used because if you ever update the hook logic and dependencies *do* change this will silently swallow them. You may also try a more specific dependency, `state.identifiers_checkbox.options_checkbox.length`? – Drew Reese Oct 06 '20 at 17:12
  • 1
    You're right. I included state.identifiers_checkbox.options_checkbox.length already in the useEffect() – program_bumble_bee Oct 06 '20 at 17:29
  • @DrewReese The problem still exists somehow. Can you help a bit in-depth? – program_bumble_bee Oct 08 '20 at 07:27
  • Is it possible for you to reproduce this issue into a *running* codesandbox? It doesn't need to be your entire codebase, but just enough to reproduce the render looping. – Drew Reese Oct 08 '20 at 07:29
  • Sure. I will create one – program_bumble_bee Oct 08 '20 at 07:29
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/222700/discussion-between-bubble-cord-and-drew-reese). – program_bumble_bee Oct 08 '20 at 07:42

1 Answers1

2

The reason why there's an infinite render loop in your code when you include the onChange handler in the effect dependencies is because when whenever the handleCheckboxChange function is called, the handleCheckboxChange function is redefined due to state.identifiers_checkbox.selected_checkboxes and state.identifiers_checkbox.options_checkbox in the dependency array since both of them are updated in the redux state every time handleCheckboxChange is called.

Then, when you pass onChange into the dependency array of the useEffect, it runs the effect every time the onChange changes which is every time handleCheckboxChange is run - hence the infinite render loop.

So, the easiest way of fixing this would be to put onChange into a ref in the child component so you don't need to include it in the dependency array, that way it remains up-to-date in the effect, and doesn't cause an infinite render loop of doom.

  const onChangeRef = useRef(onChange);
  useEffect(()=>{onChangeRef.current = onChange},[onChange])
  useEffect(() => {
    if (info.prevui === "intervention") {
      if (state.identifiers_checkbox.options_checkbox.length > 1) {
        onChangeRef.current({ name: info.abbr, selected: 1 });
      }
    }
  }, [
    info.abbr,
    info.prevui,
    dispatch,
    state.identifiers_checkbox.options_checkbox.length,
  ]);

For an example of this: The codesandbox you posted in the chat (with the handleCheckBoxChange in the useEffect which causes the infinite loop as handleCheckboxChange is created everytime the component re-renders: https://codesandbox.io/s/react-functional-component-forked-g7vh1

  useEffect(() => {
    handleCheckboxChange("ornge");

    setTimeout(() => {
      handleCheckboxChange("tmto");
    }, 5000);
  }, [handleCheckboxChange]);

Then fixed: https://codesandbox.io/s/infinite-loop-fix-4y6zm?file=/src/index.js

  const changeRef = useRef(handleCheckboxChange);
  useEffect(() => {
    changeRef.current = handleCheckboxChange;
  }, [handleCheckboxChange]);
  useEffect(() => {
    changeRef.current("ornge");

    setTimeout(() => {
      changeRef.current("tmto");
    }, 5000);
  }, []);

Elaboration as requested:

React's useRef hook returns a value that is mutable and will always remain referentially stable. Essentially for each render, changeRef always points to the same object in memory as it did before, thus what it refers to is stable. This is different than most things, because for instance []===[] returns false because the arrays refer to different objects in memory.

useRef is known by React to create these mutable referentially stable objects, so they don't make it a requirement to pass them into the dependency arrays of any of the hooks as they know it isn't necessary.

By stuffing the onChange function into a ref and then making sure to update that ref whenever onChange changes via shallow equality, now we have a way to refer to the latest version of onChange without having to worry about adding it to the dependencies of the effect.

The reason why React has these dependency arrays to re-run the various hooks is because of Closures - each time the function component renders, all the various functions and objects are recreated (although useMemo, useCallback, and useState allow for certain things to not be recreated every time). When a function is created, then it has a way to refer to all the variables in the scope that the function can see.

That's why something like the following works. Even though test goes out of scope before funct is called, funct has a closure around it so it can always see it as long as funct is in scope somewhere.

let funct;
{
 const test = 5;
 funct = ()=>{console.log(test)}
}
funct(); // logs 5

These closures are what React Hooks rely on to work as they do. It's also the reason why the eslint plugin react hooks has the exhaustive-deps piece, because if you don't include something, then by the time the value is used, it'll become stale.

React's useRef hook allows us to bypass the closures because the refs are able to be mutated, so using ref.current gives us whatever value has been put in there regardless of when the closure was created.

Zachary Haber
  • 10,376
  • 1
  • 17
  • 31
  • I was just refering to the component that has the `onChange` mentioned in the post as a child component, since you didn't post any component names or full code, I just had to guess. – Zachary Haber Oct 13 '20 at 14:54
  • My bad. I've updated my parent and child components accordingly. If now they help you out my query about passing in the child component – program_bumble_bee Oct 13 '20 at 15:14
  • If I am not wrong, both of these code sandbox links are still giving error for missing handler ? – program_bumble_bee Oct 13 '20 at 15:25
  • The second link's warning message is: `The 'handleCheckboxChange' function makes the dependencies of useEffect Hook (at line 66) change on every render.` which is basically them trying to be helpful in saying that the `useEffect` that I'm using to update the `changeRef` is running every time (which is intentional in this case) as it's a demonstration of things. – Zachary Haber Oct 13 '20 at 16:04
  • It worked. Thanks. The issue is now resolved! Not getting the dependency error anymore. Can you please help elaborate bitmore on how using ref worked exactly for this? – program_bumble_bee Oct 13 '20 at 17:01
  • @bubble-cord, I've added a bunch of explanation and a couple links for you – Zachary Haber Oct 13 '20 at 17:25