Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Referencing outdated state in React useEffect hook

I want to save state to localStorage when a component is unmounted. This used to work in componentWillUnmount.

I tried to do the same with the useEffect hook, but it seems state is not correct in the return function of useEffect.

Why is that? How can I save state without using a class?

Here is a dummy example. When you press close, the result is always 0.

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function Example() {
  const [tab, setTab] = useState(0);
  return (
    <div>
      {tab === 0 && <Content onClose={() => setTab(1)} />}
      {tab === 1 && <div>Why is count in console always 0 ?</div>}
    </div>
  );
}

function Content(props) {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // TODO: Load state from localStorage on mount

    return () => {
      console.log("count:", count);
    };
  }, []);

  return (
    <div>
      <p>Day: {count}</p>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => props.onClose()}>close</button>
    </div>
  );
}

ReactDOM.render(<Example />, document.querySelector("#app"));

CodeSandbox

like image 296
roeland Avatar asked Dec 05 '18 13:12

roeland


People also ask

How do I get the past state in React hooks?

While there's currently no React Hook that does this out of the box, you can manually retrieve either the previous state or props from within a functional component by leveraging the useRef , useState , usePrevious , and useEffect Hooks in React.

Can we change state in useEffect hook?

@alaboudi Yes.. as Shubham Khatri said it will render again. but you can skip calling your effect after the re-rendering using the second argument refer reactjs.org/docs/…

How do you trigger a useEffect on state change?

Passing no 2nd argument causes the useEffect to run every render. Then, when it runs, it fetches the data and updates the state. Then, once the state is updated, the component re-renders, which triggers the useEffect again.

How do I fix React hook useEffect has missing dependencies?

The warning "React Hook useEffect has a missing dependency" occurs when the useEffect hook makes use of a variable or function that we haven't included in its dependencies array. To solve the error, disable the rule for a line or move the variable inside the useEffect hook.


2 Answers

I tried to do the same with the useEffect hook, but it seems state is not correct in the return function of useEffect.

The reason for this is due to closures. A closure is a function's reference to the variables in its scope. Your useEffect callback is only ran once when the component mounts and hence the return callback is referencing the initial count value of 0.

The answers given here are what I would recommend. I would recommend @Jed Richard's answer of passing [count] to useEffect, which has the effect of writing to localStorage only when count changes. This is better than the approach of not passing anything at all writing on every update. Unless you are changing count extremely frequently (every few ms), you wouldn't see a performance issue and it's fine to write to localStorage whenever count changes.

useEffect(() => { ... }, [count]);

If you insist on only writing to localStorage on unmount, there's an ugly hack/solution you can use - refs. Basically you would create a variable that is present throughout the whole lifecycle of the component which you can reference from anywhere within it. However, you would have to manually sync your state with that value and it's extremely troublesome. Refs don't give you the closure issue mentioned above because refs is an object with a current field and multiple calls to useRef will return you the same object. As long as you mutate the .current value, your useEffect can always (only) read the most updated value.

CodeSandbox link

const {useState, useEffect, useRef} = React;

function Example() {
  const [tab, setTab] = useState(0);
  return (
    <div>
      {tab === 0 && <Content onClose={() => setTab(1)} />}
      {tab === 1 && <div>Count in console is not always 0</div>}
    </div>
  );
}

function Content(props) {
  const value = useRef(0);
  const [count, setCount] = useState(value.current);

  useEffect(() => {
    return () => {
      console.log('count:', value.current);
    };
  }, []);

  return (
    <div>
      <p>Day: {count}</p>
      <button
        onClick={() => {
          value.current -= 1;
          setCount(value.current);
        }}
      >
        -1
      </button>
      <button
        onClick={() => {
          value.current += 1;
          setCount(value.current);
        }}
      >
        +1
      </button>
      <button onClick={() => props.onClose()}>close</button>
    </div>
  );
}

ReactDOM.render(<Example />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>
like image 155
Yangshun Tay Avatar answered Oct 18 '22 02:10

Yangshun Tay


This will work - using React's useRef - but its not pretty:

function Content(props) {
  const [count, setCount] = useState(0);
  const countRef = useRef();

  // set/update countRef just like a regular variable
  countRef.current = count;

  // this effect fires as per a true componentWillUnmount
  useEffect(() => () => {
    console.log("count:", countRef.current);
  }, []);
}

Note the slightly more bearable (in my opinion!) 'function that returns a function' code construct for useEffect.

The issue is that useEffect copies the props and state at composition time and so never re-evaluates them - which doesn't help this use case but then its not what useEffects are really for.

Thanks to @Xitang for the direct assignment to .current for the ref, no need for a useEffect here. sweet!

like image 6
Andy Lorenz Avatar answered Oct 18 '22 04:10

Andy Lorenz