React: state contains empty object after setState on object with values - reactjs

There are certainly a lot of questions and answers about setState, and I've tried looking at them, but haven't found a problem (or solution) that resembled mine closely enough to be useful. It's still very possible that there is one out there that deals with this, and if so, please point it out, and apologies for the duplicate.
Here's the relevant code from a functional React component:
const [ state, setState ] = useState({})
useEffect(data => {
const getFromServer = axios.get('http://localhost:3030/poolInfo')
.then(res => {
console.log("RES.DATA LOOKS LIKE THIS:, ", res.data);
setState(res.data); // I also tried setState({...res.data})
console.log("IN AXIOS, STATE IS NOW: ", state);
})
.catch (err => console.error("YO YOU GOT AN ERROR IN AXIOS ", err))
},[])
Here are the results of the two console.logs above:
RES.DATA LOOKS LIKE THIS:, Object { balance: 1000000000000000000, feeAndSplit: Array [500, 20] }
IN AXIOS, STATE IS NOW: Object { }
The first console.log shows the data exactly like I would expect it. (Hitting the same API with Postman returns the same data, too.) I call setState on literally the same variable that just had data a second ago in the logs, and poof! Now it's an empty object.
I thought maybe the console.log itself was doing something weird to res.data, so I also tried commenting out the first console.log, but still got the same results.
I've wondered if maybe this has something to do with setting state inside useEffect, but if it does, I'm at a loss. (For example, this answer seems to indicate that as long as an empty array is passed in at the end of useEffect, everything should be fine and dandy.)
What happened to res.data, and/or how can I set the state to it?
Thanks!

Everything's working, except you've put your log statement in a place where it's not useful.
state is a local const. It will never change, and that's not what setState is trying to do. The purpose of calling setState is to tell react to rerender the component. When the component rerenders, a new local const will be created, which will have that new value. Code in the new render can access that new value, but code in the old render is still referring to the previous value.
So if you'd like to verify that it's rerendering with the new value, put the log statement in the body of the component so it can log when rendering:
const [ state, setState ] = useState({})
console.log("Rendering with: ", state);
useEffect(data => {
const getFromServer = axios.get('http://localhost:3030/poolInfo')
.then(res => {
console.log("RES.DATA LOOKS LIKE THIS:, ", res.data);
setState(res.data);
})
.catch (err => console.error("YO YOU GOT AN ERROR IN AXIOS ", err))
},[])

Setting a state has some delay, so in order to get the value of your state right after setState('value'); is to use React.useEffect like so:
const [state, setState] = useState();
const handleState = () => {
// Here we assume that user has clicked some button
setState('bla bla bla...');
// If I try to log the state here, I get undefined as the output
console.log(state); // ** BAD PRACTICE **
}
React.useEffect(() => {
console.log(state); // Output: 'bla bla bla...'
}, [state]); // Here I passed 'state' as dependency, whenever state changes, the body of useEffect will be called.
Think of useEffect as a callback for whatever you've passed as dependency.
In some cases you have multiple dependencies, so passing more array members is possible:
React.useEffect(() => {
// do your job
}, [state, someOtherState]);

Related

useEffect wont work on conditional change when still in execution of async function

On a page load I have two useEffects. Both are executing at load, where the first one can possibly set a state, that should trigger the second useEffect one further time. But actually it won't.
Actually it should trigger, as it executes in two cases: When i change the order of these useEffects (could be a solution, but why???), or when i comment out the void getOrPostOnWishlist();, thus when removing the async call from the useEffect. But why is that a problem here?
Here some example code snippet with some comments:
...
const setItemIdToBeHandled = (itemId: number | undefined) =>
setState((prevState) => ({...prevState, itemIdToBeHandled: itemId}));
...
// async, called on second useEffect
const getOrPostOnWishlist = async () => {
if (state.itemIdToBeHandled) {
// if there is an item to be handled, retrieve new wishlist with added item
await addItemToNewWishlist(state.itemIdToBeHandled);
} else if (!state.wishlist) {
// if no wishlist is locally present, check if wishlist exists on api
await checkForPresentWishlist();
}
};
// possibly setting state
React.useEffect(() => {
const urlItemId = UrlSearchParamsHelper.wishlistItemId;
if (urlItemId) {
console.log("found item id in url:", urlItemId);
setItemIdToBeHandled(urlItemId);
}
}, []);
// on state change, but also on load
React.useEffect(() => {
console.log("condition:", state.itemIdToBeHandled); // sticks on 'undefined'
void getOrPostOnWishlist(); // when commented out, above console will show 'undefined', and then an itemId (considering the first useEffect sets the state);
}, [state.itemIdToBeHandled]);
This led to the following output:
But when just commenting out the async call in the second useEffect, this led to:
Googled around, and also tried useCallback, but that didn't work. Doesn't seem to be the issue here, since it's somewhat not about the content of the called function, but about the very fact, that the calling useEffect is not even executed.
It feels like even without await inside the useEffect, a useEffect is still blocked, when it has executed an async function.
Or am i missing something? If some more details are needed, let me know

How do I properly set the state to a response output in React using Typescript?

I'm new to React and I'm sure this is a very simple solution. However, I can't seem to figure out how to properly set the state of a component to be the response from a REST call I make.
Here's the response object format I'm returning
export interface CoinMarketChartsAll {
oneDay: Array<CoinChartDataMapped>;
sevenDay: Array<CoinChartDataMapped>;
oneMonth: Array<CoinChartDataMapped>;
}
And here's my functional component where I fetch this data and try to set the state equal to it
const [loading, setLoading] = useState(true);
const [cryptoData, setCryptoData] = useState<CoinMarketChartsAll>();
useEffect(() => {
fetchMarketChart();
}, []);
const fetchMarketChart = async () => {
setLoading(true);
if (!cryptoData) {
getCoinMarketChart('lgcy-network').then((response) => {
setCryptoData(response);
setLoading(false);
})
.catch((error) => {
console.log(error);
setLoading(false);
});
}
}
I tried logging the output inside the if statement and cryptoData was always equal to undefined. Until I toggled the if statement to be
if (cryptoData) {
Then it would load it into the state, but when I reloaded the page it would never make it into the if statement. Can anyone please point out what I'm doing wrong? Thanks!
I tried logging the output inside the if statement and cryptoData was always equal to undefined.
That is simply because the state setter takes effect after a "tick": you can do setCryptoData(true), but cryptoData will remain undefined until that next tick.
when I reloaded the page it would never make it into the if statement
When you first changed your if condition, React Hot Module swap kicked in and updated your component, while preserving its current state, therefore your cryptoData state was already truthy.
But after page reload, you get a "fresh" component instance, which starts with a freshly initialized state, i.e. with undefined value, since you define it with no initial value (useState<CoinMarketChartsAll>(/* no initial value */))

Why is my setState not updating the state or triggering re-render?

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..

Graphql subscriptions inside a useEffect hook doesn't access latest state

I'm building a basic Slack clone. So I have a "Room", which has multiple "Channels". A user subscribes to all messages in a Room, but we only add them to the current message list if the new message is part of the user's current Channel
const [currentChannel, setCurrentChannel] = useState(null);
const doSomething = (thing) => {
console.log(thing, currentChannel)
}
useEffect(() => {
// ... Here I have a call which will grab some data and set the currentChannel
Service.joinRoom(roomId).subscribe({
next: (x) => {
doSomething(x)
},
error: (err: any) => { console.log("error: ", err) }
})
}, [])
I'm only showing some of the code here to illustrate my issue. The subscription gets created before currentChannel gets updated, which is fine, because we want to listen to everything, but then conditionally render based on currentChannel.
The issue I'm having, is that even though currentChannel gets set correctly, because it was null when the next: function was defined in the useEffect hook, doSomething will always log that currentChannel is null. I know it's getting set correctly because I'm displaying it on my screen in the render. So why does doSomething get scoped in a way that currentChannel is null? How can I get it to call a new function each time that accesses the freshest state of currentChannel each time the next function is called? I tried it with both useState, as well as storing/retrieving it from redux, nothing is working.
Actually it is related to all async actions involving javascript closures: your subscribe refers to initial doSomething(it's recreated on each render) that refers to initial currentChannel value. Article with good examples for reference: https://dmitripavlutin.com/react-hooks-stale-closures/
What can we do? I see at least 2 moves here: quick-n-dirty and fundamental.
We can utilize that useState returns exact the same(referentially same) setter function each time and it allows us to use functional version:
const doSomething = (thing) => {
setCurrentChannel(currentChannelFromFunctionalSetter => {
console.log(thing, currentChannelFromFunctionalSetter);
return currentChannelFromFunctionalSetter;
}
}
Fundamental approach is to utilize useRef and put most recent doSomething there:
const latestDoSomething = useRef(null);
...
const doSomething = (thing) => { // nothing changed here
console.log(thing, currentChannel)
}
latestDoSomething.current = doSomething; // happens on each render
useEffect(() => {
Service.joinRoom(roomId).subscribe({
next: (x) => {
// we are using latest version with closure on most recent data
latestDoSomething.current(x)
},

Infinite console logging in React.js component

I'm working on my MERN app, and when I'm logging smth in NewsPage component, it logs infinitely.
NewsPage component:
const NewsPage = ({news, fetchNews}) => {
const postNews = (title, body) => {
axios.post("http://localhost:9000/news", { title, body });
}
useEffect(() => {
fetchNews();
}, [fetchNews, postNews])
return (
<>
<AddNewsForm postNews={postNews}/>
<h1>News:</h1>
<NewsItemPage news={news} />
</>
);
};
const mapStateToProps = state => ({
news: state.news
})
export default connect(mapStateToProps, {fetchNews})(NewsPage);
Fetch news action:
export const fetchNews = () => dispatch => {
fetchRequest();
try {
const fetch = async () => {
const res = await axios.get("http://localhost:9000/news");
dispatch({
type: a.FETCH_NEWS_SUCCESS,
payload: res.data
});
}
fetch()
} catch (e) {
dispatch({
type: a.FETCH_NEWS_FAILURE,
error: e
});
}
}
It works correctly, I can fetch news from and post news to my backend, but if I log anything in console, it would be logging infinitely, and I will not get any error.
is there a way to fix this, and is this a real problem?
Its likely because whatever function the console log is located in is being used in render, which itself is a loop. Otherwise, there is no other way that I can see why it would repeat. It probably won't end up being a problem, unless the code you are executing slows down, which might cause performance issues in the future.
You're tracking fetchNews and postNews in your useEffect array, which will re-rerun fetchNews(); on every render.
Unless the values in the useEffect second argument are primitives, you need to use some deep compare methods for those: https://stackoverflow.com/a/54096391/4468021
Actually, you have wrong useEffect usage.
Effect would be called each time when component receive new props, so, it looks like this:
1) Component mounts, call function from useEffect
2) It makes API call, update data in store
3) Data passed to container, updates "dumb" component
4) Dumb component makes re-rendering, calling func from useEffect, and that's infinity loop.
In fact, It is pretty weird that you don't have memory leak.
What you can do, is:
1) Add some conditional rendering. I pretty sure, you need to call it only on initial load.
2) Add something like ImmutableJS, it would not re-render component and would not mutate store if nothing has changed

Resources