I have the following pseudo code and my issue is that since setting the state is asynchronous, by the time the state is done updating, the event has already been fired twice and I end up with duplicate objects in my state
const userList [ userList, setUserList ] = useState([]);
const onEvent = (user) => {
//then event fires again but previous setUserList has not finished setting the state
//so it is not found in userList and then I find myself with 2 of the same users in the list
if (userList.findIndex(u => u.name=== user.name) === -1)
setUserList(userList=> [...userList, {name:user.name}]);
}
The callback function for setUserList gives you the most recent value of the userList. So perform your check inside the function, not outside of it.
setUserList(previous => {
if (previous.findIndex(u => u.name === user.name) === -1) {
return [...previous, { name: user.name }];
}
return previous; // Since this line does not change the state, the component will not rerender
});
Related
I'm using SSE to get a stream of strings, which I need to store in a useState to display it word by word. However, after I have changed the state and the component rerenders, the old state is retained OUTSIDE the eventSource.onmessage function. As soon as it comes inside it, the state gets the default value.
const [currentResp, setCurrentResp] = useState('');
const [respHistory, setRespHistory] = useState({
count: 0,
queryName: null,
queryData: [],
});
console.log('currentResp after it has been changed', currentResp);
const toggleRequest = () => {
if (query == respHistory.queryName) {
console.log('asd');
} else {
setRespHistory({
count: 1,
queryName: query,
...respHistory,
});
}
const url = 'https://somesseurl/sse';
const sse = new EventSource(url);
function getRealtimeData(data) {
console.log('Inside Function currentResp - ', currentResp);
const currentSSEText = data.choices[0].text;
setCurrentResp(currentResp + currentSSEText);
}
sse.onmessage = (e) => getRealtimeData(JSON.parse(e.data));
sse.onerror = (error) => {
console.log(error);
sse.close();
};
return () => {
sse.close();
console.log('Closed');
};
};
I want to retain the old state even inside the function, such that I keep concatinating the string stream that is coming from the SSE.
It is happening because you are using the current state reference to update the state.
setCurrentResp(currentResp + currentSSEText);
In react, the state updates are asynchronous, so when you update any state react puts that state update inside a queue, and then react will execute the state updates one by one from the queue and the state will be updated.
So when you're trying to update the state using the currentResp as the previous state value, it may not work because currentResp may not have the latest value.
So if you want to access the previous state value inside a state update, you can do it like below
setCurrentResp((prev) => {
return prev + currentSSEText
});
I am using Electron with React and I am facing a little problem.
I am creating a functional component and in the useEffect hook I subscribe to ipcRenderer to listen for when ipcMain replies.
When ipcRenderer event triggers I am unable to access the latest state updates. All state variables values inside ipcRenderer.on function contain data when the component was initially created.
In the code below customers is an array state variable. If I console.log its value every time the ipcRenderer.on is fired it is always empty. I am absolutely sure this variable is not empty inside the component's context because it contains information that is rendered in a grid. When ipcRenderer.on is triggered my grid is reset or cleared. All I am trying to do is refresh a row in the grid when ipcRenderer.on is triggered.
useEffect(() => {
// Actions triggered in response to main process replies
ipcRenderer.on(IPCConstants.UPDATE_SALE_CUSTOMER, (event: any, arg: IUpdateResponse) => {
setIsLoading(false);
if(!arg.success) {
notifyError(arg.message);
return;
}
setCustomers(
customers.map(cst => {
if(cst.CUS_CustomerId === arg.customer.CUS_CustomerId){
cst.RowId = `${generateRandomValue()}`;
cst.isActive = arg.customer.isActive;
}
return cst;
})
)
});
return () => {
ipcRenderer.removeAllListeners(IPCConstants.UPDATE_SALE_CUSTOMER);
};
}, []);
useEffect(() => {
// Actions triggered in response to main process replies
ipcRenderer.on(IPCConstants.UPDATE_SALE_CUSTOMER, (event: any, arg: IUpdateResponse) => {
setIsLoading(false);
if(!arg.success) {
notifyError(arg.message);
return;
}
setCustomers(
customers.map(cst => {
if(cst.CUS_CustomerId === arg.customer.CUS_CustomerId){
cst.RowId = `${generateRandomValue()}`;
cst.isActive = arg.customer.isActive;
}
return cst;
})
)
});
return () => {
ipcRenderer.removeAllListeners(IPCConstants.UPDATE_SALE_CUSTOMER);
};
}, [customers, otherState (if needed)]); // <= whichever state is not up to date
You need to include the proper dependencies.
the useEffect will keep up to date with these state values
alternatively if you don't need the functionality of useState (unlikely) you can use useRef and the useEffect will always have the up to date ref without passing it as a dependency.
const handleFormSubmit = (event) => {
event.preventDefault()
let match = persons.filter(person => person.name === newName)
if (!(match.length > 0 && window.confirm(`${newName} is already added to phonebook, replace the old number with a new one?`))) {
let newPerson = {name: newName, number: newNumber, id: persons[persons.length - 1].id + 1}
ContactServices
.create(newPerson) //this is a function linked to API to update the phonebook
.then(response => {
setPersons(persons.concat(response))
setFilterPersons(persons)
setNewName('')
setNewNumber('')
//the four setState above does not work but setMessage works
setMessage(`new number ${newPerson.name} is added`)
setTimeout(() => {
setMessage(null)
}, 5000)
})
}
//...rest of the code
I'm having problems figuring out why only some of my setStates don't work. setMessage works but not setPersons or setNewName. I tried passing in a function instead a new object into setState, and console.log within the callback function. It worked so it seems that the function is executed but the new state is just not saved? I cannot use useEffect in a callback function here either.
change this condition to something meaningful for javascript this condition always returns false so your code inside ** if ** never works
window.confirm(`${newName} is already added to phonebook, replace the old number with a new one?`)){
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, it runs before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
// Should show updated state -
console.log(state);
}, [state])
The callback will run only after the state has finished changing and a render has occurred.
Note: "state" in the example is interchangeable with whatever state piece you're dealing with in your case, be it persons / newName / newNumber etc..
I am fetching data inside useEffect and updating the local state when data arrives. But If I dont pass dependency array to useEffect, the state is updated but it runs infinite loop. On the other hand if I pass dependency array as empty or with state, then it does not update the state (i.e) data is not set into the local state variable from my axios request.
const [availableCars, setAvailableCars] = useState([]);
useEffect(() => {
postRequest(formData, headers).then((data) => {
console.log('Vehicles Response:', data);
console.log('Vehicles Response:', data.status);
if (data.status == 'ok') {
const vehiclesData = data.vehicles;
setAvailableCars(vehiclesData);
}
});
}, []);
You are causing an infinite loop of re-rerendering because you are always setting the response data in the state, even if it did not change, which in turn causes a re-render, then you assign to the state again, and the loop continues.
What you need to do is check for the received data against what you have in the state, and only set it when they are different.
I have no idea what your data looks like, but you can do something like:
if (data.status == 'ok' && JSON.stringify(data.vehicles) !== JSON.stringify(availableCars)) {
setAvailableCars(data.vehicles);
}
I am using JSON's stringify() here because equality between objects is done by reference in JavaScript. If you don't want to use that, you can use isEqual()
from Lodash.
What happens is due to the differences in component life cycle which were united in functional component under the useEffect hook.
Try this I hope it will solve the issue:
const [availableCars, setAvailableCars] = useState([]);
const [ready, setReady] = useState(false);
useEffect(() => {
if(ready === false) {
postRequest(formData, headers).then((data) => {
console.log('Vehicles Response:', data);
console.log('Vehicles Response:', data.status);
if (data.status == 'ok') {
const vehiclesData = data.vehicles;
setAvailableCars(vehiclesData);
}
});
setReady(true);
}
}, [ready]);
I'm very new to react and i'm confused why my state is not updated in another method of mine see example below.
fetchMovies = () => {
const self = this;
axios.get("https://api.themoviedb.org/3/trending/movie/day?api_key=XXXXXXX")
.then(function(response){
console.log(response.data)
self.setState({
collection: response.data.results
})
console.log(self.state.collection)
});
}
makeRow = () => {
console.log(this.state.collection.length);
if(this.state.collection.length !== 0) {
var movieRows = [];
this.state.collection.forEach(function (i) {
movieRows.push(<p>{i.id}</p>);
});
this.setState({
movieRow: movieRows
})
}
}
componentDidMount() {
this.fetchMovies();
this.makeRow();
}
When inside of fetchMovies function i can access collection and it has all the data but this is the part i can't understand in the makeRow function when i console log the state i would of expected the updated state to show here but it doesn't i'm even executing the functions in sequence.
Thanks in advance.
the collection is set after the async call is resolved. Even though makeRow method is called after fetchMoview, coz of async call, u will never know when the call will be resolved and collection state will be set.
There is no need to keep movieRows in the state as that is just needed for rendering. Keeping html mockup in the state is never a good idea.
So u should just call fetchMoviews in the componentDidMount and render the data in as follows:
render() {
const { collection } = this.state;
return (
<>
{
collection.map(c => <p>{c.id}</p>)
}
</>
)
}
make sure the initial value for collection in the state is [] .
The setState() documentation contains the following paragraph:
Think of setState() as a request rather than an immediate command
to update the component. For better perceived performance, React may
delay it, and then update several components in a single pass. React
does not guarantee that the state changes are applied immediately.
To access the modified state you need to use the function signature setState(updater, [callback]), so in your case it should be;
self.setState({
collection: response.data.results
}, () => { // Will be executed after state update
console.log(self.state.collection)
// Call your make row function here and remove it from componentDidMount if that is all it does.
self.makeRow()
} )