7

I have the following component:

import React, { useState, useEffect } from "react";

const App = () => {
    const [data, setData] = useState<null | any[]>(null);
    const [checked, setChecked] = useState(false);
    const [loading, setLoading] = useState(false);

    useEffect(() => {
        setLoading(true);

        (async () => {
            if (data) {
                // Could do something else here if data already exsisted
                console.log("Data exists already");
            }

            const ret = await fetch("https://jsonplaceholder.typicode.com/users?delay=1000", { cache: "no-store" });
            const json = await ret.json();
            setData(json);
            setLoading(false);
        })();

    }, [checked]);

    return (
        <>
            <h1>useEffect Testing {loading && " ⌛"}</h1>
            <hr />
            <input type="checkbox" checked={checked} onChange={(e) => setChecked(e.target.checked)} />
            <pre style={{ fontSize: "0.8em" }}>{JSON.stringify(data)}</pre>
        </>
    );
};

export default App;

Currently, my if(data) is pointless however if I wanted to check the current data state and then run an asynchronous function based on the state the eslint(react-hooks/exhaustive-deps) tells me the hook is missing the data dependency, which it is. The problem comes when I add data to the dependency list causing an infinite loop.

Any idea how to resolve this? It feels like it should be fairly simple pattern however everything I find recomends using the extended setState(prev => prev + 1) overload which doesn't help me in this case.

hedgecox
  • 73
  • 1
  • 3
  • 1
    Ignore the linter. That's a *warning*, not an *error*, and if you alter a dependency in a useEffect hook it will loop endlessly, always. Sorry if I sound frustrated it's not your fault, we get a lot of questions like this one. That exhaustive deps linter rule is *usually* a false positive, except when it isn't,, which is why the React team can't just get rid of it. – Jared Smith Jul 30 '20 at 14:11
  • @JaredSmith Bad idea, situation may arise when the `useEffect` remembers the wrong `data` in its closure – Nikita Madeev Jul 30 '20 at 14:14
  • @NikitaMadeev you think it's a better idea to create an infinite loop? – Jared Smith Jul 30 '20 at 14:14
  • 1
    @JaredSmith of course, `console.log("Data exists already"); return;` Why use a linter at all if you ignore it? – Nikita Madeev Jul 30 '20 at 14:16
  • 1
    @NikitaMadeev sorry maybe I was unclear: I'm suggesting that the OP ignore the linter *in this specific case* when following its advice creates an infinite loop, not in general. – Jared Smith Jul 30 '20 at 14:17
  • 1
    A good idea is not to touch data exactly in this method. If OP wants to check it, it's better to check before starting the effect – Drag13 Jul 30 '20 at 14:21

1 Answers1

3

You are setting data and also reading it in the effect. This will cause you an infinite loop unless you manually stop it. Here are a few solutions.

Return if there is data instead of modify it:

useEffect(() => {
    setLoading(true);

    (async () => {
        if (data) {
            // Could do something else here if data already exsisted
            console.log("Data exists already");
            return;
        }

        const ret = await fetch("https://jsonplaceholder.typicode.com/users?delay=1000", { cache: "no-store" });
        const json = await ret.json();
        setData(json);
        setLoading(false);
    })();

}, [checked, data]);

Remove your reliance on data in the effect that sets it (what I'd do):

useEffect(() => {
    setLoading(true);

    (async () => {
        const ret = await fetch("https://jsonplaceholder.typicode.com/users?delay=1000", { cache: "no-store" });
        const json = await ret.json();
        setData(json);
        setLoading(false);
    })();

}, [checked]);

useEffect(() => {
        if (data) {
            // Could do something else here if data already exsisted
            console.log("Data changed and exists!");
        }
}, [data]);

Or you can manually do something that stops the infinite loop.

useEffect(() => {
    if (loading || data) {
      console.log('Do something with loading or data, but do not modify it!')
      return;
    }

    setLoading(true);

    (async () => {
        const ret = await fetch("https://jsonplaceholder.typicode.com/users?delay=1000", { cache: "no-store" });
        const json = await ret.json();
        setData(json);
        setLoading(false);
    })();
}, [checked, data]);
Diesel
  • 5,099
  • 7
  • 43
  • 81
  • The two useEffects was exactly what I needed! The code I attached was a hugely simplified version of what I was actually doing however separating the two effects allows me to check and action the changes without causing the infinite loop. – hedgecox Jul 31 '20 at 08:55
  • Yah, and separating concerns is a principal of React with the transition to useEffect. Mark this as accepted if it works please! Glad it helped. – Diesel Jul 31 '20 at 13:31