React useState hook event handler using initial state
How to Fix a Stale useState
Currently, your issue is that you're reading a value from the past. When you define handleResize
it belongs to that render, therefore, when you rerender, nothing happens to the event listener so it still reads the old value from its render.
There are a several ways to solve this. First let's look at the most simple solution.
Create your function in scope
Your event listener for the mouse down event passes the point
value to your resizerMouseDown
function. That value is the same value that you set your activePoint
to, so you can move the definition of your handleResize
function into resizerMouseDown
and console.log(point)
. Because this solution is so simple, it cannot account for situations where you need to access your state outside of resizerMouseDown
in another context.
See the in-scope function solution live on CodeSandbox.
useRef
to read a future value
A more versatile solution would be to create a useRef
that you update whenever activePoint
changes so that you can read the current value from any stale context.
const [activePoint, _setActivePoint] = React.useState(null);
// Create a ref
const activePointRef = React.useRef(activePoint);
// And create our custom function in place of the original setActivePoint
function setActivePoint(point) {
activePointRef.current = point; // Updates the ref
_setActivePoint(point);
}
function handleResize() {
// Now you'll have access to the up-to-date activePoint when you read from activePointRef.current in a stale context
console.log(activePointRef.current);
}
function resizerMouseDown(event, point) {
/* Truncated */
}
See the useRef
solution live on CodeSandbox.
Addendum
It should be noted that these are not the only ways to solve this problem, but these are my preferred methods because the logic is more clear to me despite some of the solutions being longer than other solutions offered. Please use whichever solution you and your team best understand and find to best meet your specific needs; don't forget to document what your code does though.
I can't access the initial state of my useState hook inside of my method to change the state
When you called setData function, you passed an array instead of an object, so you replaced your object with an array, in the second execution of the function, it tried to access your property chartData.datasets that is undefined, and it read an item of an array that is undefined, this is why you have the error.
const chartData = {
labels: (games || []).map(({score}, i) => "label " + (i+1)),
datasets: [
{
label: 'My Overall Progress',
// I only have the || [] here in case `games` isn't already an array as default state
data: (games || []).map(({score}) => score).reverse(),
fill: true,
borderColor: 'rgb(75, 192, 192)',
tension: 0.1,
}
]
}
How useState works with eventListener in react
This is the best I can explain:
For the context, when you click the toggle flag button, only your first useEffect(callback,[handler])
runs after EVERY rendering and your second useEffect(callback,[eventName,element])
runs ONLY ONCE after the initial rendering.
- Now when you pass savedHandler.current directly to the event listener, line 21, the event listener attaches function returned by
useCallback
as it is and hence on every event, the same function is being called due to reasons: The event is registered only ONCE and the cleanup function does not run since theuseEffect
runs only once after the initial rendering(the cleanup function will run in instances, just before the next side effect due to dependency changes, and when the component unmount). So you are registering the event only once with a memoized callback which persist across renderings. The update of savedHandler.current in the firstuseEffect
doesn't update the callback passed to event listener in the seconduseEffect
since it doesn't re-run and hence doesn't update the callback passed to the event listener. - Now when you use savedHandler.current function inside an anonymous function, the scenario is completely different here. When you pass such functions as a callback, on every event a new function is being invoked unlike the first instance. The anonymous callback of first event and second event is not same despite having the same code. So here you are not stuck with the same event listener callback that you passed previously and hence you have now access to the latest memoized savedHandler.current value inside the callback function updated by the first useEffect despite the second effect not running again.
This is it. To confirm for yourself, try to add handler as the dependency on your second useEffect
as well, and pass savedHandler.current directly to the event listener. You will get the updated state value as the useEffect
now runs after every update of handler and the event listener gets the latest callback to invoke upon.
And instead of creating another variable, you can directly do element.addEventListener(eventName, (event) => savedHandler.current(event));
useState hook can only set one object at the time and return the other object of the same array to initial state
Wow, this was a doozy. The ultimate problem stems from the fact that you're calling the following component:
export default function FormsByAudience(props: FormsByAudienceProps) {
const [critics, setCritics] = useCritics(props.books);
const setCritic = (index: number) => (critic: IcriticState) => {
const c = [...critics];
c[index] = critic;
setCritics(c);
};
// ...
// in render method:
setCritic={setCritic(criticIdx)}
for every single different critic (and reader):
props.audiences.critics.map((c) => (
<div key={c.id}>
<div>{c.name}</div>
{props.shouldRenderCritics &&
props.criticsChildren &&
cloneElement(props.criticsChildren, { criticId: c.id })}
</div>
))}
where props.criticsChildren
contains <FormsByAudience audienceId={id} books={books} />
.
As a result, inside a single render, there are lots of separate bindings for the critics
variable. You don't have a single critics
for the whole application, or for a given audience; you have multiple critics
arrays, one for each critic. Setting the state for one critic's critics
array in FormsByAudience
does not result in changes to other critics
arrays that the other critics' React elements close over.
To fix it, given that the critics
array is being created from props.books
, the critics
state should be put near the same level where the books are used, and definitely not inside a component in a nested mapper function. Then pass down the state and the state setters to the children.
The exact same thing applies to the readers
state.
Here is a minimal live Stack Snippet illustrating this problem:
const people = ['bob', 'joe'];
const Person = ({name, i}) => {
const [peopleData, setPeopleData] = React.useState(people.map(() => 0));
console.log(peopleData.join(','));
const onChange = e => {
setPeopleData(
peopleData.map((num, j) => j === i ? e.target.value : num)
)
};
return (
<div>
<div>{name}</div>
<input type="number" value={peopleData[i]} onChange={onChange} />
</div>
);
};
const App = () => {
return people.map(
(name, i) => <Person key={i} {...{ name, i }} />
);
};
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>
Wrong React hooks behaviour with event listener
This is a common problem for functional components that use the useState
hook. The same concerns are applicable to any callback functions where useState
state is used, e.g. setTimeout
or setInterval
timer functions.
Event handlers are treated differently in CardsProvider
and Card
components.
handleCardClick
and handleButtonClick
used in the CardsProvider
functional component are defined in its scope. There are new functions each time it runs, they refer to cards
state that was obtained at the moment when they were defined. Event handlers are re-registered each time the CardsProvider
component is rendered.
handleCardClick
used in the Card
functional component is received as a prop and registered once on component mount with useEffect
. It's the same function during the entire component lifespan and refers to stale state that was fresh at the time when the handleCardClick
function was defined the first time. handleButtonClick
is received as a prop and re-registered on each Card
render, it's a new function each time and refers to fresh state.
Mutable state
A common approach that addresses this problem is to use useRef
instead of useState
. A ref is basically a recipe that provides a mutable object that can be passed by reference:
const ref = useRef(0);
function eventListener() {
ref.current++;
}
In this case a component should be re-rendered on a state update like it's expected from useState
, refs aren't applicable.
It's possible to keep state updates and mutable state separately but forceUpdate
is considered an anti-pattern in both class and function components (listed for reference only):
const useForceUpdate = () => {
const [, setState] = useState();
return () => setState({});
}
const ref = useRef(0);
const forceUpdate = useForceUpdate();
function eventListener() {
ref.current++;
forceUpdate();
}
State updater function
One solution is to use a state updater function that receives fresh state instead of stale state from the enclosing scope:
function eventListener() {
// doesn't matter how often the listener is registered
setState(freshState => freshState + 1);
}
In this case a state is needed for synchronous side effects like console.log
, a workaround is to return the same state to prevent an update.
function eventListener() {
setState(freshState => {
console.log(freshState);
return freshState;
});
}
useEffect(() => {
// register eventListener once
return () => {
// unregister eventListener once
};
}, []);
This doesn't work well with asynchronous side effects, notably async
functions.
Manual event listener re-registration
Another solution is to re-register the event listener every time, so a callback always gets fresh state from the enclosing scope:
function eventListener() {
console.log(state);
}
useEffect(() => {
// register eventListener on each state update
return () => {
// unregister eventListener
};
}, [state]);
Built-in event handling
Unless the event listener is registered on document
, window
or other event targets that are outside of the scope of the current component, React's own DOM event handling has to be used where possible, this eliminates the need for useEffect
:
<button onClick={eventListener} />
In the last case the event listener can be additionally memoized with useMemo
or useCallback
to prevent unnecessary re-renders when it's passed as a prop:
const eventListener = useCallback(() => {
console.log(state);
}, [state]);
- Previous edition of this answer suggested to use mutable state that was applicable to initial
useState
hook implementation in React 16.7.0-alpha version but isn't workable in final React 16.8 implementation.useState
currently supports only immutable state.*
How to use State Variable (useState) in an EventHandler
Each render of your Child
will get a new x
, a new props
object, etc. However you are binding your event listener only once and so capturing only the initial props.num
value.
Two ways to fix:
Rebind event listener when num
changes, by passing num
as a dependency to your effect to bind the event listener:
const Child = ({ num }) => {
useEffect(() => {
// no need to define this in main function since it is only
// used inside this effect
const md = () => { console.log(num); };
document.getElementById("box").addEventListener("mousedown", md);
return () => {
document.removeEventListener("mousedown", md);
};
}, [num]);
return <div id="box">click {num}</div>;
};
Or use a ref
to hold the value of num and bind your event listener to the ref. This gives you a level of indirection to handle the change:
const Child = ({ num }) => {
const numRef = useRef(); // will be same object each render
numRef.current = num; // assign new num value each render
useEffect(() => {
// no need to define this in main function since it is only
// used inside this effect
// binds to same ref object, and reaches in to get current num value
const md = () => { console.log(numRef.current); };
document.getElementById("box").addEventListener("mousedown", md);
return () => {
document.removeEventListener("mousedown", md);
};
}, []);
return <div id="box">click {num}</div>;
};
Run event handler functions synchronously after a React state change
A slight difference to Drew's answer but achieved using the same tools (useEffect
).
// Constants for dialog state
const DIALOG_CLOSED = 0;
const DIALOG_OPEN = 1;
const DIALOG_CONFIRM = 2;
const DIALOG_CANCELLED = 3;
const Parent = () => {
// useState to keep track of dialog state
const [dialogState, setDialogState] = useState(DIALOG_CLOSED);
// Set dialog state to cancelled when dismissing.
const handleDismiss = () => {
setDialogState(DIALOG_CANCELLED);
}
// set dialog state to confirm when confirming.
const handleConfirm = () => {
setDialogState(DIALOG_CONFIRM);
}
// useEffect that triggers on dialog state change.
useEffect(() => {
// run code when confirm was selected and dialog is closed.
if (dialogState === DIALOG_CONFIRM) {
const focusOnElementB = () => { .... };
focusOnElementB()
}
// run code when cancel was selected and dialog is closed.
if (dialogState === DIALOG_CANCELLED) {
const focusOnElementA = () => { .... };
focusOnElementA()
}
}, [dialogState])
return (
<>
<Button onClick={() => { setDialogState(DIALOG_OPEN) }}>Open dialog</Button>
<DialogModal
isOpen={dialogState === DIALOG_OPEN}
onDismiss={handleDismiss}
onConfirm={handleConfirm}
/>
</>
)
}
React useState() hook returns undefined when using option chained value as initial count
I fixed it using useEffect(). Maybe the setCarQuantity() was failed to set the value immediately and after using useEffect() it listens to the state change.
const [carQuantity, setCarQuantity] = useState(totalQuantity);
useEffect(() => {
setCarQuantity(totalQuantity);
}, [totalQuantity]);
useState Hook not working inside event listener
The value(updated value) of cursor
is not available during the execution of escFunction.
In order to access the value it has to be converted to setCursor(currentId => currentId + 1);
or
setCursor(currentId =>{
// do pre processing
return cursor+1
})
As hinted in comments :
Passing a callback allows you to access the current state value. Your code as it is creates a closure around the state value at the time the child was last rendered.
cursor from the state inside that closure function is always the initial value, because of the closure function
Related Topics
How to Get the Index of an Array in a Meteor Template Each Loop
Find All Text Nodes in HTML Page
How to Set a Cookie with Expire Time
Vue V-On:Click Does Not Work on Component
JavaScript 'In' Operator for 'Undefined' Elements in Arrays
How to Use Blob Url, Mediasource or Other Methods to Play Concatenated Blobs of Media Fragments
Es6 Promises - Something Like Async.Each
How to Access Dom Elements in Electron
How to Persist a Es6 Map in Localstorage (Or Elsewhere)
Jquery Attribute Selector Variable
React React-Router-Dom Pass Props to Component
What Is Array Literal Notation in JavaScript and When Should You Use It
Why JavaScript Function Declaration (And Expression)
Dynamic Extension Context Menu That Depends on Selected Text
Fetching Values from Email in Protractor Test Case