I am trying to change the state programmatically while using the useState hook.
I defined it this way:
const [data, setData] = React.useState({
text: "Hello StackOverflow!",
someNumber: 2
});
For testing purposes, I created an interval which increases someNumber every second.
setInterval(() => {
setData({ ...data, someNumber: data.someNumber + 1 });
}, 1000);
When I now mount the component, the text starts flashing very fast with different numbers. I guess the Interval is started multiple times but I don't know why. It's my first project using the new hooks.
A full example of my code can be found on code sandbox snippet
And in case the link goes down, here's the component:
import React from "react";
import TextField from "#material-ui/core/TextField";
export default function StateTextFields() {
const [data, setData] = React.useState({
text: "Hello StackOverflow!",
someNumber: 2
});
setTimeout(() => {
setData({ ...data, text: "How are you?" });
}, 1000);
setInterval(() => {
setData({ ...data, someNumber: data.someNumber + 1 });
}, 1000);
return (
<TextField
id="standard-name"
label="Name"
value={data.text + " " + data.someNumber}
margin="normal"
/>
);
}
Move your setInterval inside a useEffect. This will stop it from creating a new interval every render.
React.useEffect(() => {
setInterval(() => {
setData(prevState => ({ ...prevState, someNumber: prevState.someNumber + 1 }));
}, 1000);
setTimeout(...)
},[])
This will make it so that these functions only run one time when the component mounts.
Unconditionally calling a function that modifies state inside a functional component is like doing the same in the render of a class component. It will modify state, which causes a re-render, which will modify state, and so on infinitely.
You need to use the prevState callback to update. This is because useEffect is not listening to changes made to data like useEffect(() => {}, [data]) making data go stale. But we also don't want to change it to listen to changes, because that would cause your interval to be created infinitely again. So we use prevState which always uses the most recent copy of state.
You are setting setTimeout and setInterval on each component render call.
Your Timeouts/Intervals will get fired on every re-render. You have to put it in a useEffect which replaced lifecycle hooks like componentDidMount().
Example:
useEffect(()=>{
const myInterval = setInterval(() => {
setData({ ...data, someNumber: data.someNumber + 1 })
}, 1000);
return ()=>{myInterval.clearInterval()} //cleanup
}, []) //The empty array means "only do this once, after the first render"
There are multiple problems here.
You are setting multiple timeouts/intervals that deal with the same data. They are updating the same values over different periods of time because of the asynchronicity. For example: The timeout changes data at almost the same time as the interval. Which results in the timeout calculating 2 + 1, meanwhile the interval thinks data.someNumber is still 2 instead of 3.
When not using hooks like React.useEffect react will recreate the methods each time it rerenders the component, a.k.a. every time state changes, a.k.a. every time data changes.
As BrianThompson pointed out the following solution fixes these problems. I added some comments to explain why.
React.useEffect(() => {
setInterval(() => {
// using prevState is essential here because it negates the need for 'data' as
// that would force you to recreate the hook every time 'data' changes
setData(prevState => ({ ...prevState, someNumber: prevState.someNumber + 1 }));
}, 1000);
setTimeout(...)
},[])
Related
My goal is to set up a game loop but a simple test isn't working as expected. In the following component, I am trying the useEffect hook to increment food. I expect to see "Food: 1". Instead I see "Food: 0". When I inspect the component with the dev tools, I can see that food is 2. I've discovered that the component mounts, increments food, unmounts, mounts again and increments food once more.
I have two questions:
Can I do something about the double mount? (like prevent it or wait until the final mount with a nested component perhaps?)
Why does the displayed food count still equal zero? Is it because game inside <span>Food: {game.food}</span> still refers to the initial instance? If so, how do I get the latest instance?
Component:
import React from "react";
class Game {
food = 0;
}
export default function App() {
const [game, setGame] = React.useState(new Game());
React.useEffect(() => {
setGame((game) => {
game.food += 1;
return game;
});
});
return <span>Food: {game.food}</span>;
}
Don't Mutate State Objects
React uses reference comparisons and expects the reference of the root state object to change if any data within it has changed.
For Example:
// DON'T
setGame((game) => {
// mutate and return same object
game.food += 1;
return game;
});
// DO
setGame((current) => {
// create new object with updated food value
return {
...current,
food: current.food + 1
};
});
Using the same reference will cause components to not update as expected.
useEffect Dependency Array
A useEffect without a dependency array will trigger every time the component renders.
If you wish for the useEffect to only trigger on mount provide an empty dependency array.
For Example:
// Every Render
useEffect(() => {
alert('I trigger every render');
});
// On Mount
useEffect(() => {
alert('I trigger on mount');
}, []);
// Everytime the reference for game changes
useEffect(() => {
alert('I trigger everytime the game state is update');
}, [game]);
Conclusion
"Mount twice" probably you are using react 18 and have strict mode enabled. It will trigger useEffect twice in dev mode from docs
If you want to update the view, you should make the reference of the game variable changes (instead of changing its attrs).
Solution
const initialGame = {
food: 0
}
export default function App() {
const [game, setGame] = React.useState(initialGame);
React.useEffect(() => {
setGame((game) => {
game.food += 1;
return {...game};
});
}, []);
return <span>Food: {game.food}</span>;
}
No you should not useEffect as a loop, its execution depends on your component states and its parent component, so this leaves 3 solutions 1st while loop, 2nd requestAnimationFrame and 3rd setInterval. while loop is discouraged because it will block event loop and canceling/stopping can be tedious.
double mount ? i think its react double checking function, which does this only dev mode. Once you switch to requestAnimationFrame you won't be having that issue.
use tried mutate state and react doesn't recognizes this so it doesn't re render. solution: return new object.
updating states
useEffect(() => {
setGame((current) => {
const newState = { ...current, food: current.food + 1 }
return newState
})
}, [])
using setInterval to act as loop
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000)
return () => clearInterval(id)
}, [])
using requestAnimationFrame to act as loop
// credit: https://css-tricks.com/using-requestanimationframe-with-react-hooks/
const requestRef = React.useRef()
const animate = (time) => {
setCount((count) => count + 1)
requestRef.current = requestAnimationFrame(animate)
}
useEffect(() => {
requestRef.current = requestAnimationFrame(animate)
return () => cancelAnimationFrame(requestRef.current)
}, []) // Make sure the effect runs only once
I have a question about the "proper" (or most idiomatic) way to implement network fetch behavior in React based on a single changing property.
A simplified example of the functionality I'm building is below: I am looking to build a multi-page form that "auto-saves" a draft of form inputs as the user navigates back/forth between pages.
TL;DR - I thought useEffect hooks would be the right way to save a draft to the backend every time a url slug prop changes, but I'm running into issues, and wondering about suggestions for the "right" tool for this type of behavior.
Here is my attempt so far. My code is technically working how I want it to, but violates React's recommended hook dependency pattern (and breaks the exhaustive-deps ESLint rule).
import React from 'react';
const ALL_SLUGS = [
'apple',
'banana',
'coconut',
];
function randomUrlSlug() {
return ALL_SLUGS[Math.floor((Math.random() * ALL_SLUGS.length))];
}
// just resovles the same object passed in
const dummySaveDraftToBackend = (input) => {
return new Promise((resolve, _reject) => {
setTimeout(() => {
resolve(input);
}, 1000);
});
};
export function App() {
const [urlSlug, setUrlSlug] = React.useState(randomUrlSlug());
return (
<MyComponent urlSlug={urlSlug} setUrlSlug={setUrlSlug} />
);
}
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
// useCallback memoization is technically unnecessary as written here,
// but if i follow the linter's advice (listing handleSave as a dependency of the useEffect below), it also suggests memoizing here.
// However, complexState is also technically a dependency of this callback memo, which causes the fetch to trigger every time state changes.
//
// Similarly, moving all of this inside the effect hook, makes the hook dependent on `complexState`, which means the call to the backend happens every time a user changes input data.
const handleSave = React.useCallback(() => {
console.log('*** : start fetch');
setLoading(true);
dummySaveDraftToBackend(complexState).then((resp) => {
console.log('fetch response: ', resp);
// to keep this example simple, here we are just updating
// a dummy "responseCount", but in the actual implementation,
// I'm using a state reducer, and want to make some updates to form state based on error handling, backend validation, etc.
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState]);
// I know this triggers on mount and am aware of strategies to prevent that.
// Just leaving that behavior as-is for the simplified example.
React.useEffect(() => {
if (urlSlug) {
handleSave();
}
}, [urlSlug]); // <- React wants me to also include my memoized handleSave function here, whose reference changes every time state changes. If I include it, the fetch fires every time state changes.
return (
<div className="App">
<h2>the current slug is:</h2>
<h3>{urlSlug}</h3>
<div>the current state is:</div>
<pre>{JSON.stringify(complexState, null, 2)}</pre>
<div>
<h2>edit foo</h2>
<input value={complexState.foo} onChange={(e) => setComplexState((s) => ({ ...s, foo: e.target.value }))} disabled={loading} />
</div>
<div>
<h2>edit baz</h2>
<input value={complexState.baz} onChange={(e) => setComplexState((s) => ({ ...s, baz: e.target.value }))} disabled={loading} />
</div>
<div>
<button
type="button"
onClick={() => setUrlSlug(randomUrlSlug())}
disabled={loading}
>
click to change to a random URL slug
</button>
</div>
</div>
);
}
As written, this does what I want it to do, but I had to omit my handleSave function as a dependency of my useEffect to get it to work. If I list handleSave as a dependency, the hook then relies on complexState, which changes (and thus fires the effect) every time the user modifies input.
I'm concerned about violating React's guidance for not including dependencies. As-is, I would also need to manually prevent the effect from running on mount. But because of the warning, I'm wondering if I should not use a useEffect pattern for this, and if there's a better way.
I believe I could also manually read/write state to a ref to accomplish this, but haven't explored that in much depth yet. I have also explored using event listeners on browser popstate events, which is leading me down another rabbit hole of bugginess.
I know that useEffect hooks are typically intended to be used for side effects based on event behavior (e.g. trigger a fetch on a button click). In my use case however, I can't rely solely on user interactions with elements on the page, since I also want to trigger autosave behavior when the user navigates with their browser back/forward controls (I'm using react-router; current version of react-router has hooks for this behavior, but I'm unfortunately locked in to an old version for the project I'm working on).
Through this process, I realized my understanding might be a bit off on proper usage of hook dependencies, and would love some clarity on what the pitfalls of this current implementation could be. Specifically:
In my snippet above, could somebody clarify to me why ignoring the ESLint rule could be "bad"? Specifically, why might ignoring a dependency on some complex state can be problematic, especially since I dont want to trigger an effect when that state changes?
Is there a better pattern I could use here - instead of relying on a useEffect hook - that is more idiomatic? I basically want to implement a subscriber pattern, i.e. "do something every time a prop changes, and ONLY when that prop changes"
If all the "state" that is updated after saving it to backend is only a call count, declare this as a separate chunk of state. This eliminates creating a render loop on complexState.
Use a React ref to cache the current state value and reference the ref in the useEffect callback. This is to separate the concerns of updating the local form state from the action of saving it in the backend on a different schedule.
Ideally each useState hook's "state" should be closely related properties/values. The complexState appears to be your form data that is being saved in the backend while the responseCount is completely unrelated to the actual form data, but rather it is related to how many times the data has been synchronized.
Example:
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow' });
const [responseCount, setResponseCount] = React.useState(0);
const complexStateRef = React.useRef();
React.useEffect(() => {
complexStateRef.current = complexState;
}, [complexState]);
React.useEffect(() => {
const handleSave = async (complexState) => {
console.log('*** : start fetch');
setLoading(true);
try {
const resp = await dummySaveDraftToBackend(complexState);
console.log('fetch response: ', resp);
setResponseCount(count => count + 1);
} catch(error) {
// handle any rejected Promises, errors, etc...
} finally {
setLoading(false);
}
};
if (urlSlug) {
handleSave(complexStateRef.current);
}
}, [urlSlug]);
return (
...
);
}
This feels like a move in the wrong direction (towards more complexity), but introducing an additional state to determine if the urlSlug has changed seems to work.
export function MyComponent({ urlSlug, setUrlSlug }) {
const [slug, setSlug] = React.useState(urlSlug);
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
const handleSave = React.useCallback(() => {
if (urlSlug === slug) return // only when slug changes and not on mount
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState, urlSlug, slug]);
React.useEffect(() => {
if (urlSlug) {
handleSave();
setSlug(urlSlug)
}
}, [urlSlug, handleSave]);
Or move handleSave inside the useEffect (with additional slug check)
Updated with better semantics
export function MyComponent({ urlSlug, setUrlSlug }) {
const [autoSave, setAutoSave] = React.useState(false); // false for not on mount
React.useEffect(() => {
setAutoSave(true)
}, [urlSlug])
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
React.useEffect(() => {
const handleSave = () => {
if(!autoSave) return
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}
if (urlSlug) {
handleSave();
setAutoSave(false)
}
}, [autoSave, complexState]);
So I have this:
let total = newDealersDeckTotal.reduce(function(a, b) {
return a + b;
},
0);
console.log(total, 'tittal'); //outputs correct total
setTimeout(() => {
this.setState({ dealersOverallTotal: total });
}, 10);
console.log(this.state.dealersOverallTotal, 'dealersOverallTotal1'); //outputs incorrect total
newDealersDeckTotal is just an array of numbers [1, 5, 9] e.g.
however this.state.dealersOverallTotal does not give the correct total but total does? I even put in a timeout delay to see if this solved the problem.
any obvious or should I post more code?
setState() is usually asynchronous, which means that at the time you console.log the state, it's not updated yet. Try putting the log in the callback of the setState() method. It is executed after the state change is complete:
this.setState({ dealersOverallTotal: total }, () => {
console.log(this.state.dealersOverallTotal, 'dealersOverallTotal1');
});
In case of hooks, you should use useEffect hook.
const [fruit, setFruit] = useState('');
setFruit('Apple');
useEffect(() => {
console.log('Fruit', fruit);
}, [fruit])
setState is asynchronous. You can use callback method to get updated state.
changeHandler(event) {
this.setState({ yourName: event.target.value }, () =>
console.log(this.state.yourName));
}
Using async/await
async changeHandler(event) {
await this.setState({ yourName: event.target.value });
console.log(this.state.yourName);
}
The setState is asynchronous in react, so to see the updated state in console use the callback as shown below (Callback function will execute after the setState update)
this.setState({ email: 'test#example.com' }, () => {
console.log(this.state.email)
)}
I had an issue when setting react state multiple times (it always used default state). Following this react/github issue worked for me
const [state, setState] = useState({
foo: "abc",
bar: 123
});
// Do this!
setState(prevState => {
return {
...prevState,
foo: "def"
};
});
setState(prevState => {
return {
...prevState,
bar: 456
};
});
The setState() operation is asynchronous and hence your console.log() will be executed before the setState() mutates the values and hence you see the result.
To solve it, log the value in the callback function of setState(), like:
setTimeout(() => {
this.setState({dealersOverallTotal: total},
function(){
console.log(this.state.dealersOverallTotal, 'dealersOverallTotal1');
});
}, 10)
If you work with funcions you need to use UseEffect to deal with setState's asynchrony (you can't use the callback as you did when working with classes). An example:
import { useState, useEffect } from "react";
export default function App() {
const [animal, setAnimal] = useState(null);
function changeAnimal(newAnimal) {
setAnimal(newAnimal);
// here 'animal' is not what you would expect
console.log("1", animal);
}
useEffect(() => {
if (animal) {
console.log("2", animal);
}
}, [animal]);
return (
<div className="App">
<button onClick={() => changeAnimal("dog")} />
</div>
);
}
First console.log returns null, and the second one returns 'dog'
just add componentDidUpdate(){} method in your code, and it will work.
you can check the life cycle of react native here:
https://images.app.goo.gl/BVRAi4ea2P4LchqJ8
As well as noting the asynchronous nature of setState, be aware that you may have competing event handlers, one doing the state change you want and the other immediately undoing it again. For example onClick on a component whose parent also handles the onClick. Check by adding trace. Prevent this by using e.stopPropagation.
I had the same situation with some convoluted code, and nothing from the existing suggestions worked for me.
My problem was that setState was happening from callback func, issued by one of the components. And my suspicious is that the call was occurring synchronously, which prevented setState from setting state at all.
Simply put I have something like this:
render() {
<Control
ref={_ => this.control = _}
onChange={this.handleChange}
onUpdated={this.handleUpdate} />
}
handleChange() {
this.control.doUpdate();
}
handleUpdate() {
this.setState({...});
}
The way I had to "fix" it was to put doUpdate() into setTimeout like this:
handleChange() {
setTimeout(() => { this.control.doUpdate(); }, 10);
}
Not ideal, but otherwise it would be a significant refactoring.
So I have this:
let total = newDealersDeckTotal.reduce(function(a, b) {
return a + b;
},
0);
console.log(total, 'tittal'); //outputs correct total
setTimeout(() => {
this.setState({ dealersOverallTotal: total });
}, 10);
console.log(this.state.dealersOverallTotal, 'dealersOverallTotal1'); //outputs incorrect total
newDealersDeckTotal is just an array of numbers [1, 5, 9] e.g.
however this.state.dealersOverallTotal does not give the correct total but total does? I even put in a timeout delay to see if this solved the problem.
any obvious or should I post more code?
setState() is usually asynchronous, which means that at the time you console.log the state, it's not updated yet. Try putting the log in the callback of the setState() method. It is executed after the state change is complete:
this.setState({ dealersOverallTotal: total }, () => {
console.log(this.state.dealersOverallTotal, 'dealersOverallTotal1');
});
In case of hooks, you should use useEffect hook.
const [fruit, setFruit] = useState('');
setFruit('Apple');
useEffect(() => {
console.log('Fruit', fruit);
}, [fruit])
setState is asynchronous. You can use callback method to get updated state.
changeHandler(event) {
this.setState({ yourName: event.target.value }, () =>
console.log(this.state.yourName));
}
Using async/await
async changeHandler(event) {
await this.setState({ yourName: event.target.value });
console.log(this.state.yourName);
}
The setState is asynchronous in react, so to see the updated state in console use the callback as shown below (Callback function will execute after the setState update)
this.setState({ email: 'test#example.com' }, () => {
console.log(this.state.email)
)}
I had an issue when setting react state multiple times (it always used default state). Following this react/github issue worked for me
const [state, setState] = useState({
foo: "abc",
bar: 123
});
// Do this!
setState(prevState => {
return {
...prevState,
foo: "def"
};
});
setState(prevState => {
return {
...prevState,
bar: 456
};
});
The setState() operation is asynchronous and hence your console.log() will be executed before the setState() mutates the values and hence you see the result.
To solve it, log the value in the callback function of setState(), like:
setTimeout(() => {
this.setState({dealersOverallTotal: total},
function(){
console.log(this.state.dealersOverallTotal, 'dealersOverallTotal1');
});
}, 10)
If you work with funcions you need to use UseEffect to deal with setState's asynchrony (you can't use the callback as you did when working with classes). An example:
import { useState, useEffect } from "react";
export default function App() {
const [animal, setAnimal] = useState(null);
function changeAnimal(newAnimal) {
setAnimal(newAnimal);
// here 'animal' is not what you would expect
console.log("1", animal);
}
useEffect(() => {
if (animal) {
console.log("2", animal);
}
}, [animal]);
return (
<div className="App">
<button onClick={() => changeAnimal("dog")} />
</div>
);
}
First console.log returns null, and the second one returns 'dog'
just add componentDidUpdate(){} method in your code, and it will work.
you can check the life cycle of react native here:
https://images.app.goo.gl/BVRAi4ea2P4LchqJ8
As well as noting the asynchronous nature of setState, be aware that you may have competing event handlers, one doing the state change you want and the other immediately undoing it again. For example onClick on a component whose parent also handles the onClick. Check by adding trace. Prevent this by using e.stopPropagation.
I had the same situation with some convoluted code, and nothing from the existing suggestions worked for me.
My problem was that setState was happening from callback func, issued by one of the components. And my suspicious is that the call was occurring synchronously, which prevented setState from setting state at all.
Simply put I have something like this:
render() {
<Control
ref={_ => this.control = _}
onChange={this.handleChange}
onUpdated={this.handleUpdate} />
}
handleChange() {
this.control.doUpdate();
}
handleUpdate() {
this.setState({...});
}
The way I had to "fix" it was to put doUpdate() into setTimeout like this:
handleChange() {
setTimeout(() => { this.control.doUpdate(); }, 10);
}
Not ideal, but otherwise it would be a significant refactoring.
I have a very simple example I wrote in a class component:
setErrorMessage(msg) {
this.setState({error_message: msg}, () => {
setTimeout(() => {
this.setState({error_message: ''})
}, 5000);
});
}
So here I call the setState() method and give it a callback as a second argument.
I wonder if I can do this inside a functional component with the useState hook.
As I know you can not pass a callback to the setState function of this hook. And when I use the useEffect hook - it ends up in an infinite loop:
So I guess - this functionality is not included into functional components?
The callback functionality isn't available in react-hooks, but you can write a simple get around using useEffect and useRef.
const [errorMessage, setErrorMessage] = useState('')
const isChanged = useRef(false);
useEffect(() => {
if(errorMessage) { // Add an existential condition so that useEffect doesn't run for empty message on first rendering
setTimeout(() => {
setErrorMessage('');
}, 5000);
}
}, [isChanged.current]); // Now the mutation will not run unless a re-render happens but setErrorMessage does create a re-render
const addErrorMessage = (msg) => {
setErrorMessage(msg);
isChanged.current = !isChanged.current; // intentionally trigger a change
}
The above example is considering the fact that you might want to set errorMessage from somewhere else too where you wouldn't want to reset it. If however you want to reset the message everytime you setErrorMessage, you can simply write a normal useEffect like
useEffect(() => {
if(errorMessage !== ""){ // This check is very important, without it there will be an infinite loop
setTimeout(() => {
setErrorMessage('');
}, 5000);
}
}, [errorMessage])