Reputation: 9023
I am trying to learn hooks and the useState
method has made me confused. I am assigning an initial value to a state in the form of an array. The set method in useState
is not working for me, both with and without the spread syntax.
I have made an API on another PC that I am calling and fetching the data which I want to set into the state.
Here is my code:
<div id="root"></div>
<script type="text/babel" defer>
// import React, { useState, useEffect } from "react";
// import ReactDOM from "react-dom";
const { useState, useEffect } = React; // web-browser variant
const StateSelector = () => {
const initialValue = [
{
category: "",
photo: "",
description: "",
id: 0,
name: "",
rating: 0
}
];
const [movies, setMovies] = useState(initialValue);
useEffect(() => {
(async function() {
try {
// const response = await fetch("http://192.168.1.164:5000/movies/display");
// const json = await response.json();
// const result = json.data.result;
const result = [
{
category: "cat1",
description: "desc1",
id: "1546514491119",
name: "randomname2",
photo: null,
rating: "3"
},
{
category: "cat2",
description: "desc1",
id: "1546837819818",
name: "randomname1",
rating: "5"
}
];
console.log("result =", result);
setMovies(result);
console.log("movies =", movies);
} catch (e) {
console.error(e);
}
})();
}, []);
return <p>hello</p>;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<StateSelector />, rootElement);
</script>
<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
Neither setMovies(result)
nor setMovies(...result)
works.
I expect the result
variable to be pushed into the movies
array.
Upvotes: 891
Views: 904781
Reputation: 509
React's useEffect has its own state/lifecycle. It's related to mutation of state, and it will not update the state until the effect is destroyed.
Just pass a single argument in parameters in useEffect params array or leave it a blank, it will work perfectly.
const [data, setData] = useState();
useEffect(() => {
setData({
countries: ["Aus", "Bad"],
projects: "projects",
regions: "regions",
});
}, []);
Alternatively, you can try React.useRef() for instant change in the React hook.
const movies = React.useRef(null);
useEffect(() => {
movies.current = "values";
console.log(movies.current);
}, []);
Upvotes: 14
Reputation: 447
As other answers above have clarified the error here, which is that useState
is asynchronous and you are trying to use the value just after setState
. It is not updating on the console.log()
part because of the asynchronous nature of setState
, it lets your further code to execute, while the value updating happens on the background. Thus you are getting the previous value. When the setState
is completed on the background it will update the value and you will have access to that value on the next render.
If anyone is interested to understand this in detail. Here is a really good Conference talk on the topic.
https://www.youtube.com/watch?v=8aGhZQkoFbQ
Upvotes: 16
Reputation: 361
Since useState is async direct replacement of state is not a good idea. so it is better to access the previous state and set new updated value.
for ex: setState((prev)=>[...prev, ...result]); //result is array of objects
Upvotes: 2
Reputation: 18284
I would like to add to what @aprillion and @shubham-khatri suggested.
@shubham-khatri suggested this:
useEffect(() => {
// action on update of movies
}, [movies]);
right ?
But my code was like this (myfunction
being triggered on a click event)
const myfunction = useEffect(async () => {
// action on update of movies
}, [movies]);
This above code was still not working. Note the difference, the difference is I have async
. In case you have async
the code will not wait for updated movies
irrespective of [movies]
, so you will have to do like this:
const myfunction = useEffect(async (mymovies) => {
// action on passed value of mymovies
}, []);
and the calling code changed from onClick={myfunction}
to onClick={() => myfunction(movies)}
Upvotes: 2
Reputation: 122
You can try to use the useEffect hook with a dependency array containing isOpen, the callback function will be executed every time the isOpen state changes, ensuring that console.log(isOpen) logs the updated state value. This way, you can correctly handle state updates and ensure that your UI reflects the current state of your component.
Upvotes: 1
Reputation: 16229
Most of the answers here are about how to update a state based on its previous value, but I don't understand how that relates to the question
The useState set method is not reflecting a change immediately
When an event that triggers a certain code, occurs, the code starts running, and when it finshes, react will check if there was a state update and if it is the case, only then the value of the useState
hook is updated and this leads to a new render in which the new value is availabe.
const [example,setExemple] = useState("")
//...
<button
onClick={() => {
const newValue = "new";
setExample(newValue);
console.log(example); // output "" and this is normal, because the component didn't rerenderd yet so the new value is not availabe yet
}}
>
Update state
</button>
Supposing we have a scenario where we have a state which depends on another state, for example we want to make an API call based on the new value of example
every time it is updated and then store the data from response in another state anotherExample
.
to achieve so we have two ways:
1. use the value of newValue
:
<button
onClick={async () => {
const newValue = "new";
const response = await axios.get(`http://127.0.0.1:5000/${newValue}`);
setExample(newValue);
setAnotherExample(response.data);
}}
>
test
</button>
since you know that example
will receive this value, you can create your logic based on it directly.
2. trigger a useEffect to run each time example
is updated by including example
in its dependency array:
<button
onClick={() => {
const newValue = "new";
setExample(newValue);
}}
>
test
</button>
useEffect(() => {
async function test(){
const response = await axios.get(`http://127.0.0.1:5000/${example}`);
setAnotherExample(response.data);
}
test();
}, [example])
so when example
is updated with the event function the component rerenders, we are now in a new different render that once finished, useEffect
will run because the value of example
is different from what is was during the last render, and since it is a new different render, the new value of example
useState hook is available here.
Note: the useEffect
hook will run anyway during the first mount.
while the first method will make all the work in one render š (a better approach) "React groups multiple state updates into a single re-render for better performance" the second method will do it in two renders, the first when example
is updated and the second when anotherExample
is updated from inside useEffect
š
since the component only rerenders when the new value of a useState
hook is different from the old one, so when newValue
is equal to example
the component will not rerender so the useEffect
will not run and anotherExample
will not be updated š (a better approach), however in the first method the API is called anyway and we don't want to do that if there is no need also if this happens anotherExample
will be updated (anotherExample
will receive the same data it already contains because it is the same REQUEST since newValue
is equal to example
) but if the response in an object or an array then, Object.is
method (that the useState
hook utilizezs), cannot detect if the new value is equal to the previous one, therefore, the component will rerender š
As it is mentioned above, each one has its advantage, so it depends on the use case.
the second method is more recommended, however the first can be more performant in some cases, for example when you are sure the code will only run when newValue
gets a new value using onChange
, or maybe when you want to use some other local variables that you will no longer have access to from inside useEffect
Upvotes: 19
Reputation: 6525
My issue wasn't really trying to access the state right away after calling the set method. I was attempting to do it in a completely different function after the rerender had happened but the update still wasn't being reflected. The target function was defined in a functional component but it was being called from a class component.
In my case, I ended up realizing that it was an issue caused by a stale closure. This was likely happening because class components do not use useState
functionality and so the class component in my code took the function passed to it and created a copy of it or something and that copy was not using the most up-to-date reference to my variable. Actual variables passed directly to the class component still got reflected properly though.
Replacing the class component with a functional component solved the issue for me.
Upvotes: 1
Reputation: 32572
I believe a super clean way would be to create a custom hook that provides the ability to pass a callback to the setter function, then it would be a 100% guarantee to do some actions exactly after the update of the state.
By taking a look at this post you can understand how to make the useStateCallback
hook. Defining a state by using the useStateCallback
would be like the following:
const [count, setCount] = useStateCallback(0);
const handleFooBar = () => {
setCount(c => c + 1, () => { // The callback function
// All actions here will be run exactly AFTER the update of the count state
})
};
Upvotes: 1
Reputation: 24620
I know that there are already very good answers. But I want to give another idea how to solve the same issue, and access the latest 'movie' state, using my module react-useStateRef it has 11,000+ weekly downloads.
As you understand by using React state you can render the page every time the state change. But by using React ref, you can always get the latest values.
So the module react-useStateRef
let you use state's and ref's together. It's backward compatible with React.useState
, so you can just replace the import
statement
const { useEffect } = React
import { useState } from 'react-usestateref'
const [movies, setMovies] = useState(initialValue);
useEffect(() => {
(async function() {
try {
const result = [
{
id: "1546514491119",
},
];
console.log("result =", result);
setMovies(result);
console.log("movies =", movies.current); // will give you the latest results
} catch (e) {
console.error(e);
}
})();
}, []);
Upvotes: 31
Reputation: 783
I just finished a rewrite with useReducer, following the Kent C. Dodds article (ref below) which really gave me a solid result that suffers not one bit from these closure problems.
See: https://kentcdodds.com/blog/how-to-use-react-context-effectively
I condensed his readable boilerplate to my preferred level of DRYness -- reading his sandbox implementation will show you how it actually works.
import React from 'react'
// ref: https://kentcdodds.com/blog/how-to-use-react-context-effectively
const ApplicationDispatch = React.createContext()
const ApplicationContext = React.createContext()
function stateReducer(state, action) {
if (state.hasOwnProperty(action.type)) {
return { ...state, [action.type]: state[action.type] = action.newValue };
}
throw new Error(`Unhandled action type: ${action.type}`);
}
const initialState = {
keyCode: '',
testCode: '',
testMode: false,
phoneNumber: '',
resultCode: null,
mobileInfo: '',
configName: '',
appConfig: {},
};
function DispatchProvider({ children }) {
const [state, dispatch] = React.useReducer(stateReducer, initialState);
return (
<ApplicationDispatch.Provider value={dispatch}>
<ApplicationContext.Provider value={state}>
{children}
</ApplicationContext.Provider>
</ApplicationDispatch.Provider>
)
}
function useDispatchable(stateName) {
const context = React.useContext(ApplicationContext);
const dispatch = React.useContext(ApplicationDispatch);
return [context[stateName], newValue => dispatch({ type: stateName, newValue })];
}
function useKeyCode() { return useDispatchable('keyCode'); }
function useTestCode() { return useDispatchable('testCode'); }
function useTestMode() { return useDispatchable('testMode'); }
function usePhoneNumber() { return useDispatchable('phoneNumber'); }
function useResultCode() { return useDispatchable('resultCode'); }
function useMobileInfo() { return useDispatchable('mobileInfo'); }
function useConfigName() { return useDispatchable('configName'); }
function useAppConfig() { return useDispatchable('appConfig'); }
export {
DispatchProvider,
useKeyCode,
useTestCode,
useTestMode,
usePhoneNumber,
useResultCode,
useMobileInfo,
useConfigName,
useAppConfig,
}
With a usage similar to this:
import { useHistory } from "react-router-dom";
// https://react-bootstrap.github.io/components/alerts
import { Container, Row } from 'react-bootstrap';
import { useAppConfig, useKeyCode, usePhoneNumber } from '../../ApplicationDispatchProvider';
import { ControlSet } from '../../components/control-set';
import { keypadClass } from '../../utils/style-utils';
import { MaskedEntry } from '../../components/masked-entry';
import { Messaging } from '../../components/messaging';
import { SimpleKeypad, HandleKeyPress, ALT_ID } from '../../components/simple-keypad';
export const AltIdPage = () => {
const history = useHistory();
const [keyCode, setKeyCode] = useKeyCode();
const [phoneNumber, setPhoneNumber] = usePhoneNumber();
const [appConfig, setAppConfig] = useAppConfig();
const keyPressed = btn => {
const maxLen = appConfig.phoneNumberEntry.entryLen;
const newValue = HandleKeyPress(btn, phoneNumber).slice(0, maxLen);
setPhoneNumber(newValue);
}
const doSubmit = () => {
history.push('s');
}
const disableBtns = phoneNumber.length < appConfig.phoneNumberEntry.entryLen;
return (
<Container fluid className="text-center">
<Row>
<Messaging {...{ msgColors: appConfig.pageColors, msgLines: appConfig.entryMsgs.altIdMsgs }} />
</Row>
<Row>
<MaskedEntry {...{ ...appConfig.phoneNumberEntry, entryColors: appConfig.pageColors, entryLine: phoneNumber }} />
</Row>
<Row>
<SimpleKeypad {...{ keyboardName: ALT_ID, themeName: appConfig.keyTheme, keyPressed, styleClass: keypadClass }} />
</Row>
<Row>
<ControlSet {...{ btnColors: appConfig.buttonColors, disabled: disableBtns, btns: [{ text: 'Submit', click: doSubmit }] }} />
</Row>
</Container>
);
};
AltIdPage.propTypes = {};
Now everything persists smoothly everywhere across all my pages
Upvotes: 13
Reputation: 281932
Much like .setState()
in class components created by extending React.Component
or React.PureComponent
, the state update using the updater provided by useState
hook is also asynchronous, and will not be reflected immediately.
Also, the main issue here is not just the asynchronous nature but the fact that state values are used by functions based on their current closures, and state updates will reflect in the next re-render by which the existing closures are not affected, but new ones are created. Now in the current state, the values within hooks are obtained by existing closures, and when a re-render happens, the closures are updated based on whether the function is recreated again or not.
Even if you add a setTimeout
the function, though the timeout will run after some time by which the re-render would have happened, the setTimeout
will still use the value from its previous closure and not the updated one.
setMovies(result);
console.log(movies) // movies here will not be updated
If you want to perform an action on state update, you need to use the useEffect
hook, much like using componentDidUpdate
in class components since the setter returned by useState
doesn't have a callback pattern
useEffect(() => {
// action on update of movies
}, [movies]);
As far as the syntax to update state is concerned, setMovies(result)
will replace the previous movies
value in the state with those available from the async request.
However, if you want to merge the response with the previously existing values, you must use the callback syntax of state updation along with the correct use of spread syntax like
setMovies(prevMovies => ([...prevMovies, ...result]));
Upvotes: 964
Reputation: 1536
Not saying to do this, but it isn't hard to do what the OP asked without useEffect.
Use a promise to resolve the new state in the body of the setter function:
const getState = <T>(
setState: React.Dispatch<React.SetStateAction<T>>
): Promise<T> => {
return new Promise((resolve) => {
setState((currentState: T) => {
resolve(currentState);
return currentState;
});
});
};
And this is how you use it (example shows the comparison between count
and outOfSyncCount
/syncCount
in the UI rendering):
const App: React.FC = () => {
const [count, setCount] = useState(0);
const [outOfSyncCount, setOutOfSyncCount] = useState(0);
const [syncCount, setSyncCount] = useState(0);
const handleOnClick = async () => {
setCount(count + 1);
// Doesn't work
setOutOfSyncCount(count);
// Works
const newCount = await getState(setCount);
setSyncCount(newCount);
};
return (
<>
<h2>Count = {count}</h2>
<h2>Synced count = {syncCount}</h2>
<h2>Out of sync count = {outOfSyncCount}</h2>
<button onClick={handleOnClick}>Increment</button>
</>
);
};
Upvotes: -1
Reputation: 4691
Without any addtional NPM package
//...
const BackendPageListing = () => {
const [ myData, setMyData] = useState( {
id: 1,
content: "abc"
})
const myFunction = ( x ) => {
setPagenateInfo({
...myData,
content: x
})
console.log(myData) // not reflecting change immediately
let myDataNew = {...myData, content: x };
console.log(myDataNew) // Reflecting change immediately
}
return (
<>
<button onClick={()=>{ myFunction("New Content")} }>Update MyData</button>
</>
)
Upvotes: 1
Reputation: 22340
Additional details to the previous answer:
While React's setState
is asynchronous (both classes and hooks), and it's tempting to use that fact to explain the observed behavior, it is not the reason why it happens.
TLDR: The reason is a closure scope around an immutable const
value.
read the value in render function (not inside nested functions):
useEffect(() => { setMovies(result) }, [])
console.log(movies)
add the variable into dependencies (and use the react-hooks/exhaustive-deps eslint rule):
useEffect(() => { setMovies(result) }, [])
useEffect(() => { console.log(movies) }, [movies])
use a temporary variable:
useEffect(() => {
const newMovies = result
console.log(newMovies)
setMovies(newMovies)
}, [])
use a mutable reference (if we don't need a state and only want to remember the value - updating a ref doesn't trigger re-render):
const moviesRef = useRef(initialValue)
useEffect(() => {
moviesRef.current = result
console.log(moviesRef.current)
}, [])
If async was the only reason, it would be possible to await setState()
.
However, both props
and state
are assumed to be unchanging during 1 render.
Treat
this.state
as if it were immutable.
With hooks, this assumption is enhanced by using constant values with the const
keyword:
const [state, setState] = useState('initial')
The value might be different between 2 renders, but remains a constant inside the render itself and inside any closures (functions that live longer even after render is finished, e.g. useEffect
, event handlers, inside any Promise or setTimeout).
Consider following fake, but synchronous, React-like implementation:
// sync implementation:
let internalState
let renderAgain
const setState = (updateFn) => {
internalState = updateFn(internalState)
renderAgain()
}
const useState = (defaultState) => {
if (!internalState) {
internalState = defaultState
}
return [internalState, setState]
}
const render = (component, node) => {
const {html, handleClick} = component()
node.innerHTML = html
renderAgain = () => render(component, node)
return handleClick
}
// test:
const MyComponent = () => {
const [x, setX] = useState(1)
console.log('in render:', x) // ā
const handleClick = () => {
setX(current => current + 1)
console.log('in handler/effect/Promise/setTimeout:', x) // ā NOT updated
}
return {
html: `<button>${x}</button>`,
handleClick
}
}
const triggerClick = render(MyComponent, document.getElementById('root'))
triggerClick()
triggerClick()
triggerClick()
<div id="root"></div>
Upvotes: 541
Reputation: 55
If we have to update state only, then a better way can be if we use the push method to do so.
Here is my code. I want to store URLs from Firebase in state.
const [imageUrl, setImageUrl] = useState([]);
const [reload, setReload] = useState(0);
useEffect(() => {
if (reload === 4) {
downloadUrl1();
}
}, [reload]);
const downloadUrl = async () => {
setImages([]);
try {
for (let i = 0; i < images.length; i++) {
let url = await storage().ref(urls[i].path).getDownloadURL();
imageUrl.push(url);
setImageUrl([...imageUrl]);
console.log(url, 'check', urls.length, 'length', imageUrl.length);
}
}
catch (e) {
console.log(e);
}
};
const handleSubmit = async () => {
setReload(4);
await downloadUrl();
console.log(imageUrl);
console.log('post submitted');
};
This code works to put URLs in state as an array. This might also work for you.
Upvotes: 3
Reputation: 31
Use the Background Timer library. It solved my problem.
const timeoutId = BackgroundTimer.setTimeout(() => {
// This will be executed once after 1 seconds
// even when the application is the background
console.log('tac');
}, 1000);
Upvotes: -3
Reputation: 363
I found this to be good. Instead of defining state (approach 1) as, example,
const initialValue = 1;
const [state,setState] = useState(initialValue)
Try this approach (approach 2),
const [state = initialValue,setState] = useState()
This resolved the rerender issue without using useEffect since we are not concerned with its internal closure approach with this case.
P.S.: If you are concerned with using old state for any use case then useState with useEffect needs to be used since it will need to have that state, so approach 1 shall be used in this situation.
Upvotes: 5
Reputation: 1057
var [state,setState]=useState(defaultValue)
useEffect(()=>{
var updatedState
setState(currentState=>{ // Do not change the state by get the updated state
updateState=currentState
return currentState
})
alert(updateState) // the current state.
})
Upvotes: 1
Reputation: 7680
The closure is not the only reason.
Based on the source code of useState
(simplified below). Seems to me the value is never assigned right away.
What happens is that an update action is queued when you invoke setValue
. And after the schedule kicks in and only when you get to the next render, these update action then is applied to that state.
Which means even we don't have closure issue, react version of useState
is not going to give you the new value right away. The new value doesn't even exist until next render.
function useState(initialState) {
let hook;
...
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = action(baseState); // setValue HERE
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending);
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}
function dispatchAction(queue, action) {
const update = {
action,
next: null
};
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
isMount = false;
workInProgressHook = fiber.memoizedState;
schedule();
}
There's also an article explaining the above in the similar way, https://dev.to/adamklein/we-don-t-know-how-react-state-hook-works-1lp8
Upvotes: 12
Reputation: 1597
With custom hooks from my library, you can wait for the state values to update:
useAsyncWatcher(...values):watcherFn(peekPrevValue: boolean)=>Promise
- is a promise wrapper around useEffect that can wait for updates and return a new value and possibly a previous one if the optional peekPrevValue
argument is set to true. import React, { useState, useEffect, useCallback } from "react";
import { useAsyncWatcher } from "use-async-effect2";
function TestComponent(props) {
const [counter, setCounter] = useState(0);
const [text, setText] = useState("");
const textWatcher = useAsyncWatcher(text);
useEffect(() => {
setText(`Counter: ${counter}`);
}, [counter]);
const inc = useCallback(() => {
(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
setCounter((counter) => counter + 1);
const updatedText = await textWatcher();
console.log(updatedText);
})();
}, []);
return (
<div className="component">
<div className="caption">useAsyncEffect demo</div>
<div>{counter}</div>
<button onClick={inc}>Inc counter</button>
</div>
);
}
export default TestComponent;
useAsyncDeepState
is a deep state implementation (similar to this.setState (patchObject)) whose setter can return a promise synchronized with the internal effect. If the setter is called with no arguments, it does not change the state values, but simply subscribes to state updates. In this case, you can get the state value from anywhere inside your component, since function closures are no longer a hindrance.import React, { useCallback, useEffect } from "react";
import { useAsyncDeepState } from "use-async-effect2";
function TestComponent(props) {
const [state, setState] = useAsyncDeepState({
counter: 0,
computedCounter: 0
});
useEffect(() => {
setState(({ counter }) => ({
computedCounter: counter * 2
}));
}, [state.counter]);
const inc = useCallback(() => {
(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
await setState(({ counter }) => ({ counter: counter + 1 }));
console.log("computedCounter=", state.computedCounter);
})();
});
return (
<div className="component">
<div className="caption">useAsyncDeepState demo</div>
<div>state.counter : {state.counter}</div>
<div>state.computedCounter : {state.computedCounter}</div>
<button onClick={() => inc()}>Inc counter</button>
</div>
);
}
Upvotes: 1
Reputation: 692
// replace
return <p>hello</p>;
// with
return <p>{JSON.stringify(movies)}</p>;
Now you should see, that your code actually does work. What does not work is the console.log(movies)
. This is because movies
points to the old state. If you move your console.log(movies)
outside of useEffect
, right above the return, you will see the updated movies object.
Upvotes: -2