React stale useState value in closure - how to fix? - reactjs

I want to use a state variable (value) when a modal is closed. However, any changes made to the state variable while the modal is open are not observed in the handler. I don't understand why it does not work.
CodeSandbox
or
Embedded CodeSandbox
Open the modal
Click 'Set value'
Click 'Hide modal'
View console log.
My understanding is that the element is rendered when the state changes (Creating someClosure foo), but then when the closure function is called after that, the value is still "". It appears to me to be a "stale value in a closure" problem, but I can't see how to fix it.
I have looked at explanations regarding how to use useEffect, but I can't see how they apply here.
Do I have to use a useRef or some other way to get this to work?
[Edit: I have reverted the React version in CodeSandbox, so I hope it will run now. I also implemented the change in the answers below, but it did not help.]
import { useState } from "react";
import { Modal, Button } from "react-materialize";
import "./styles.css";
export default function App() {
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState("");
console.log("Creating someClosure value =", value);
const someClosure = (argument) => {
console.log("In someClosure value =", value);
console.log("In someClosure argument =", argument);
setIsOpen(false);
};
return (
<div className="App">
<Button onClick={() => setIsOpen(true)}>Show modal</Button>
<Modal open={isOpen} options={{ onCloseStart: () => someClosure(value) }}>
<Button onClick={() => setValue("foo")}>Set value</Button>
<Button onClick={() => setIsOpen(false)}>Hide modal</Button>
</Modal>
</div>
);
}

This is a hard concept. You are using into your member function a state which evaluates "" at render so regardless state change the function signature still the same before render this is the reason why useEffect and useCallback should be used to trait side effects about state change. But there are a way to ensure get correct state without hooks. Just passing state as a parameter to function, by this approach you will receive the current state at render so just with few changes you achieve this.
At someClosure just create an argument:
const someClosure = (value) => {...}
So into modal component,
options={{ onCloseStart: someClosure(value) }}
Should be what you are looking for

Issue
The issue here is that you've declared a function during some render cycle and the current values of any variable references are closed over in scope:
const someClosure = () => {
console.log("In someClosure value =", value); // value -> ""
setIsOpen(false);
};
This "instance" of the callback is passed as a callback to a component and is invoked at a later point in time:
<Modal open={isOpen} options={{ onCloseStart: someClosure }}>
<Button onClick={() => setValue("foo")}>Set value</Button>
<Button onClick={() => setIsOpen(false)}>Hide modal</Button>
</Modal>
When the modal is triggered to close the callback with the now stale closure over the value state value is called.
Solution
Do I have to use a useRef or some other way to get this to work?
Basically yes, use a React ref and a useEffect hook to cache the state value that can be mutated/accessed at any time outside the normal React component lifecycle.
Example:
import { useEffect, useRef, useState } from "react";
...
export default function App() {
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState("");
const valueRef = useRef(value);
useEffect(() => {
console.log("Creating someClosure value =", value);
valueRef.current = value; // <-- cache current value
}, [value]);
const someClosure = (argument) => {
console.log("In someClosure value =", valueRef.current); // <-- access current ref value
console.log("In someClosure argument =", argument);
setIsOpen(false);
};
return (
<div className="App">
<Button onClick={() => setIsOpen(true)}>Show modal</Button>
<Modal
open={isOpen}
options={{
onCloseStart: () => someClosure(valueRef.current) // <-- access current ref value
}}
>
<Button onClick={() => setValue("foo")}>Set value</Button>
<Button onClick={() => setIsOpen(false)}>Hide modal</Button>
</Modal>
</div>
);
}

Although Drew's solution has solved the problem, but this proplem is actually caused by <Model> element which use options to pass callback function which has been resolved at first render. element don't update their options in the later rendering. This should be a bug.
In Drew's solution.
options={{
onCloseStart: () => someClosure(valueRef.current) // <-- access current ref value
}}
this callback's argument is a ref object which has similar to a pointer. when the ref's current changed, it looks like the value is not stalled.
You can verify by add:
onClick={()=>someClosure(value)}
in the <Model> element and you will see the value is updated.
This is a interesting problem, so I check the <Model> element source code in Github:
useEffect(() => {
const modalRoot = _modalRoot.current;
if (!_modalInstance.current) {
_modalInstance.current = M.Modal.init(_modalRef.current, options);
}
return () => {
if (root.contains(modalRoot)) {
root.removeChild(modalRoot);
}
_modalInstance.current.destroy();
};
// deep comparing options object
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [safeJSONStringify(options), root]);
You can find that the author use SafeJSONStringify(options) to do a deep comparing which don't care any state's value change.

Related

get current useState values inside functions (w/ short, explicit codepen)

I understand (somewhat) how to use useEffect to update state, but I struggle with situations like when you need current state inside of another function, before the "nextTick" as it were.
Here is a simple Codepen with the exact issue. Make sure the Pen console is open.
https://codepen.io/kirkbross/pen/vYRNpqG?editors=1111
const App = () => {
const [state, setState] = React.useState(null);
// how can I make sure the below function knows what the current state really is?
const handleAppend = (state) => {
console.log("click");
console.log(state?.text + " foobar");
};
return (
<div class="app">
<div className="row">
<span>Text: </span>
<input
type="text"
onChange={() => setState({ text: e.target.value })}
/>
</div>
<div className="row">
<button onClick={handleAppend}>
Append "foobar" to text and log it to console
</button>
</div>
</div>
);
};
You're shadowing your state variable in your handleAppend function. You don't need to pass in an argument since state is available in scope of the component
const handleAppend = () => {
console.log("click");
console.log(state?.text + " foobar");
};
I did some changes. You dont need to use ur state as a parameter, since your textState lives inside your app component and there for you can reach it within your function.
Also, i changed the state and setState to textState, setTextState to make it less confusing. Also after clicking on the button and console logging, i cleared the textState so the next value wont be effected. Check it out below.
function App() {
const [textState, setTextState] = React.useState(null);
const handleAppend = () => {
console.log("click");
console.log(textState + " foobar");
setTextState('')
//also, you could make the input box clear after each press on button by adding value={textState} in the input.
};
return (
<div className="App">
<input
type="text"
onChange={(e) => setTextState(e.target.value)}
/>
<button onClick={handleAppend}>
Append "foobar" to text and log it to console
</button>
</div>
);
}
My real world case was more complicated than the Pen. The actual function needing state was a useCallback function and I had forgotten to add state to the dep array of the useCallback function.
const handleDragEnd = useCallback(
async (result) => {
const { source, destination, draggableId } = result;
console.log(state); // shows up now.
},
[state], // I had forgotten to add state to the useCallback dep array
);

React state not updating when used outside hook

I'm playing around with a hook that can store some deleted values. No matter what I've tried, I can't get the state from this hook to update when I use it in a component.
const useDeleteRecords = () => {
const [deletedRecords, setDeletedRecords] = React.useState<
Record[]
>([]);
const [deletedRecordIds, setDeletedRecordIds] = React.useState<string[]>([]);
// ^ this second state is largely useless – I could just use `.filter()`
// but I was experimenting to see if I could get either to work.
React.useEffect(() => {
console.log('records changed', deletedRecords);
// this works correctly, the deletedRecords array has a new item
// in it each time the button is clicked
setDeletedRecordIds(deletedRecords.map((record) => record.id));
}, [deletedRecords]);
const deleteRecord = (record: Record) => {
console.log(`should delete record ${record.id}`);
// This works correctly - firing every time the button is clicked
setDeletedRecords(prev => [...prev, record]);
};
const wasDeleted = (record: Record) => {
// This never works – deletedRecordIds is always [] when I call this outside the hook
return deletedRecordIds.some((r) => r === record.id);
};
return {
deletedRecordIds,
deleteRecord,
wasDeleted,
} // as const <-- no change
}
Using it in a component:
const DisplayRecord = ({ record }: { record: Record }) => {
const { deletedRecordIds, wasDeleted, deleteRecord } = useDeleteRecords();
const handleDelete = () => {
// called by a button on a row
deleteRecord(record);
}
React.useEffect(() => {
console.log('should fire when deletedRecordIds changes', deletedRecordIds);
// Only fires once for each row on load? deletedRecordIds never changes
// I can rip out the Ids state and do it just with deletedRecords, and the same thing happens
}, [deletedRecordIds]);
}
If it helps, these are in the same file – I'm not sure if there's some magic to exporting a hook in a dedicated module? I also tried as const in the return of the hook but no change.
Here's an MCVE of what's going on: https://codesandbox.io/s/tender-glade-px631y?file=/src/App.tsx
Here's also the simpler version of the problem where I only have one state variable. The deletedRecords state never mutates when I use the hook in the parent component: https://codesandbox.io/s/magical-newton-wnhxrw?file=/src/App.tsx
problem
In your App (code sandbox) you call useDeleteRecords, then for each record you create a DisplayRecord component. So far so good.
function App() {
const { wasDeleted } = useDeleteRecords(); // ✅
console.log("wtf");
return (
<div className="App" style={{ width: "70vw" }}>
{records.map((record) => {
console.log("was deleted", wasDeleted(record));
return !wasDeleted(record) ? (
<div key={record.id}>
<DisplayRecord record={record} /> // ✅
</div>
) : null;
})}
</div>
);
}
Then for each DisplayRecord you call useDeleteRecords. This maintains a separate state array for each component ⚠️
const DisplayRecord = ({ record }: { record: Record }) => {
const { deletedRecords, deleteRecord } = useDeleteRecords(); // ⚠️
const handleDelete = () => {
// called by a button on a row
deleteRecord(record);
};
React.useEffect(() => {
console.log("should fire when deletedRecords changes", deletedRecords);
// Only fires once for each row on load? deletedRecords never changes
}, [deletedRecords]);
return (
<div>
<div>{record.id}</div>
<div onClick={handleDelete} style={{ cursor: "pointer" }}>
[Del]
</div>
</div>
);
};
solution
The solution is to maintain a single source of truth, keeping handleDelete and deletedRecords in the shared common ancestor, App. These can be passed down as props to the dependent components.
function App() {
const { deletedRecords, deleteRecord, wasDeleted } = useDeleteRecords(); // 👍🏽
const handleDelete = (record) => (event) { // 👍🏽 delete handler
deleteRecord(record);
};
return (
<div className="App" style={{ width: "70vw" }}>
{records.map((record) => {
console.log("was deleted", wasDeleted(record));
return !wasDeleted(record) ? (
<div key={record.id}>
<DisplayRecord
record={record}
deletedRecords={deletedRecords} // 👍🏽 pass prop
handleDelete={handleDelete} // 👍🏽 pass prop
/>
</div>
) : null;
})}
</div>
);
}
Now DisplayRecord can read state from its parent. It does not have local state and does not need to call useDeleteRecords on its own.
const DisplayRecord = ({ record, deletedRecords, handleDelete }) => {
React.useEffect(() => {
console.log("should fire when deletedRecords changes", deletedRecords);
}, [deletedRecords]); // ✅ passed from parent
return (
<div>
<div>{record.id}</div>
<div
onClick={handleDelete(record)} // ✅ passed from parent
style={{ cursor: "pointer" }}
children="[Del]"
/>
</div>
);
};
code demo
I would suggest a name like useList or useSet instead of useDeleteRecord. It's more generic, offers the same functionality, but is reusable in more places.
Here's a minimal, verifiable example. I named the delete function del because delete is a reserved word. Run the code below and click the ❌ to delete some items.
function App({ items = [] }) {
const [deleted, del, wasDeleted] = useSet([])
React.useEffect(_ => {
console.log("an item was deleted", deleted)
}, [deleted])
return <div>
{items.map((item, key) =>
<div className="item" key={key} data-deleted={wasDeleted(item)}>
{item} <button onClick={_ => del(item)} children="❌" />
</div>
)}
</div>
}
function useSet(iterable = []) {
const [state, setState] = React.useState(new Set(...iterable))
return [
Array.from(state), // members
newItem => setState(s => (new Set(s)).add(newItem)), // addMember
item => state.has(item) // isMember
]
}
ReactDOM.render(
<App items={["apple", "orange", "pear", "banana"]}/>,
document.querySelector("#app")
)
div.item { display: inline-block; border: 1px solid dodgerblue; padding: 0.25rem; margin: 0.25rem; }
[data-deleted="true"] { opacity: 0.3; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Since you are updating deletedRecordIds inside a React.useEffect, this variable will have the correct value only after the render complete. wasDeleted is a closure that capture the value of deletedRecordIds when the component renders, thus it always have a stale value. As yourself are suggesting, the correct way to do that is to use .filter() and remove the second state.
Talking about the example you provided in both cases you are defining 5 hooks: one hook for each DisplayRecord component and one for the App. Each hook define is own states, thus there are 5 deletedRecords arrays on the page. Clicking on Del, only the array inside that specific component will be updated. All other component won't be notified by the update, because the state change is internal to that specific row. The hook state in App will never change because no one is calling its own deleteRecord function.
You could solve that problem in 2 way:
Pulling up the state: The hook is called just once in the App component and the deleteRecord method is passed as parameter to every DisplayRecord component. I updated your CodeSandbox example.
Use a context: Context allows many component to share the same state.

React Context is not working as expected: Unable to change the value of shared variable

I made a context to share the value of the variable "clicked" throughout my nextjs pages, it seems to give no errors but as you can see the variable's value remains FALSE even after the click event. It does not change to TRUE. This is my first time working with context, what am I doing wrong?
I'm using typescript
PS: After the onClick event the log's number shoots up by 3 or 4, is it being executed more than once, but how?
controlsContext.tsx
import { createContext, FC, useState } from "react";
export interface MyContext {
clicked: boolean;
changeClicked?: () => void;
}
const defaultState = {
clicked: false,
}
const ControlContext = createContext<MyContext>(defaultState);
export const ControlProvider: FC = ({ children }) => {
const [clicked, setClicked] = useState(defaultState.clicked);
const changeClicked = () => setClicked(!clicked);
return (
<ControlContext.Provider
value={{
clicked,
changeClicked,
}}
>
{children}
</ControlContext.Provider>
);
};
export default ControlContext;
Model.tsx
import ControlContext from "../contexts/controlsContext";
export default function Model (props:any) {
const group = useRef<THREE.Mesh>(null!)
const {clicked, changeClicked } = useContext(ControlContext);
const handleClick = (e: MouseEvent) => {
//e.preventDefault();
changeClicked();
console.log(clicked);
}
useEffect(() => {
console.log(clicked);
}, [clicked]);
useFrame((state, delta) => (group.current.rotation.y += 0.01));
const model = useGLTF("/scene.gltf ");
return (
<>
<TransformControls enabled={clicked}>
<mesh
ref={group}
{...props}
scale={clicked ? 0.5 : 0.2}
onClick={handleClick}
>
<primitive object={model.scene}/>
</mesh>
</TransformControls>
</>
)
}
_app.tsx
import {ControlProvider} from '../contexts/controlsContext';
function MyApp({ Component, pageProps }: AppProps) {
return (
<ControlProvider>
<Component {...pageProps}
/>
</ControlProvider>
)
}
export default MyApp
Issues
You are not actually invoking the changeClicked callback.
React state updates are asynchronously processed, so you can't log the state being updated in the same callback scope as the enqueued update, it will only ever log the state value from the current render cycle, not what it will be in a subsequent render cycle.
You've listed the changeClicked callback as optional, so Typescript will warn you if you don't use a null-check before calling changeClicked.
Solution
const { clicked, changeClicked } = useContext(ControlContext);
...
<mesh
...
onClick={(event) => {
changeClicked && changeClicked();
}}
>
...
</mesh>
...
Or declare the changeClicked as required in call normally. You are already providing changeClicked as part of the default context value, and you don't conditionally include in in the provider, so there's no need for it to be optional.
export interface MyContext {
clicked: boolean,
changeClicked: () => void
}
...
const { clicked, changeClicked } = useContext(ControlContext);
...
<mesh
...
onClick={(event) => {
changeClicked();
}}
>
...
</mesh>
...
Use an useEffect hook in to log any state updates.
const { clicked, changeClicked } = useContext(ControlContext);
useEffect(() => {
console.log(clicked);
}, [clicked]);
Update
After working with you and your sandbox it needed a few tweaks.
Wrapping the index.tsx JSX code with the ControlProvider provider component so there was a valid context value being provided to the app. The UI here had to be refactored into a React component so it could itself also consume the context value.
It seems there was some issue with the HTML canvas element, or the mesh element that was preventing the Modal component from maintaining a "solid" connection with the React context. It wasn't overtly clear what the issue was here, but passing the context values directly to the Modal component as props resolved the issue with the changeClicked callback becoming undefined.
A few things -
setClicked((prev) => !prev);
instead of
setClicked(!clicked);
As it ensures it's not using stale state. Then you are also doing -
changeClicked
But it should be -
changeClicked();
Lastly, you cannot console.log(clicked) straight after calling the set state function, it will be updated in the next render

state not changing on onChange event in react?

This is my code:
function AddPost() {
const [file, setFile] = useState({})
const handleChange = (event) => {
setFile(event.target.files[0]);
console.log(file);
}
return (
<div>
<TextField type='file' onChange={handleChange} label='image' variant='outlined' />
</div>
)
}
I am not getting file info. on console, while i am selecting a file . Instead of that I am getting empty object why ?
You need to use a useEffect to besure you are doing your action after a state is updated. Here is an example :
import React, { Component } from "react";
import { render } from "react-dom";
const App = () => {
const [num, setNum] = React.useState(0);
const handleClick = () => {
setNum(1);
console.log('num =', num);
}
// Note the dependency array below
React.useEffect(() => console.log('num (useEffect) = ', num), [num]);
return (
<div>
<button onClick={handleClick}>Click</button>
</div>
);
};
render(<App />, document.getElementById("root"));
and here is repro on Stackblitz.
Here, on click, num will be 0 in the function, but it will be set to 1 in the useEffect.
Just the file is not updated yet (whitin onChange) , so you see the initial state
The state update is performed asynchronously and only visible in the next render call.
In the next render call, file will be updated to the new value and then your (also new) function handleChange will use that value.
This means in your current code the log will always be off by one (empty object, file 1, file 2 etc.) when uploading one file after each other.
The behavior is described in the documentation of the useState hook.
To fix the log, simply write
const newFile = event.target.files[0];
setFile(newFile);
console.log(newFile);

How do I use my updated variable in an already binded onClick event of another component?

I have an array of objects in which each object has an onclick event. I map my array of objects to a component. I have a button in which I change a variable's value.
If I click my button, it changes the variable's value, but when I fire the onclick event on one of my components it does not use the variable's changed value. (I'm guessing that this is normal behaviour because the event was already binded to the component when it was created.)
Although, how can I use my updated variable value in my component's onClick event?
First I declare my variables
const [myVariable, setMyVariable] = useState("first value");
const [mymap, setmymap] = useState([
{
key: 1,
name: "one",
onClick: event => handleClick(event)
},
{
key: 2,
name: "two",
onClick: event => handleClick(event)
}
]);
setVariable is my onClick event of my first button that changes my variable's value
const setVariable = () => {
setMyVariable("second value");
};
handleClick is the onClick event of my components
const handleClick = () => {
console.log(myVariable);
};
Here I return my UI with my map of MyComponent
return (
<div>
<button onClick={setVariable}>Set variable</button>
<h1>{myVariable}</h1>
{mymap.map(element => (
<MyComponent key={element.key} onclickevent={element.onClick} />
))}
</div>
);
In this very example storing the event in every mymap item looks redundant. If there is no reason for it then simpler version will work as expected:
import React, { useState, useCallback } from "react";
const MyComponent = ({onclickevent}) => (<div>
<button onClick={onclickevent}>
Use variable from MyComponent
</button>
</div>);
const MyPage = props => {
const [myVariable, setMyVariable] = useState("first value");
// handler changes only when `myVariable` changes
const clickHandler = useCallback(() => {
console.log(myVariable);
}, [myVariable]);
// removing handlers from state
const [mymap, setmymap] = useState([
{
key: 1,
name: "one"
},
{
key: 2,
name: "two"
}
]);
return (
<div>
<button onClick={() => setMyVariable("second value")}>Set another value</button>
<h1>{myVariable}</h1>
{mymap.map(element => (
<MyComponent key={element.key} onclickevent={clickHandler} />
))}
</div>
);
};
export default MyPage;
Above instead storing handler in a state and then obtaining it when iterating over state elements mymap.map(el => el.onClick) it is directly passed to children (example shows those are all the same anyway). But we also need to make sure that handler is not stale and changes depending on the value it expects:
const clickHandler = useCallback(() => {
console.log(myVariable);
}, [myVariable]);
Updated Codesandbox example provided in a question:
PouncingPoodle, I stuck with answering by myself. But I've just found
the answer which fits perfectly for your trouble
Thanks so much #Travnikov.dev for the perfect point.
Here is my solution:
useEffect(() => {
setmymap(prev => {
let update = [...prev];
for (let i = 0; i < update.length; i ++) {
update[i].onClick = (event => handleClick(event))
update[i] = {...update[i], onClick: update[i].onClick}
}
return update;
})
}, [myVariable]);
So what's happening is I have an useEffect with the dependency of myVariable. Once my value has changed, I just update my components' array of objects's onClick to exactly what is was, except this time the onClick will have the updated value.

Resources