Why is calling setState twice causing a single state update? - javascript

This is probably a beginner React mistake but I want to call "addMessage" twice using "add2Messages", however it only registers once. I'm guessing this has something to do with how hooks work in React, how can I make this work?
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (message) => {
setMessages(messages.concat(message));
};
const add2Messages = () => {
addMessage("Message1");
addMessage("Message2");
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => add2Messages()}>Add 2 messages</button>
</div>
);
}
I'm using React 17.0.2

When a normal form of state update is used, React will batch the multiple setState calls into a single update and trigger one render to improve the performance.
Using a functional state update will solve this:
const addMessage = (message) => {
setMessages(prevMessages => [...prevMessages, message]);
};
const add2Messages = () => {
addMessage('Message1');
addMessage('Message2');
};
More about functional state update:
Functional state update is an alternative way to update the state. This works by passing a callback function that returns the updated state to setState.
React will call this callback function with the previous state.
A functional state update when you just want to increment the previous state by 1 looks like this:
setState((previousState) => previousState + 1)
The advantages are:
You get access to the previous state as a parameter. So when the new state depends on the previous state, the parameter is helpful as it solves the problem of stale state (something that you can encounter when you use normal state update to determine the next state as the state is updated asynchronously)
State updates will not get skipped.
Better memoization of handlers when using useCallback as the dependencies can be empty most of the time:
const addMessage = useCallback((message) => {
setMessages(prevMessages => [...prevMessages, message]);
}, []);

import React from "react";
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (message) => {
setMessages(messages => [...messages, message]);
};
const add2Messages = () => {
addMessage("Message1");
addMessage("Message2");
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => add2Messages()}>Add 2 messages</button>
</div>
);
}

This is because messages still refers to the original array. It will get the new array at the next re-render, which will occur after the execution of add2Messages.
Here are 2 solutions to solve your problem :
Use a function when calling setMessages
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (message) => {
setMessages(prevMessages => prevMessages.concat(message));
};
const add2Messages = () => {
addMessage("Message1");
addMessage("Message2");
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => add2Messages()}>Add 2 messages</button>
</div>
);
}
Modify addMessage to handle multiple messages
export default function MyFunction() {
const [messages, setMessages] = React.useState([]);
const addMessage = (...messagesToAdd) => {
setMessages(prevMessages => prevMessages.concat(messagesToAdd));
// setMessages(messages.concat(messagesToAdd)); should also work
};
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message}</div>
))}
<button onClick={() => addMessage("Message1", "Message2")}>
Add 2 messages
</button>
</div>
);
}

Changing addMessage function as below will make your code work as expected
const addMessage = (message) => {
setMessages(messages => messages.concat(message));
};
Your code didn't work because in case of synchronous event handlers(add2Messages) react will do only one batch update of state instead of updating state after every setState calls. Which is why when second addMessage was called here, the messages state variable will have [] only.
const addMessage = (message) => {
setMessages(messages.concat(message));
};
const add2Messages = () => {
addMessage('Message1'); // -> [].concat("Message1") = Message1
addMessage('Message2'); // -> [].concat("Message2") = Message2
};
So if you want to alter the state value based on previous state value(especially before re-rendering), you can make use of functional updates.

Related

useState variable is one step behind its assignment

Sorry in advance if the question is a bit vague, still quite new to JS and react. Anyways, my problem is that in the following code the newFilter state hook is one step behind the event.target.value, which should have been assigned to newFilter at onChange, could anyone enlighten me why the newFilter gets updated one step later?
Output in console from console.log, when input change happens:
The code:
function App() {
const [countries, setCountries] = useState([]);
const [newFilter, setNewFilter] = useState('');
const [allCountries, setAllCountries] = useState([]);
useEffect(() => {
axios.get("https://restcountries.com/v3.1/all").then((response) => {
setAllCountries(response.data);
});
}, []);
const handleFilterChange = (event) => {
setNewFilter(event.target.value);
console.log("this is event.target.value", event.target.value)
console.log("this is the newFilter", newFilter)
if (event.target.value) {
let countriesToShow = allCountries.filter((country) =>
country.name.common.toLowerCase().match(event.target.value.toLowerCase())
);
setCountries(countriesToShow);
}
};
return (
<div>
<strong>
<p>Find countries</p>
</strong>{" "}
<input value={newFilter} onChange={handleFilterChange} />
</div>
);
}
export default App;
React state updates are asynchronous & are not run immediately (kind of like setTimeout(func , 0).
See https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous.
Hence when you update a state property using its previous value, you should use the callback argument for the state setter.
handleFilterChange = (event) => {
this.setState((state)=> {
newFilter: event.target.value,
countries: (event.target.value)?allCountries.filter(...):allCountries
});
}

State not updating until I add or remove a console log

const BankSearch = ({ banks, searchCategory, setFilteredBanks }) => {
const [searchString, setSearchString] = useState();
const searchBanks = (search) => {
const filteredBanks = [];
banks.forEach((bank) => {
if (bank[searchCategory].toLowerCase().includes(search.toLowerCase())) {
console.log(bank[searchCategory].toLowerCase());
filteredBanks.push(bank);
}
});
setFilteredBanks(filteredBanks);
};
const debounceSearch = useCallback(_debounce(searchBanks, 500), []);
useEffect(() => {
if (searchString?.length) {
debounceSearch(searchString);
} else setFilteredBanks([]);
}, [searchString, searchCategory]);
const handleSearch = (e) => {
setSearchString(e.target.value);
};
return (
<div className='flex'>
<Input placeholder='Bank Search' onChange={handleSearch} />
</div>
);
};
export default BankSearch;
filteredBanks state is not updating
banks is a grandparent state which has a lot of objects, similar to that is filteredBanks whose set method is being called here which is setFilteredBanks
if I add a console log and save or remove it the state updates
Adding or removing the console statement and saving the file, renders the function again, the internal function's state is updated returned with the (setState) callback.
(#vnm)
Adding filteredBanks to your dependency array won't do much because it is part of the lexical scope of the function searchBanks
I'm not entirely sure of the total context of this BankSearch or what it should be. What I do see is that there are some antipatterns and missing dependencies.
Try this:
export default function BankSearch({ banks, searchCategory, setFilteredBanks }) {
const [searchString, setSearchString] = useState();
const searchBanks = useCallback(
search => {
const filteredBanks = [];
banks.forEach(bank => {
if (bank[searchCategory].toLowerCase().includes(search.toLowerCase())) {
filteredBanks.push(bank);
}
});
setFilteredBanks(filteredBanks);
},
[banks, searchCategory, setFilteredBanks]
);
const debounceSearch = useCallback(() => _debounce(searchBanks, 500), [searchBanks]);
useEffect(() => {
if (searchString?.length) {
debounceSearch(searchString);
} else setFilteredBanks([]);
}, [searchString, searchCategory, setFilteredBanks, debounceSearch]);
const handleSearch = e => {
setSearchString(e.target.value);
};
return (
<div className="flex">
<Input placeholder="Bank Search" onChange={handleSearch} />
</div>
)}
It feels like the component should be a faily simple search and filter and it seems overly complicated for what it needs to do.
Again, I don't know the full context, however, I'd look into the compont architecture/structuring of the app and state.

React useEffect updating with default initialized useState

I'm trying to create an array state variable called usersInfos that I can append to whenever I do an api call, but useEffect is updating with the initial value of the userData state variable.
// api.js
import axios from 'axios';
export function fetchUserData () {
return axios.get('https://randomuser.me/api')
.then(res => {
return res;
})
.catch(err => {
console.error(err);
})
}
import { fetchUserData } from '../../src/api';
import { useState, useEffect } from 'react';
import ProfileCard from './profilecard';
export default function UserProfile() {
const [userData, setUserData] = useState();
const [usersInfos, setUsersInfos] = useState([]);
const getUserData = async () => {
let ud = await fetchUserData()
setUserData(ud);
}
useEffect(() => {
getUserData();
}, [])
useEffect(() => {
const newInfos = [
...usersInfos,
userData,
]
setUsersInfos(newInfos);
console.log(usersInfos);
}, [userData])
return (
<div className='userprofile'>
<button onClick={() => { getUserData() }}> Fetch Random User </button>
<button onClick={() => { console.log(usersInfos) }}> log usersInfos </button>
{usersInfos.map((user, idx) => {
<ProfileCard userData={user} key={idx} />
})}
</div>
)
}
I'm assuming it's something to do with the state batch updating, but I'm not sure. What would be the best practice way of doing this?
I am console logging with the button at the bottom, after the page has loaded.
When userData is initialized to (), I get console log: (2) [undefined, {…}]
When userData is initialized to (0), I get console log: (2) [0, {…}].
When userData is initialized to ([]), i get console log:(2) [Array(0), {…}]
Thanks
When the 2nd effect runs for the first time, userData is undefined because the network request isn't resolved yet. You could simply run the effect only when userData is not undefined.
useEffect(() => {
if (userData) {
const newInfos = [
...usersInfos,
userData,
]
setUsersInfos(newInfos);
}
}, [userData])
However, if the code presented in the question is almost complete, I would use the code below instead. In this way I can update both states with a single effect.
The code below is a snippet from the CodeSandbox fully functional example.
export default function UserProfile() {
const [userData, setUserData] = useState();
const [usersInfos, setUsersInfos] = useState([]);
const getUserData = useCallback(async () => {
let ud = await fetchUserData();
setUserData(ud);
setUsersInfos((prevInfos) => [...prevInfos, ud]);
}, []);
useEffect(() => {
getUserData();
}, [getUserData]);
return (
<div className="userprofile">
<button onClick={getUserData}>Fetch Random User</button>
<button
onClick={() => {
console.log(usersInfos);
}}
>
log usersInfos
</button>
{userData && (
<p>
Current user: {userData.name.first} {userData.name.last}
</p>
)}
<ul>
{usersInfos.map((user, idx) => (
<ProfileCard userData={user} key={idx} />
))}
</ul>
</div>
);
}
If you try to log userInfos just after the setter, it will log the previous state because the function inside the effect is a closure and it captures the value of the state when the effect runs. Since React state is immutable the state update does not works like an assignment but it will be updated at the next render.

react native state is updated but functions still using the initial state set at the time of mounting

In my react native functional component, the state gets updated but when I want to use this state inside a function (for e.g, to send data to API), it uses the initial state only.
imports...
const Component = ({ navigation }) => {
const [ids, setIds] = useState([1,2]);
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => <HeaderRight
onPress={() =>
console.log(ids); // this logs initial state, i.e, [1,2]
updateIdsToServerViaAPI(); // and therefore I'm unable to update ids using this method
}
/>
});
}, [navigation]);
const updateIdsToServerViaAPI = async () => {} // function that takes updated ids from state.
const onPress = async () => {
const newIds = [...ids, 3, 4];
setIds(newIds);
}
const onPressInsideComp = () => {
console.log(ids);
// here updated ids gets logged.
}
return (
<View>
<Button onPress={onPress} />
{ids.map(id => (
<Text key={id}>{id}</Text> {\* Here you will see 4 texts after pressing button, that means state gets updated*\}
)}
<Button onPress={onPressInsideComp} />
</View>
);
}
Seems like this issue happens only when functions are called inside useLayoutEffect or useEffect but when I call onPressInsideComp from the button inside the component, it logs properly!
I am badly stuck on this weird issue !!
You have only provided the navigation prop in the dependency array of your useLayoutEffect wrapper, thus the function is not recreated if the ids state changes.
You might want to create a different function, wrapped inside a useCallback which gets the ids state as a dependency and provide this function in your useLayoutEffect.
const doSomething = useCallback(() => {
console.log(ids);
updateIdsToServerViaAPI();
}, [ids])
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => <HeaderRight
onPress={() =>
doSomething(ids)
}
/>
});
}, [navigation, ids, doSomething]);
In your code, the console.log(ids) is resolved at the moment of the function definition, and not at execution time, so it takes the reference you get in the definition const [ids, setIds} = useState([1,2]).
Maybe just try to get your ids with a function of state instead of using a variable that has been defined before:
const [ids, setIds] = useState([1,2]);
const get_ids = () => this.state.ids;
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => <HeaderRight
onPress={() =>
console.log(get_ids());
updateIdsToServerViaAPI();
}
/>
});
}, [navigation]);

React Hooks multiple alerts with individual countdowns

I've been trying to build an React app with multiple alerts that disappear after a set amount of time. Sample: https://codesandbox.io/s/multiple-alert-countdown-294lc
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function TimeoutAlert({ id, message, deleteAlert }) {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
}
let _ID = 0;
function App() {
const [alerts, setAlerts] = useState([]);
const addAlert = message => setAlerts([...alerts, { id: _ID++, message }]);
const deleteAlert = id => setAlerts(alerts.filter(m => m.id !== id));
console.log({ alerts });
return (
<div className="App">
<button onClick={() => addAlert("test ")}>Add Alertz</button>
<br />
{alerts.map(m => (
<TimeoutAlert key={m.id} {...m} deleteAlert={deleteAlert} />
))}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
The problem is if I create multiple alerts, it disappears in the incorrect order. For example, test 0, test 1, test 2 should disappear starting with test 0, test 1, etc but instead test 1 disappears first and test 0 disappears last.
I keep seeing references to useRefs but my implementations don't resolve this bug.
With #ehab's input, I believe I was able to head down the right direction. I received further warnings in my code about adding dependencies but the additional dependencies would cause my code to act buggy. Eventually I figured out how to use refs. I converted it into a custom hook.
function useTimeout(callback, ms) {
const savedCallBack = useRef();
// Remember the latest callback
useEffect(() => {
savedCallBack.current = callback;
}, [callback]);
// Set up timeout
useEffect(() => {
if (ms !== 0) {
const timer = setTimeout(savedCallBack.current, ms);
return () => clearTimeout(timer);
}
}, [ms]);
}
You have two things wrong with your code,
1) the way you use effect means that this function will get called each time the component is rendered, however obviously depending on your use case, you want this function to be called once, so change it to
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
}, []);
adding the empty array as a second parameter, means that your effect does not depend on any parameter, and so it should only be called once.
Your delete alert depends on the value that was captured when the function was created, this is problematic since at that time, you don't have all the alerts in the array, change it to
const deleteAlert = id => setAlerts(alerts => alerts.filter(m => m.id !== id));
here is your sample working after i forked it
https://codesandbox.io/s/multiple-alert-countdown-02c2h
well your problem is you remount on every re-render, so basically u reset your timers for all components at time of rendering.
just to make it clear try adding {Date.now()} inside your Alert components
<button onClick={onClick}>
{message} {id} {Date.now()}
</button>
you will notice the reset everytime
so to achieve this in functional components you need to use React.memo
example to make your code work i would do:
const TimeoutAlert = React.memo( ({ id, message, deleteAlert }) => {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
},(oldProps, newProps)=>oldProps.id === newProps.id) // memoization condition
2nd fix your useEffect to not run cleanup function on every render
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
finally something that is about taste, but really do you need to destruct the {...m} object ? i would pass it as a proper prop to avoid creating new object every time !
Both answers kind of miss a few points with the question, so after a little while of frustration figuring this out, this is the approach I came to:
Have a hook that manages an array of "alerts"
Each "Alert" component manages its own destruction
However, because the functions change with every render, timers will get reset each prop change, which is undesirable to say the least.
It also adds another lay of complexity if you're trying to respect eslint exhaustive deps rule, which you should because otherwise you'll have issues with state responsiveness. Other piece of advice, if you are going down the route of using "useCallback", you are looking in the wrong place.
In my case I'm using "Overlays" that time out, but you can imagine them as alerts etc.
Typescript:
// useOverlayManager.tsx
export default () => {
const [overlays, setOverlays] = useState<IOverlay[]>([]);
const addOverlay = (overlay: IOverlay) => setOverlays([...overlays, overlay]);
const deleteOverlay = (id: number) =>
setOverlays(overlays.filter((m) => m.id !== id));
return { overlays, addOverlay, deleteOverlay };
};
// OverlayIItem.tsx
interface IOverlayItem {
overlay: IOverlay;
deleteOverlay(id: number): void;
}
export default (props: IOverlayItem) => {
const { deleteOverlay, overlay } = props;
const { id } = overlay;
const [alive, setAlive] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setAlive(false), 2000);
return () => {
clearTimeout(timer);
};
}, []);
useEffect(() => {
if (!alive) {
deleteOverlay(id);
}
}, [alive, deleteOverlay, id]);
return <Text>{id}</Text>;
};
Then where the components are rendered:
const { addOverlay, deleteOverlay, overlays } = useOverlayManger();
const [overlayInd, setOverlayInd] = useState(0);
const addOverlayTest = () => {
addOverlay({ id: overlayInd});
setOverlayInd(overlayInd + 1);
};
return {overlays.map((overlay) => (
<OverlayItem
deleteOverlay={deleteOverlay}
overlay={overlay}
key={overlay.id}
/>
))};
Basically: Each "overlay" has a unique ID. Each "overlay" component manages its own destruction, the overlay communicates back to the overlayManger via prop function, and then eslint exhaustive-deps is kept happy by setting an "alive" state property in the overlay component that, when changed to false, will call for its own destruction.

Categories

Resources