shin
shin

Reputation: 343

useEffect with async function being called in a loop

My situation is this:

export default function Component({ navigation }) {
    const [ item, setItem ] = useState([]);
    
    useEffect(() => {
        AsyncStorage.getItem('someItem')
        .then(data => JSON.parse(data))
        .then(jsonData => {
            setItem(jsonData);
        })
        .catch(error => {});
    }, [item]);

The problem is that useEffect seems to be called in a loop, even when "item" doesn't change. If I remove item from the dependencies array, it gets called once only and when item changes the component does not re-render. Is there a solution?

Solved like this:

export default function Component({ navigation }) {
        const [ item, setItem ] = useState([]);
        const [ update, setUpdate ] = useState(false);

        const handleUpdate = () => {
            setUpdate(!update);
        }
        
        useEffect(() => {
            AsyncStorage.getItem('someItem')
            .then(data => JSON.parse(data))
            .then(jsonData => {
                setItem(jsonData);
            })
            .catch(error => {});
        }, [update]);

And then calling handleUpdate (or passing it to a child component and letting the child call it) when I want to update item's state.

Upvotes: 2

Views: 989

Answers (2)

codemonkey
codemonkey

Reputation: 7935

The issue is the same described in this post. When you use an object as a dependency, useEffect thinks it's different on every render. Since you use an array, which is an object, you get the infinite loop. Let's say your state variable was instead a primitive type like a string or a number, then it would not keep rendering as useEffect would be able to figure out that the value has not changed.

So, a possible solution in your specific case, because there is a JSON string being returned, could be using that JSON string as a parallel state variable to check for changes.

Consider something like this:

const simulateCall = () => Promise.resolve('["ds","1234sd","dsad","das"]')

export default function App() {
  const [state, setArrayState] = React.useState([])
  const [stringState, setStringState] = React.useState('')

  React.useEffect(() => {
    simulateCall()
    .then(data => {
      setStringState(data);
      setArrayState(JSON.parse(data));
    })
    .catch(error => console.log(error));
}, [stringState]);


  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      {state.map((item, i) => <div key={i}>{item}</div>)}

    </div>
  );
}

Upvotes: 1

Ravid
Ravid

Reputation: 303

You have an infinite loop: The second argument you send to useEffect() is an array of dependencies. Every time one of these dependencies change - the first argument to useEffect() which is a callback will be invoked. So here you do:

Every time item changes - run code that changes item (because the callback sets value with setItem)

notice: useEffect will also invoke the callback sent to it once at first.

Upvotes: 4

Related Questions