React Usestate Hook Event Handler Using Initial State

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.

  1. 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 the useEffect 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 first useEffect doesn't update the callback passed to event listener in the second useEffect since it doesn't re-run and hence doesn't update the callback passed to the event listener.
  2. 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



Leave a reply



Submit