React setState() not setting state on certain items - javascript

I'm new to React and have some pretty simple code that is acting strange (I think).
The main app fetches a list of blog posts from a server, then passes them through props to a child component which spits the list out. By default, I'm trying to make the posts only show a preview like a title, and each post will have a state attached to it so I can keep track of which ones are fully shown or previewed.
I have the states set up like this:
const [posts, setPosts] = useState([])
const [postFullView, setPostFullView] = useState([])
The list initially is rendered as an empty list so nothing gets returned. When the data fetch finishes, it re-renders with all the posts.
I use useEffect for this in the child component:
useEffect(() => {
console.log('render') //Just to verify this got called
setPosts(props.posts) //Logs empty array 3 lines down,
//setPosts([4,5,6]) //Works fine, gets logged as [4,5,6]
console.log(props.posts) //Logs an array of 32 objects - so props is clearly not empty
console.log(posts) //Logs empty array as if setPosts did nothing, but logs [4,5,6] if I comment out setPosts(props.post) and use setPosts([4,5,6])
setPostFullView(posts.map(post => {return {id: post.id, view: false}}))
console.log(postFullView) //Will be empty since posts is empty
}, [props])
Hopefully, I explained clearly what I'm confused about - I can setState using a hard-coded array, but passing in props.posts does not do anything, even though it has content.

There is nothing wrong about your code, and the reason console.log(posts) spits empty array, it because setPosts(props.posts) is async call and not executed immediately, but tells react it should render again with new value for state.
Sometimes, like in your hardcoded array case, the code will work "fine", but it not guaranteed, for sure in production when code executed faster

yea its nothing wrong because the setState api is async did not show the changes in log but code is work properly and also if you need to check your state you can use React developer Tools extension for browser too see the state status
https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en

Actually, your first issue is about understanding the state changing and the re-rendering on ReactJS. why you do not use the first initial state in the first render just like below:
const YourComponent = ({ posts: initialPosts }) => {
const [posts, setPosts] = useState(initialPosts);
Also, there is no need to have the first line you can use it exactly on the second line initializing, like this:
const YourComponent = ({ posts: initialPosts }) => {
const [postFullView, setPostFullView] = useState(
({ id }) => ({ id, view: false }) // es6 arrow function with using restructuring assignment and returning the object
);
After all, when you are using a useEffect or other hooks APIs, please add the specific internal state or prop name to dependencies, no put all the props in the array of dependencies, it caused bad costs and make your project slow to run.

Related

How can I have react batch multiple setStates for fetched data from useEffect?

I am still trying to wrap my head around how react handles renders and this particular behavior has had me scratching my head all day. When i run this code, I get 3 console logs. The first is a null, as expected since useEffect didn't run yet. Next, I get the fetched worldData array from my api call as expected. However, I then get a third console log with the same said array, which leads me to believe my component is being rerendered. If I add another set state and another api call, I see yet another console log.
function App() {
const [worldData, setWorldData] = useState(null)
const [countriesData, setCountriesData] = useState(null)
useEffect(() => {
const fetchData = async () => {
const [world, countries] = await Promise.all([
fetch("https://disease.sh/v3/covid-19/countries?yesterday=true").then(res => res.json()),
fetch("https://disease.sh/v3/covid-19/all?yesterday=true").then(res => res.json())
])
.catch((err) => {
console.log(err)
})
setWorldData(world)
setCountriesData(countries)
}
fetchData();
}, []);
console.log(worldData);
It seems like react is rendering every time I set a State, which is what I assume it's designed to do. However, I've read elsewhere that react batches multiple set states together when set together in useEffect. So why is my set states not being batched then? Is it because of the asynchronous code? If so, is there a way I can fetch multiple endpoints and set all the retrieved data simultaneously so that my component only needs to rerender once?
A couple bits
(1) If you're ONLY concerned about having a single state update then you can defined a single state and update that as such
const [fetched_data, updateFetchedData] = useState({world:null, countries:null});
...
// where *world* and *countries* are variables containing your fetched data
updateFetchedData({world, countries})
...
fetched_data.world // using the fetched data somewhere
(2) HOWEVER, your code should not care about receiving multiple updates and should cater for it as such...
Your useState should define what an empty result would look like, for example instead of
const [worldData, setWorldData] = useState(null); // probably shouldn't look like this
how about
const [worldData, setWorldData] = useState([]); // this is what an empty result set looks like
And inside whatever component that uses both world and countries data ( I assume one references the other which is why you want both updates at once), just put a conditional statement that if it's not found then put some string 'Loading' or 'NA' or whatever until it's loaded.

How do I make UseEffect hook run step by step and not last?

I have UseEffect hook that fetches data from DB and I want it to run FIRST in my component, but it runs last.
How do I make it run before "console.log(titleee)"?
Code:
const [cPost, setCPost] = useState([]);
const postId = id.match.params.id;
useEffect(() => {
axios.get('http://localhost:5000/posts/'+postId)
.then(posts => {
setCPost(posts.data);
console.log("test");
})
}, []);
const titleee = cPost.title;
console.log(titleee);
I don't think that's the correct path that you want to take.
In order to show the cPost on your page after the request /posts/+postId finished you can opt-out for two following options.
You can show a "loader" to the user if the cPost data is crucial for your whole component.
const [fetchingCPost, setFetchingCPost] = useState(false)
const [cPost, setCPost] = useState({});
const postId = id.match.params.id;
useEffect(() => {
setFetchingCPost(true)
axios.get('http://localhost:5000/posts/'+postId)
.then(posts => {
setFetchingCPost(false)
setCPost(posts.data);
})
}, []);
return fetchingCPost && <div>Loading</div>
Or you can have some default values set from the start for cPost. Just to make sure that your code doesn't break. I think the first solution might be more UX acceptable.
const [cPost, setCPost] = useState({title: '', description: ''});
If you want to store title as a separate variable you can use useMemo for instance or do it via useState same as with cPost. But even then you can't "create" it after the request finishes, you can simply change its value.
In case you want to use useMemo you can make it dependent on your cPost.
const cPostTitle = useMemo(() => {
return !!cPost.title ? cPost.title : ''
}, [cPost])
You have to change you'r way of thinking when programming in react. It is important to know how react works. React does not support imperative programming, it rather support declarative and top down approach , in which case you have to declare your markup and feed it with you'r data then the only way markup changes is by means of changing you'r data. So in you'r case you are declaring a watched variable using useState hook const [cPost, setCPost] = useState([]); , this variable (cPost) has initial values of [] then react continues rendering you'r markup using initial value, to update the rendered title to something you get from a network request (eg: a rest API call) you use another hook which is called after you'r component is rendered (useEffect). Here you have chance to fetch data and update you'r state. To do so you did as following :
useEffect(() => {
axios.get('http://localhost:5000/posts/'+postId)
.then(posts => {
setCPost(posts.data);
})
}, []);
this code results in a second render because part of data is changes. Here react engine goes ahead and repaint you'r markup according data change.
If you check this sandbox you'll see two console logs , in first render title is undefined in second render it's something we got from network.
Try adding async/await and see if it works. Here is the link btw for your reference https://medium.com/javascript-in-plain-english/how-to-use-async-function-in-react-hook-useeffect-typescript-js-6204a788a435

How to reduce the number of times useEffect is called?

Google's lighthouse tool gave my app an appalling performance score so I've been doing some investigating. I have a component called Home
inside Home I have useEffect (only one) that looks like this
useEffect(() => {
console.log('rendering in here?') // called 14 times...what?!
console.log(user.data, 'uvv') // called 13 times...again, What the heck?
}, [user.data])
I know that you put the second argument of , [] to make sure useEffect is only called once the data changes but this is the main part I don't get. when I console log user.data the first 4 console logs are empty arrays. the next 9 are arrays of length 9. so in my head, it should only have called it twice? once for [] and once for [].length(9) so what on earth is going on?
I seriously need to reduce it as it must be killing my performance. let me know if there's anything else I can do to dramatically reduce these calls
this is how I get user.data
const Home = ({ ui, user }) => { // I pass it in here as a prop
const mapState = ({ user }) => ({
user,
})
and then my component is connected so I just pass it in here
To overcome this scenario, React Hooks also provides functionality called useMemo.
You can use useMemo instead useEffect because useMemo cache the instance it renders and whenever it hit for render, it first check into cache to whether any related instance has been available for given deps.. If so, then rather than run entire function it will simply return it from cache.
This is not an answer but there is too much code to fit in a comment. First you can log all actions that change user.data by replacing original root reducer temporarlily:
let lastData = {};
const logRootReducer = (state, action) => {
const newState = rootReducer(state, action);
if (newState.user.data !== lastData) {
console.log(
'action changed data:',
action,
newState.user.data,
lastData
);
lastData = newState.user.data;
}
return newState;
};
Another thing causing user.data to keep changing is when you do something like this in the reducer:
if (action.type === SOME_TYPE) {
return {
...state,
user: {
...state.user,
//here data is set to a new array every time
data: [],
},
};
}
Instead you can do something like this:
const EMPTY_DATA = [];
//... other code
data: EMPTY_DATA,
Your selector is getting user out of state and creating a new object that would cause the component to re render but the dependency of the effect is user.data so the effect will only run if data actually changed.
Redux devtools also show differences in the wrong way, if you mutate something in state the devtools will show them as changes but React won't see them as changes. When you assign a new object to something data:[] then redux won't show them as changes but React will see it as a change.

Why is my state not updating with fetched data in time for the useEffect to update my DOM with the new state?

I am new to using hooks in React. I am trying to fetch data when the component first mounts by utilizing useEffect() with a second parameter of an empty array. I am then trying to set my state with the new data. This seems like a very straightforward use case, but I must be doing something wrong because the DOM is not updating with the new state.
const [tableData, setTableData] = useState([]);
useEffect(() => {
const setTableDataToState = () => {
fetchTableData()
.then(collection => {
console.log('collection', collection) //this logs the data correctly
setTableData(collection);
})
.catch(err => console.error(err));
};
setTableDataToState();
}, []);
When I put a long enough timeout around the setTableData() call (5ms didn't work, 5s did), the accurate tableData will display as expected, which made me think it may be an issue with my fetch function returning before the collection is actually ready. But the console.log() before setTableData() is outputting the correct information-- and I'm not sure how it could do this if the data wasn't available by that point in the code.
I'm hoping this is something very simple I'm missing. Any ideas?
The second argument passed to useEffect can be used to skip an effect.
Documentation: https://reactjs.org/docs/hooks-effect.html
They go on to explain in their example that they are using count as the second argument:
"If the count is 5, and then our component re-renders with count still
equal to 5, React will compare [5] from the previous render and [5]
from the next render. Because all items in the array are the same (5
=== 5), React would skip the effect. That’s our optimization."
So you would like it to re-render but only to the point that the data changes and then skip the re-render.

Why is my component that receives props not working when I destructure out the properties, but when I use props.key it's working?

The Problem
I have an application that uses this React Redux Boilerplate: https://github.com/flexdinesh/react-redux-boilerplate
I created a new page that is connected to the injected reducer + saga.
I receive following props: posts, loading, error, loadPosts and match
When I use these directly the app is working as expected. But as soon as I start to destructure the props, the app is behaving unexpectedly.
Especially with the match props.
When I do it like this:
const SubforumPage = (props) => {
useEffect(() => {
const { id: subId } = props.match.params;
console.log('props: ', subId);
}, []);
// .... other code
}
No problem everything works.
But when I do it like this:
const SubforumPage = ({match}) => {
useEffect(() => {
const { id: subId } = match.params;
console.log('props: ', subId);
}, []);
// .... other code
}
match suddenly gets undefined!
I have really no clue what so ever why this is happening. It's the first time that I see an error like this.
This specific page is set up like this in the routing file:
<Route path="/sub/:id" component={SubforumPage} />
And it's clearly working when using (props) in the function arguments but not with ({match})
Why is this? Can please someone help me out here.
What have I tried?
I continuesly started destructuring one prop after another. At first this approach works and it's still not undefined but when I get to some props, it's different which ones, it will stop working.
I think it has to do something with how I use my useEffect() hook?
I pass an empty array so it does just run when mounting. It seems like when I refresh the page, the posts are cleared out but the useEffect doesn't run anymore, so the new posts doesn't get fetched. Because hen also the console.log inside the useEffect hook is undefined doesn't even run. But for example the loading prop in console.log outside of useEffect is indeed not undefined
(But that still does not explain why it's working with (props) as argument).
Am I just using useEffect wrong?
Many thanks
Ok guys that was completely my fault. Guess I'm too tired :D. Here is what caused the problem:
I fetch my post in the useEffect hook. I also render a component where I pass in the posts. But the posts are not available because the component has to wait for the data to come in. So I completely forgot that I have to wait for the data.
Before:
return <PostsGroup posts={posts} />;
After: (correct)
return <PostsGroup posts={posts || []} />;
I had a check in place looking like this:
if (loading) return <CircularProgress />;
(before the other return). But it doesn't matter because loading is false when the component initially renders.
So I also set the initial value from loading to true (in my initialState of the reducer). So I have now two checks in place.
Sorry guys. So stupid.

Categories