2

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.

shin
  • 333
  • 3
  • 11

2 Answers2

4

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.

Ravid
  • 293
  • 2
  • 9
  • 1
    @Antonio specifically said `useEffect` is still being run even when `item` does not change -- wouldn't that stop the infinite loop? – dhuang Jan 28 '21 at 18:21
  • 1
    The item does change. Because it's a new object altogether even if it's inner content's are the same. It's a new reference. – Lakshya Thakur Jan 28 '21 at 18:32
  • You're right, for some reason I didn't think about it. I guess I should use another state as dependency to trigger the update. I'll try that, thank you. – shin Jan 28 '21 at 18:34
1

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>
  );
}

codemonkey
  • 7,325
  • 5
  • 22
  • 36