Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I access state in an useEffect without re-firing the useEffect?

I need to add some event handlers that interact with an object outside of React (think Google Maps as an example). Inside this handler function, I want to access some state that I can send through to this external object.

If I pass the state as a dependency to the effect, it works (I can correctly access the state) but the add/remove handler is added every time the state changes.

If I don't pass the state as the dependency, the add/remove handler is added the appropriate amount of times (essentially once), but the state is never updated (or more accurately, the handler can't pull the latest state).

Codepen example:

Perhaps best explained with a Codepen: https://codepen.io/cjke/pen/dyMbMYr?editors=0010

const App = () => {
  const [n, setN] = React.useState(0);

  React.useEffect(() => {
    const os = document.getElementById('outside-react')
    const handleMouseOver = () => {
      // I know innerHTML isn't "react" - this is an example of interacting with an element outside of React
      os.innerHTML = `N=${n}`
    }
    
    console.log('Add handler')
    os.addEventListener('mouseover', handleMouseOver)
    
    return () => {
      console.log('Remove handler')
      os.removeEventListener('mouseover', handleMouseOver)
    }
  }, []) // <-- I can change this to [n] and `n` can be accessed, but add/remove keeps getting invoked

  return (
    <div>
      <button onClick={() => setN(n + 1)}>+</button>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));

Summary

If the dep list for the effect is [n] the state is updated, but add/remove handler is added/removed for every state change. If the dep list for the effect is [] the add/remove handler works perfectly but the state is always 0 (the initial state).

I want a mixture of both. Access the state, but only the useEffect once (as if the dependency was []).


Edit: Further clarification

I know how I can solve it with lifecycle methods, but not sure how it can work with Hooks.

If the above were a class component, it would look like:


class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = { n: 0 };
  }

  handleMouseOver = () => {
    const os = document.getElementById("outside-react");
    os.innerHTML = `N=${this.state.n}`;
  };
  
  componentDidMount() {
    console.log("Add handler");
    const os = document.getElementById("outside-react");
    os.addEventListener("mouseover", this.handleMouseOver);
  }

  componentWillUnmount() {
    console.log("Remove handler");
    const os = document.getElementById("outside-react");
    os.removeEventListener("mouseover", handleMouseOver);
  }

  render() {
    const { n } = this.state;

    return (
      <div>
        <strong>Info:</strong> Click button to update N in state, then hover the
        orange box. Open the console to see how frequently the handler is
        added/removed
        <br />
        <button onClick={() => this.setState({ n: n + 1 })}>+</button>
        <br />
        state inside react: {n}
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));

Noting how the add/remove handler is only added once (obviously ignoring the fact that the App component isn't unmounted), despite the state change.

I'm looking for a way to replicate that with hooks

like image 849
Chris Avatar asked Aug 03 '20 05:08

Chris


People also ask

Can I access state in useEffect?

With the empty dependency array the useEffect will be run only once. And it will access the state from that one run. So it will have a reference from the logAbandonListing function from this moment. This function will access the state from this moment also.

Can I set state inside a useEffect hook?

It's ok to use setState in useEffect you just need to have attention as described already to not create a loop. The reason why this happen in this example it's because both useEffects run in the same react cycle when you change both prop.

Does useEffect run on state change?

Use the useEffect hook to listen for state changes in React. You can add the state variables you want to track to the hook's dependencies array and the logic in your useEffect hook will run every time the state variables change.

Can I set state inside a useeffect hook?

▶ 1. Can I set state inside a useEffect hook? In principle, you can set state freely where you need it - including inside useEffect and even during rendering. Just make sure to avoid infinite loops by settting Hook deps properly and/or state conditionally. ▶ 2. Lets say I have some state that is dependent on some other state.

Is it possible to use setState inside useeffect?

Generally speaking, using setState inside useEffect will create an infinite loop that most likely you don't want to cause. There are a couple of exceptions to that rule which I will get into later.

How to use the useeffect method to clean up the component?

You can also pass variables on which useEffect depends to re-run the logic passed into the useEffect .The empty array will run the effect hook only once. We can also use the useEffect method as a cleanup function once the component will destroy.The useEffect can return a function to clean up the effect as like componentWillUnmount () method:

When is the useeffect statement executed?

The effect is executed every time the prop changes. Let’s extend the example a bit to demonstrate more pivotal concepts in conjunction with prop changes. I added log statements to indicate all component renderings, as well as the invocation of our useEffect statement.


Video Answer


1 Answers

You can use mutable refs to decouple reading current state from effect dependencies:

const [n, setN] = useState(0);
const nRef = useRef(n); // define mutable ref

useEffect(() => { nRef.current = n }) // nRef is updated after each render
useEffect(() => {
  const handleMouseOver = () => {
    os.innerHTML = `N=${nRef.current}` // n always has latest state here
  }
 
  os.addEventListener('mouseover', handleMouseOver)
  return () => { os.removeEventListener('mouseover', handleMouseOver) }
}, []) // no need to set dependencies

const App = () => {
  const [n, setN] = React.useState(0);
  const nRef = React.useRef(n); // define mutable ref

  React.useEffect(() => { nRef.current = n }) // nRef.current is updated after each render
  React.useEffect(() => {
    const os = document.getElementById('outside-react')
    const handleMouseOver = () => { 
      os.innerHTML = `N=${nRef.current}`  // n always has latest state here
    } 

    os.addEventListener('mouseover', handleMouseOver)
    return () => { os.removeEventListener('mouseover', handleMouseOver) }
  }, []) // no need to set dependencies 

  return (
    <div>
      <button onClick={() => setN(prev => prev + 1)}>+</button>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<div id="outside-react">div</div>
<p>Update counter with + button, then mouseover the div to see recent counter state.</p>

The event listener will be added/removed only once on mounting/unmounting. Current state n can be read inside useEffect without setting it as dependency ([] deps), so there is no re-triggering on changes.

You can think of useRef as mutable instance variables for function components and Hooks. The equivalent in class components would be the this context - that is why this.state.n in handleMouseOver of the class component example always returns latest state and works.

There is a great example by Dan Abramov showcasing above pattern with setInterval. The blog post also illustrates potential problems with useCallback and when an event listener is readded/removed with every state change.

Other useful examples are (global) event handlers like os.addEventListener or integration with external libraries/frameworks at the edges of React.

Note: React docs recommend to use this pattern sparingly. From my point of view, it is a viable alternative in situations, where you just need "the latest state" - independent of React render cycle updates. By using mutable variables, we break out of the function closure scope with potentially stale closure values.

Writing state independently from dependencies has further alternatives - you can take a look at How to register event with useEffect hooks? for more infos.

like image 52
ford04 Avatar answered Oct 31 '22 16:10

ford04