6

I have a simple date picker component with buttons for preset date ranges.

date picker component with buttons for date range presets

The problem is in my useEffect: I'm using it to communicate initial state up on render, but of course React issues a warning ("useEffect has missing dependencies").

Is there a good pattern to do this?

Child:

const LAST_7 = "LAST_7";
let to, from, filter;

// figure out values for "from", "to", and "filter" (which is set to LAST_7 in case "from" and "to" are not in props)

const initial = {
    from,
    to,
    filter,
};

const [state, setState] = useState(initial);

useEffect(() => {
    props.onUpdate(from, to);
}, []);

const handleClick = (e) => {
    const filter = e.target.value;
    const { from, to } = getDatesFromFilterValue(filter);
    setState((prev) => ({ ...prev, from, to, filter }));

    props.onUpdate(from, to);
};

Parent:

const onDatesUpdate = (from, to) => {
    setState((prev) => ({ ...prev, from, to }));
};

// ...

<Child
    onUpdate={onDatesUpdate}
></Child>
montrealist
  • 5,593
  • 12
  • 46
  • 68
  • I doubt it has any thing to do with `useEffect(() => { props.onUpdate(from, to); }, []);` – Mechanic Apr 23 '20 at 19:44
  • @Leonardo the useEffect warning originates there, 100%. My issue is probably larger: lack of practice with common React patterns. :) – montrealist Apr 23 '20 at 20:31
  • Ignoring the warning leads to a [stale closure](https://dmitripavlutin.com/react-hooks-stale-closures/). If you would like the effect to run when `from` or `to` changes the simplest way to do that would be to not use an effect at all and just do: `setState((prev) => { props.onUpdate(from, to); return { ...prev, from, to, filter, }; });` If you only want to run `props.onUpdate(from, to);` on mount then you can tell the linter to ignore the dep with `// eslint-disable-next-line react-hooks/exhaustive-deps` one line before the warning. – HMR Apr 23 '20 at 20:46
  • @HMR I want to do both: once on mount (without user interaction), then every time `from` or `to` changes as result of user interaction. P.S.: Thank you for the link! Very interesting read. – montrealist Apr 23 '20 at 23:03

1 Answers1

3

eslint warnings are there to warn users about inapproriate use of useEffect. Since hooks depend a lot on closures its extremely important that we write them properly

Now the reason that eslint warns you for missing dependency is because you use onUpdate inside of useEffect

Now ESlint is good not not extremely intelligent to figure out what you as a developer want

Its absolutely allright to want the function to be called only on initial render. However ESlint doesn't know if you have intervals or subscriptions within the function which depend on the closure variables and thus warns you that onUpdate might need to be re-run whenever its recreated or its dependency changed

If you are absolutely sure that what you are writing is correct, you could disable the warning like

useEffect(() => {
    props.onUpdate(from, to);
    // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Now that your intention is to call the onUpdate on from and to change which you do via handleClick, it actually right to add them as dependency too

const [state, setState] = useState(initial);

useEffect(() => {
    props.onUpdate(from, to);
}, [from, to]);

const handleClick = (e) => {
    const filter = e.target.value;
    const { from, to } = getDatesFromFilterValue(filter);
    setState((prev) => ({ ...prev, from, to, filter }));
};

Now one last thing, you can add onUpdate as a dependency to useEffect if you write it with a useCallback in its parent to make sure that its only created when needs

const Parent = () => {
    const [state, setState]= useState({});

    const onUpdate = useCallback((from, to) => {
        setState(prev => ({
              // use functional setState here to get updated state using prev
              // and return the updated value
        }))
    }, [])

    ...
    return (
         <Child onUpdate={onUpdate} />
    )
}

Child

const [state, setState] = useState(initial);

useEffect(() => {
    props.onUpdate(from, to);
}, [from, to, onUpdate]); 
// Now that onUpdate is created on once, adding it to dependency will not be an issue

const handleClick = (e) => {
    const filter = e.target.value;
    const { from, to } = getDatesFromFilterValue(filter);
    setState((prev) => ({ ...prev, from, to, filter }));
};
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • Please let me know if this post helped you or not – Shubham Khatri May 16 '20 at 17:40
  • Not quite, no. I'm not absolutely sure what I am writing is correct so disabling the warning sounds risky. The `useCallback` approach doesn't work. I updated my code snippet to include the parent element code - all I'm doing is calling `setState` in it. Is it safe to disable the warning then? – montrealist May 16 '20 at 22:35
  • @montrealist yes it is safe to disable the warning with what you are doing. You must also note that with the useCallback approach you must have an empty dependency . Also you may choose to avoid from and to in dependency to useEffect if you only want to execute it once. But its better to disable the wanring in such as case – Shubham Khatri May 17 '20 at 06:05