Understanding click events and memory in react function components - javascript

I've been working with function components and hooks and i'm now trying to dig deeper into events and how they hold in memory. I've been using chrome dev tools performance tab to monitor the behavior. There is a few things I'm not clear on and maybe someone can clear this up for me.
So I did 3 different setups. First one to show obvious memory leak of events been added multiple times per render. which eventually causes a crash or infinite loop of renders. Or at least thats what looks like is happing.
const App = () => {
const [count, setCount] = React.useState(0);
const onKeyDown = () => setCount(count => count + 1);
document.addEventListener('keydown', onKeyDown);
return (
<div className='wrapper'>
<div>Click any key to update counter</div>
<div className='counter'>{count}</div>
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#app"))
This shows an obvious spike in extra event calls per listener. See event log and then increase ladder of events been added.
Next up
const App = () => {
const [count, setCount] = React.useState(0);
const onKeyDown = () => setCount(count => count + 1);
React.useEffect(() => {
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [] );
return (
<div className='wrapper'>
<div>Click any key to update counter</div>
<div className='counter'>{count}</div>
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#app"))
The result was better in that the listener was only one call at a time.
But I noticed the listener count was still going through the roof. The spike in when they got added wasn't as sharp. but the count of listeners was in the thousand. Where are all these listeners getting added. Is it listeners been added by jsfiddle. Probably best to isolate this test in just a html page outside jsfiddle.
Then I read about using the hook useCallback which memoizes the function and returns the cashed version of the function. So I tried this.
const App = () => {
const [count, setCount] = React.useState(0);
const cb = React.useCallback(() => {
console.log('cb');
setCount(count => count + 1);
}, [] );
return (
<div className='wrapper'>
<div onClick={cb}>Click any key to update counter</div>
<div className='counter'>{count}</div>
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#app"))
But this turned out to be similar to the last test using useEffect.
Crazy amount of listeners still but no crashing like the first test.
So whats' the deal here am I missing something about memoizing using useCallback hook. Listeners look like they are been added like crazy and not been garbage collected.
I'm going to isolate this test without jsfiddle but just wanted to post to the community to get some insight on this first.

You don't use addEventListener in React!
Instead you'd do something like this:
const App = () => {
let count = 0;
const onAddHandler = () => {
count++;
console.log(count);
this._count.innerText = count;
}
return (
<div className='wrapper'>
<div onClick={()=>onAddHandler()}>Click any key to update counter</div>
<div className='counter' ref={(el) => this._count = el}></div>
</div>
);
}
Also, not sure why you're using React.useState. The whole point of functional components is that they're stateless. I'm not fond of this new useState hook to be used in a functional component.
The example you were probably looking for using hooks is:
import React, { useState, useEffect } from 'react';
import {render} from 'react-dom';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
render(<Example />, document.getElementById('root'));
The React documentation https://reactjs.org/docs/hooks-effect.html says that
If you’re familiar with React class lifecycle methods, you can think
of useEffect Hook as componentDidMount, componentDidUpdate, and
componentWillUnmount combined.

Related

React, useEfect and axios. infinite loop [duplicate]

Are there ways to simulate componentDidMount in React functional components via hooks?
For the stable version of hooks (React Version 16.8.0+)
For componentDidMount
useEffect(() => {
// Your code here
}, []);
For componentDidUpdate
useEffect(() => {
// Your code here
}, [yourDependency]);
For componentWillUnmount
useEffect(() => {
// componentWillUnmount
return () => {
// Your code here
}
}, [yourDependency]);
So in this situation, you need to pass your dependency into this array. Let's assume you have a state like this
const [count, setCount] = useState(0);
And whenever count increases you want to re-render your function component. Then your useEffect should look like this
useEffect(() => {
// <div>{count}</div>
}, [count]);
This way whenever your count updates your component will re-render. Hopefully this will help a bit.
There is no exact equivalent for componentDidMount in react hooks.
In my experience, react hooks requires a different mindset when developing it and generally speaking you should not compare it to the class methods like componentDidMount.
With that said, there are ways in which you can use hooks to produce a similar effect to componentDidMount.
Solution 1:
useEffect(() => {
console.log("I have been mounted")
}, [])
Solution 2:
const num = 5
useEffect(() => {
console.log("I will only run if my deps change: ", num)
}, [num])
Solution 3 (With function):
useEffect(() => {
const someFunc = () => {
console.log("Function being run after/on mount")
}
someFunc()
}, [])
Solution 4 (useCallback):
const msg = "some message"
const myFunc = useCallback(() => {
console.log(msg)
}, [msg])
useEffect(() => {
myFunc()
}, [myFunc])
Solution 5 (Getting creative):
export default function useDidMountHook(callback) {
const didMount = useRef(null)
useEffect(() => {
if (callback && !didMount.current) {
didMount.current = true
callback()
}
})
}
It is worth noting that solution 5 should only really be used if none of the other solutions work for your use case. If you do decide you need solution 5 then I recommend using this pre-made hook use-did-mount.
Source (With more detail): Using componentDidMount in react hooks
There's no componentDidMount on functional components, but React Hooks provide a way you can emulate the behavior by using the useEffect hook.
Pass an empty array as the second argument to useEffect() to run only the callback on mount only.
Please read the documentation on useEffect.
function ComponentDidMount() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log('componentDidMount');
}, []);
return (
<div>
<p>componentDidMount: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<div>
<ComponentDidMount />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
useEffect() hook allows us to achieve the functionality of componentDidMount, componentDidUpdate componentWillUnMount functionalities.
Different syntaxes of useEffect() allows to achieve each of the above methods.
i) componentDidMount
useEffect(() => {
//code here
}, []);
ii) componentDidUpdate
useEffect(() => {
//code here
}, [x,y,z]);
//where x,y,z are state variables on whose update, this method should get triggered
iii) componentDidUnmount
useEffect(() => {
//code here
return function() {
//code to be run during unmount phase
}
}, []);
You can check the official react site for more info. Official React Page on Hooks
Although accepted answer works, it is not recommended. When you have more than one state and you use it with useEffect, it will give you warning about adding it to dependency array or not using it at all.
It sometimes causes the problem which might give you unpredictable output. So I suggest that you take a little effort to rewrite your function as class. There are very little changes, and you can have some components as class and some as function. You're not obligated to use only one convention.
Take this for example
function App() {
const [appointments, setAppointments] = useState([]);
const [aptId, setAptId] = useState(1);
useEffect(() => {
fetch('./data.json')
.then(response => response.json())
.then(result => {
const apts = result.map(item => {
item.aptId = aptId;
console.log(aptId);
setAptId(aptId + 1);
return item;
})
setAppointments(apts);
});
}, []);
return(...);
}
and
class App extends Component {
constructor() {
super();
this.state = {
appointments: [],
aptId: 1,
}
}
componentDidMount() {
fetch('./data.json')
.then(response => response.json())
.then(result => {
const apts = result.map(item => {
item.aptId = this.state.aptId;
this.setState({aptId: this.state.aptId + 1});
console.log(this.state.aptId);
return item;
});
this.setState({appointments: apts});
});
}
render(...);
}
This is only for example. so lets not talk about best practices or potential issues with the code. Both of this has same logic but the later only works as expected. You might get componentDidMount functionality with useEffect running for this time, but as your app grows, there are chances that you MAY face some issues. So, rather than rewriting at that phase, it's better to do this at early stage.
Besides, OOP is not that bad, if Procedure-Oriented Programming was enough, we would never have had Object-Oriented Programming. It's painful sometimes, but better (technically. personal issues aside).
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Please visit this official docs. Very easy to understand the latest way.
https://reactjs.org/docs/hooks-effect.html
Info about async functions inside the hook:
Effect callbacks are synchronous to prevent race conditions. Put the async function inside:
useEffect(() => {
async function fetchData() {
// You can await here
const response = await MyAPI.getData(someId);
// ...
}
fetchData();
}, [someId]); // Or [] if effect doesn't need props or state
useLayoutEffect hook is the best alternative to ComponentDidMount in React Hooks.
useLayoutEffect hook executes before Rendering UI and useEffect hook executes after rendering UI. Use it depend on your needs.
Sample Code:
import { useLayoutEffect, useEffect } from "react";
export default function App() {
useEffect(() => {
console.log("useEffect Statements");
}, []);
useLayoutEffect(() => {
console.log("useLayoutEffect Statements");
}, []);
return (
<div>
<h1>Hello Guys</h1>
</div>
);
}
Yes, there is a way to SIMULATE a componentDidMount in a React functional component
DISCLAIMER: The real problem here is that you need to change from "component life cycle mindset" to a "mindset of useEffect"
A React component is still a javascript function, so, if you want something to be executed BEFORE some other thing you must simply need to execute it first from top to bottom, if you think about it a function it's still a funtion like for example:
const myFunction = () => console.log('a')
const mySecondFunction = () => console.log('b)
mySecondFunction()
myFunction()
/* Result:
'b'
'a'
*/
That is really simple isn't it?
const MyComponent = () => {
const someCleverFunction = () => {...}
someCleverFunction() /* there I can execute it BEFORE
the first render (componentWillMount)*/
useEffect(()=> {
someCleverFunction() /* there I can execute it AFTER the first render */
},[]) /*I lie to react saying "hey, there are not external data (dependencies) that needs to be mapped here, trust me, I will leave this in blank.*/
return (
<div>
<h1>Hi!</h1>
</div>
)}
And in this specific case it's true. But what happens if I do something like that:
const MyComponent = () => {
const someCleverFunction = () => {...}
someCleverFunction() /* there I can execute it BEFORE
the first render (componentWillMount)*/
useEffect(()=> {
someCleverFunction() /* there I can execute it AFTER the first render */
},[]) /*I lie to react saying "hey, there are not external data (dependencies) that needs to be maped here, trust me, I will leave this in blank.*/
return (
<div>
<h1>Hi!</h1>
</div>
)}
This "cleverFunction" we are defining it's not the same in every re-render of the component.
This lead to some nasty bugs and, in some cases to unnecessary re-renders of components or infinite re-render loops.
The real problem with that is that a React functional component is a function that "executes itself" several times depending on your state thanks to the useEffect hook (among others).
In short useEffect it's a hook designed specifically to synchronize your data with whatever you are seeing on the screen. If your data changes, your useEffect hook needs to be aware of that, always. That includes your methods, for that it's the array dependencies.
Leaving that undefined leaves you open to hard-to-find bugs.
Because of that it's important to know how this work, and what you can do to get what you want in the "react" way.
const initialState = {
count: 0,
step: 1,
done: false
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'doSomething') {
if(state.done === true) return state;
return { ...state, count: state.count + state.step, state.done:true };
} else if (action.type === 'step') {
return { ...state, step: action.step };
} else {
throw new Error();
}
}
const MyComponent = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
dispatch({ type: 'doSomething' });
}, [dispatch]);
return (
<div>
<h1>Hi!</h1>
</div>
)}
useReducer's dispatch method it's static so it means it will be the same method no matter the amount of times your component is re-rendered. So if you want to execute something just once and you want it rigth after the component is mounted, you can do something like the above example. This is a declarative way of do it right.
Source: The Complete Guide to useEffect - By Dan Abramov
That being said if you like to experiment with things and want to know how to do it "the imperative wat" you can use a useRef() with a counter or a boolean to check if that ref stores a defined reference or not, this is an imperative approach and it's recommended to avoid it if you're not familiar with what happen with react behind curtains.
That is because useRef() is a hook that saves the argument passed to it regardless of the amount of renders (I am keeping it simple because it's not the focus of the problem here, you can read this amazing article about useRef ). So it's the best approach to known when the first render of the component happened.
I leave an example showing 3 different ways of synchronise an "outside" effect (like an external function) with the "inner" component state.
You can run this snippet right here to see the logs and understand when these 3 functions are executed.
const { useRef, useState, useEffect, useCallback } = React
// External functions outside react component (like a data fetch)
function renderOnce(count) {
console.log(`renderOnce: I executed ${count} times because my default state is: undefined by default!`);
}
function renderOnFirstReRender(count) {
console.log(`renderOnUpdate: I executed just ${count} times!`);
}
function renderOnEveryUpdate(count) {
console.log(`renderOnEveryUpdate: I executed ${count ? count + 1 : 1} times!`);
}
const MyComponent = () => {
const [count, setCount] = useState(undefined);
const mounted = useRef(0);
// useCallback is used just to avoid warnings in console.log
const renderOnEveryUpdateCallBack = useCallback(count => {
renderOnEveryUpdate(count);
}, []);
if (mounted.current === 0) {
renderOnce(count);
}
if (mounted.current === 1) renderOnFirstReRender(count);
useEffect(() => {
mounted.current = mounted.current + 1;
renderOnEveryUpdateCallBack(count);
}, [count, renderOnEveryUpdateCallBack]);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(prevState => (prevState ? prevState + 1 : 1))}>TouchMe</button>
</div>
);
};
class App extends React.Component {
render() {
return (
<div>
<h1>hI!</h1>
</div>
);
}
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<MyComponent/>
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
If you execute it you will see something like this:
You want to use useEffect(), which, depending on how you use the function, can act just like componentDidMount().
Eg. you could use a custom loaded state property which is initially set to false, and switch it to true on render, and only fire the effect when this value changes.
Documentation
the exact equivalent hook for componentDidMount() is
useEffect(()=>{},[]);
hope this helpful :)

useEffect, useState and promises.. what to do [duplicate]

According to the documentation for the useState React Hook:
If the new state is computed using the previous state, you can pass a
function to setState. The function will receive the previous value,
and return an updated value.
So given
const [count, setCount] = useState(initialCount);
you can write
setCount(prevCount => prevCount + 1);
I understand the reason for using the updater function form with setState, as multiple calls may be batched. However
During subsequent re-renders, the first value returned by useState
will always be the most recent state after applying updates.
So I'm not clear why the above example couldn't be written as
setCount(count + 1);
(which is how it's presented in Using the State Hook).
Is there a case where you must use functional updates with the useState hook to get the correct result?
(Edit: Possibly related to https://github.com/facebook/react/issues/14259 )
The main scenarios when the functional update syntax is still necessary are when you are in asynchronous code. Imagine that in useEffect you do some sort of API call and when it finishes you update some state that can also be changed in some other way. useEffect will have closed over the state value at the time the effect started which means that by the time the API call finishes, the state could be out-of-date.
The example below simulates this scenario by having a button click trigger two different async processes that finish at different times. One button does an immediate update of the count; one button triggers two async increments at different times without using the functional update syntax (Naive button); the last button triggers two async increments at different times using the functional update syntax (Robust button).
You can play with this in the CodeSandbox to see the effect.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function App() {
const [count, setCount] = useState(1);
const [triggerAsyncIndex, setTriggerAsyncIndex] = useState(1);
const [triggerRobustAsyncIndex, setTriggerRobustAsyncIndex] = useState(1);
useEffect(
() => {
if (triggerAsyncIndex > 1) {
setTimeout(() => setCount(count + 1), 500);
}
},
[triggerAsyncIndex]
);
useEffect(
() => {
if (triggerAsyncIndex > 1) {
setTimeout(() => setCount(count + 1), 1000);
}
},
[triggerAsyncIndex]
);
useEffect(
() => {
if (triggerRobustAsyncIndex > 1) {
setTimeout(() => setCount(prev => prev + 1), 500);
}
},
[triggerRobustAsyncIndex]
);
useEffect(
() => {
if (triggerRobustAsyncIndex > 1) {
setTimeout(() => setCount(prev => prev + 1), 1000);
}
},
[triggerRobustAsyncIndex]
);
return (
<div className="App">
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<br />
<button onClick={() => setTriggerAsyncIndex(triggerAsyncIndex + 1)}>
Increment Count Twice Async Naive
</button>
<br />
<button
onClick={() => setTriggerRobustAsyncIndex(triggerRobustAsyncIndex + 1)}
>
Increment Count Twice Async Robust
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Another possible scenario where functional updates could be necessary would be if multiple effects are updating the state (even if synchronously). Once one effect updates the state, the other effect would be looking at out-of-date state. This scenario seems less likely to me (and would seem like a poor design choice in most cases) than async scenarios.

Why does my onClick handler not log out the latest version of state and how can I troubleshoot this?

Sandbox
import React, { useEffect, useState } from "react";
export default function App() {
let [button, setButton] = useState(null);
let [num, setNum] = useState(5);
function revealState() {
console.log(num);
}
function changeState() {
setNum(Math.random());
}
useEffect(() => {
const el = (
<button id="logStateButton" onClick={revealState}>
Log state
</button>
);
setButton(el);
}, []);
return (
<>
{button}
<button onClick={changeState}>Change state</button>
</>
);
}
Clicking on the 'Log state button' successfully logs num state. Clicking on the 'Change state button' successfully changes num state. Re-clicking the 'Log state button' doesn't log the updated value of state - it logs the old one.
Why is this? My guess is that, since useEffect runs only the once, it references only the first revealState function which references only the first num variable. Because it's not in the component's return statement it doesn't get 'refreshed'.
Whatever the cause of the problem, what're some work-arounds? Some of the requirements are:
the tag can't be rendered directly in the return statement.
we have to have the useEffect that's there and it needs to have a dep array of some sort (its undesirable for it to fire every-time the function component is re-executed).
In the real project, some important changes to the tags useEffect's callback renders might get made - therefore it's impractical to re-run the useEffect by putting something like num in its dep array.
IMO, the neatest solution is to simply add the updated event listener, every time the page is rendered:
useEffect(() => {
el.onclick = onClickHandler
});
The event listener always has access to the latest state (and props). IMO, this solution is more scalable than previously-mentioned solutions - if my event listener has to track the latest versions of multiple state & props, this could get messy. With this, all I need to do is add extra listeners in into this one useEffect callback. Thoughts?
import React, { useEffect, useState, useCallback } from "react";
export default function App() {
let [button, setButton] = useState(null);
let [num, setNum] = useState(5);
const revealState = useCallback(() => {
console.log(num);
}, [num])
function changeState() {
setNum(Math.random());
}
useEffect(() => {
const el = (
<button id="logStateButton" onClick={revealState}>
Log state
</button>
);
setButton(el);
}, [revealState]);
return (
<>
{button}
<button onClick={changeState}>Change state</button>
</>
);
}
you can listen to the revealState in useEffect. which gets initialized only when num is changed achieved using useCallback. so whenever you click the button the num is changed which initializes the revealState function and not initialized on other rerenders
you have to add num as dependency to useEffect:
useEffect(() => {
const el = (
<button id="logStateButton" onClick={revealState}>
Log state
</button>
);
setButton(el);
}, [num]);
After more clarification on your problem it seems you need to watch over both the num and the HTML state. Combining both Alan and kishore's code together is the solution. Since the useEffect is only watching the num in Alan's answer so if any changes to the tag will not cause it to rerun. kishore also mentioned another fix which is to use the useCallback but what needs to be watch is the button and not the num. Like this:
const updateButton = useCallback(function (newButton) {
setButton(newButton);
}, [button])
useEffect(() => {
const el = (
<button id="logStateButton" onClick={revealState}>
Log state
</button>
);
updateButton(el)
}, [num]);
This will tell useEffect to watch num and will return a new button only when button state is changed.

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
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.
What's happening is, the function closes on n, but while closures usually see updates to the variable, hook variables are recreated all the time staling the closure.
In hooks based components, the state is assigned a new variable on each render, which the listener function never closed on, and the closure doesn't get updated since you only create the function once on mount (with the empty dependency array). In contrary, in class based components the this stays the same so the closure can see changes.
I don't see constantly adding and removing listeners as an issue. Consider the fact, unless you use useCallback() to create your event handlers (which you should only do with memoized children, otherwise it's premature optimization) in everyday react events, React itself will literally do just this, namely, remove the previous function and set the new function.
The only way to get latest value is to specify it as dependency and here are reasoning behind that
why add or remove called again and again ?
Every time a dependency changes it re-executes entire function
Why value of n is not updating ?
every-time a functional component is rendered all assignments will re-happen just like a normal function, so the value of reference object which stored 'n=0' will remain same and on each subsequent render new object will be created which will point to updated value
There are some issues in the manner you're trying to solve this particular problem.
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.
That works because the handler functions are updated acc. to the latest n value when useeffect is called.
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).
That's because the handler functions didn't get the current value for n
Using refs here can be an advantage, as the value will persist b/w rerenders. Check this example here: https://codesandbox.io/s/wispy-pond-j80j7?file=/src/App.js
export default function App() {
const [n, setN] = React.useState(0);
const nRef = React.useRef(0);
const outsideReactRef = React.useRef(null);
const handleMouseOver = React.useCallback(() => {
outsideReactRef.current.innerHTML = `N=${nRef.current}`;
}, []);
React.useEffect(() => {
outsideReactRef.current = document.getElementById("outside-react");
console.log("Add handler");
outsideReactRef.current.addEventListener("mouseover", handleMouseOver);
return () => {
console.log("Remove handler");
outsideReactRef.current.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 => {
const newN = n + 1;
nRef.current = newN;
return newN;
})
}
>
+
</button>
</div>
);
}
well you can use window object or global object to assign the variable you want using useEffect like this :
try{
const App = () => {
const [n, setN] = React.useState(0);
React.useEffect(()=>{
window.num = n
},[n])
React.useEffect(() => {
const os = document.getElementById('outside-react')
const handleMouseOver =() => {
os.innerHTML = `N=${window.num}`
}
console.log('Add handler')
os.addEventListener('mouseover', handleMouseOver)
return () => {
console.log('Remove handler')
os.removeEventListener('mouseover', handleMouseOver)
}
}, []) // <-- I can change this to [n] and it works, but add/remove keeps getting invoked
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={() => setN(n + 1)}>+</button>
<br/>
state inside react: {n}
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
}
catch(error){
console.log(error.message)
}
<div id="outside-react">OUTSIDE REACT - hover to get state</div>
<div id="root"></div>
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
this will change window.num on n state change

When are functional updates required for computations involving previous state?

According to the documentation for the useState React Hook:
If the new state is computed using the previous state, you can pass a
function to setState. The function will receive the previous value,
and return an updated value.
So given
const [count, setCount] = useState(initialCount);
you can write
setCount(prevCount => prevCount + 1);
I understand the reason for using the updater function form with setState, as multiple calls may be batched. However
During subsequent re-renders, the first value returned by useState
will always be the most recent state after applying updates.
So I'm not clear why the above example couldn't be written as
setCount(count + 1);
(which is how it's presented in Using the State Hook).
Is there a case where you must use functional updates with the useState hook to get the correct result?
(Edit: Possibly related to https://github.com/facebook/react/issues/14259 )
The main scenarios when the functional update syntax is still necessary are when you are in asynchronous code. Imagine that in useEffect you do some sort of API call and when it finishes you update some state that can also be changed in some other way. useEffect will have closed over the state value at the time the effect started which means that by the time the API call finishes, the state could be out-of-date.
The example below simulates this scenario by having a button click trigger two different async processes that finish at different times. One button does an immediate update of the count; one button triggers two async increments at different times without using the functional update syntax (Naive button); the last button triggers two async increments at different times using the functional update syntax (Robust button).
You can play with this in the CodeSandbox to see the effect.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function App() {
const [count, setCount] = useState(1);
const [triggerAsyncIndex, setTriggerAsyncIndex] = useState(1);
const [triggerRobustAsyncIndex, setTriggerRobustAsyncIndex] = useState(1);
useEffect(
() => {
if (triggerAsyncIndex > 1) {
setTimeout(() => setCount(count + 1), 500);
}
},
[triggerAsyncIndex]
);
useEffect(
() => {
if (triggerAsyncIndex > 1) {
setTimeout(() => setCount(count + 1), 1000);
}
},
[triggerAsyncIndex]
);
useEffect(
() => {
if (triggerRobustAsyncIndex > 1) {
setTimeout(() => setCount(prev => prev + 1), 500);
}
},
[triggerRobustAsyncIndex]
);
useEffect(
() => {
if (triggerRobustAsyncIndex > 1) {
setTimeout(() => setCount(prev => prev + 1), 1000);
}
},
[triggerRobustAsyncIndex]
);
return (
<div className="App">
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<br />
<button onClick={() => setTriggerAsyncIndex(triggerAsyncIndex + 1)}>
Increment Count Twice Async Naive
</button>
<br />
<button
onClick={() => setTriggerRobustAsyncIndex(triggerRobustAsyncIndex + 1)}
>
Increment Count Twice Async Robust
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Another possible scenario where functional updates could be necessary would be if multiple effects are updating the state (even if synchronously). Once one effect updates the state, the other effect would be looking at out-of-date state. This scenario seems less likely to me (and would seem like a poor design choice in most cases) than async scenarios.

Categories

Resources