Initial value in useState hook - reactjs

Is there a point in having the initial value in useState be an empty array in this case :
const [products, setProducts] = useState([]);
useEffect(() => {
axios
.get('/shoes')
.then((res) => {
setProducts(res.data);
})
.catch((err) => {
console.error(err);
});
}, []);
When should i have initial values and when not?

Using an initial state which is the same shape as the result can make working with the stateful variable later easier. For example, if you use the empty array as the initial state, later, you'll be able to do:
return (
<div>
{products.map(prod => <span>{prod.name}</span>)}
</div>
);
Whereas if you didn't use an initial state, you'd have to make sure that products existed first:
return (
<div>
{products?.map(prod => <span>{prod.name}</span>)}
</div>
);
or
return (
<div>
{products && products.map(prod => <span>{prod.name}</span>)}
</div>
);

We usually handle our error scenario, our loading scenario within our initial API call itself. And API call is made from the useEffect().
But, useEffect() hook is invoked only after the initial render of our JSX. To handle all the above scenarios it is always best to keep the initial values of state with their appropriate data types.
Secondly, we could also have many useEffect() Hooks within the same React Component handling their respective tasks because useEffect() does take 2nd argument as list of states in an array - acting as componentDidUpdate() life cycle hook, so to have the knowledge of what state and its type we are going to use in later part of application and keeping it in useState() initially allows working with the data easy.

Related

React Useeffect running when page loads

Am using useEffect in a react functional component to fetch data from an external API but it keeps calling the API endpoint on render on the page .
Am looking for a way to stop the useeffect from running on render on the component
Use the dependency array (second argument to the useEffect), so you can specify when you need to run the useEffect.
The problem here is that you have not used the dependency array, so that it executes every time. By adding a dependency array, you specify the changes where you want useEffect to run.
useEffect(()=>{
},[<dependency array: which contains the properties>]);
If you leave the dependency array empty, it will run only once. Therefore if you want the API call to run only once, add an empty array as the second argument to your useEffect. This is your solution.
Like this:
useEffect(()=>{
//Your API Call
},[]);
useEffect is always meant to run after all the changes or render effects are update in the DOM. It will not run while or before the DOM is updated. You may not have given the second argument to useEffect, which if u do not provide will cause the useEffect to execute on each and every change. Assuming you only want to call the API just once when on after the first render you should provide an empty array.
Runs on all updates, see no second argument to useEffect:
useEffect(() => { /* call API */ });
Runs when the prop or state changes, see the second argument:
useEffect(() => { /* call API */ }, [prop, state]);
Runs only once, see the empty second argument:
useEffect(() => { /* call API */ }, []);
I recommend you to read the full documentation about the React useEffect hook.
Here is a easy example of using useEffect
function functionalComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
const url = 'https://randomuser.me/api/?results=10';
fetch(url)
.then(data => {
setData(data);
})
.catch(error => console.error(error))
}, []); // it's necessary to use [] to avoid the re-rendering
return <React.Fragment>
{data !== null && (
<React.Fragment>
{data.results.map(data => (
<div>
{data.gender}
</div>
))}
</React.Fragment>
)}
</React.Fragment>;
}
Maybe in your useEffect implementation you are avoiding the [] dependencies, this is a bit hard to understand if you come from class states. This on hooks review when a state element inside the hook change, for example if you are using an element that always change like a prop that you pass throught another component you might be setting inside the dependencies or another state, if you do not need any dependency just use it empty like the example above. As you can see in the documentation sometimes the dependencies are not used, this might generate an infinite loop.

How do hooks know when to trigger (or not trigger) the body of useEffect?

Looking at the official react docs, an example is given for writing a custom hook, as below:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
function ChatRecipientPicker() {
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
<Circle color={isRecipientOnline ? 'green' : 'red'} />
<select
value={recipientID}
onChange={e => setRecipientID(Number(e.target.value))}
>
{friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))}
</select>
</>
);
}
The part I'm confused about - my understanding is useEffect(...) will trigger each time the component re-renders, which as currently described, would only happen when setRecipientID is called. But, say another state variable is added, say, const [nameFilter, setNameFilter] = useState(''). In this case, the component will re-render every time a user types into the filter, which I think will trigger the "connection" logic in useEffect.
I think this would work if useEffect took in the friendID as the 2nd param, but being new to react I don't want to assume the official docs are not written in a resilient way, which means I'm wrong and the react plumbing somehow knows to not connect each re-render - but how?
To useEffect you need to provide a callback that is going to be executed and, optionally, an array of values that will determine when the effect is triggered. Here you have three options:
You pass nothing - useEffect is triggered each time components is rendered
You pass en empty array - useEffect is triggered only once, when component is mounted
You pass an array with props and state variables - useEffect is triggered when the component is first mounted and each time at least one of the variables changes
React does not do anything else to reduce the number of times useEffect is called, so yes you do need to explicitly provide variables your effect depends on (friendID in your case)

React - issue with useEffect() and axios.get

I am struggling to understand why my code is not working. I am using the useEffect() hook to make a call to an API, and then I am using setState() to update my component state. In my JSX, I am mapping my info array to render the data.
Here's my code:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import './App.css';
function App() {
const [info, setInfo] = useState();
console.log(info);
useEffect(() => {
const getUsers = async () => {
const res = await axios('https://api.mocki.io/v1/b043df5a');
console.log(res.data);
setInfo(res.data);
};
getUsers();
}, []);
return (
<div>
<input type='text' placeholder='Search users' />
<input type='text' placeholder='Search users' />
{info.map((el, index) => {
console.log(el);
return <h1 key={index}>{el.city}</h1>;
})}
</div>
);
}
export default App;
However, I get this error: 'TypeError: Cannot read property 'map' of undefined'. My best guess is that my JSX is rendered before my state is populated with API info.
The code does work when I write the following JSX, which allows me to check if 'info' is true:
{info && info.map((el, index) => {
console.log(el);
return <h1 key={index}>{el.city}</h1>;
})}
Is this a normal behavior? Why is useEffect not populating my state before my page is rendered?
I would appreciate your help, as I am struggling to find the solution to this specific issue.
Thanks!
Just do this:
const [info, setInfo] = useState([]);
The issue is that you have no intial value and therefore it automatically defaults to undefined. Now you are trying to call .map on a value that is undefined therefore it throws an error. With an empty array as the initial value however, .map will loop over an empty array on the first render (before the useEffect) and it won't throw any error.
that's because useEffect hook will run after the dom render phase finished and one more thing that can cause the delay of getting data is the fact that you're calling an asynchronous function which usually get some time to finished.
so what are the possible options here:
just use empty array [] as default value
check the length of the state like info.length && whatever...
sometimes you can use the useLayoutEffect which is kinda a synchronous operation. but in your case which is an api calls solution 1 and 2 is the answer
You are trying to iterate over undefined it's normal cause you need to check first before data will come to your state. You can achieve this in two ways:
info && info.map
But your initial state must be falsy value like:
useState(null)
Or better way to set initial state to empty array and map will run when data will come and you will get no error.
So basicly useEffect function is equivalent of componentDidMount. What I'm trying to say your component renders and then it executes function passed in useEffect. To avoid this eighter use check that you introduced as a fix yourself or pass default value to useState method. I would suggest your option but with condition if info exists show it and if it's not then show some kind of loading indicator.
A use Effect with a [] as second can be interpreted as a 'componentDidMount'
To give a very simple answer, your code is 'executed' the first time without the useEffect. So indeed, 'info' will not exist. At this point your 'info' variable is not yet defined. Then, when your component 'is mounted', it will execute the code in your useEffect. Only then info will be filled in.
I would recommend to go through this documentation to fully understand this: https://reactjs.org/docs/state-and-lifecycle.html

Explanation needed: getting data from API with useEffect hook and get name

const [ countries, setCountries ] = useState([])
const hook = () => {
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => {
setCountries(response.data)
})
}
useEffect(hook, [])
This one below doesn't work:
//Uncaught TypeError: Cannot read property 'name' of undefined
console.log(countries[1].name)
This one below does work:
<ul>
{countries.map(country => (
<li>{country.name}</li>
))}
</ul>
Any ide why one method of printing name does work, while the other doesn't?
Coz you can loop through the empty array, but you can't access the index which is not available yet
// So if
countries = []
// this will not throw error
{countries.map(country => (
<li>{country.name}</li>
))}
// but this will
console.log(countries[1].name)
// if you want to check try to run this
console.log(countries.length ? countries[1].name : "not available yer");
The usage of useEffect hook notifies React that component has to perform some side-effects(passed as a callback function to the hook) after it has been rendered, The default behavior of useEffect will run both after the first render and after every update, but when an empty array is passed as a dependency the side-effect will be performed only once after the component has been mounted for the first time.
In the case above useEffect(hook, []) the callback hook will be called after the component has mounted for the first time, which means the component will render with the initial state on it's first render which is an empty array ([]).
That is why when you try to access countries[1].name it errors out, because the value of countries is still an empty array on the first render.
const [ countries, setCountries ] = useState([])
const hook = () => {
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => {
setCountries(response.data)
})
}
useEffect(hook, [])
// can not use index expression to get the first element because
// the value of countries is still an empty array on first render
// it only gets populated when axios.get call is succesful inside the
// callback in useEffect hook after the component has mounted for the first time
console.log(countries[1].name)
Solution
Check for the length of the array before trying to get the first element,
if (countries.length) {
console.log(countries[1].name)
}
P.S.- You should be using a .catch block for handling the error when the API call fails.
There is an example solution for a type of request like this in the React document:
https://reactjs.org/docs/hooks-effect.html
The hooks provided by React are for the most part, asynchronous functions provided by React, to help manage the loading of data, presenting it to the DOM, and dealing with updates. The useEffect behaves in a similar way to componentHasLoaded, where the hook is triggered once the functional component has rendered, and the DOM has been loaded, but it may not have been presented to the user yet. It's important to remember this when working with useEffect. useState is another asynchronous hook, but it provides access to the state property of the hook after it has been instantiated, and won't immediately trigger a re-render of the component, unless the data is updated.
The reason you get an undefined error when you attempt to access console.log(countries[1].name) is because the array at that point is still empty.
I'll explain in code:
const myComponent = () => {
// initialise countries: []
const [ countries, setCountries ] = useState([])
const hook = () => {
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => {
// This is allow you to see the DOM change after the effect has run
setTimeout(() => setCountries(response.data), 5000);
})
}
// Tell react to run useEffect once the component is loaded
useEffect(hook, [])
// Display data
return (
<p>Countries: {countries.length}<p>
);
};
Because useEffect is an asynchronous function, it doesn't block the execution of the function and the rendering of the DOM, but refreshes the DOM once useEffect is completed. In this case, you are setting the country list, based on the result of the useEffect function.
The useEffect function will still trigger, you will have access to the state, and the function will re-render when the state is updated.
See codepen example:
https://codepen.io/jmitchell38488/pen/OJMXZPv

ReactJS: Empty component's old fetched data before fetching again

I am fetching remote data from my React component. When data is ready, child components are rendered. While data is loading, the 'Data is loading ....' text is displayed.
When the component is rendered for the second time due to the props change, I set the previous data to null in order to show that new data is loading.
const List = (props) => {
const [items, setItems] = useState(null);
useEffect(() => {
setItems(null);
fetch(`http://some_url_to_fetch_items.com?PAGE=${props.page}`)
.then((data) => {
setItems(data);
})
}, [props.page]);
if (!items) {
return "Data is loading ......."
}
return (
<ul>
{items.map(item => (
<li>{item}</li>
))}
</ul>
)
}
The problem of this approach is that when the component is rendered for the second time the setItems(null); code is not executed immediately (probably because useEffect is executed asynchronously) and the component re-renderes 3 times instead of expected 2:
1-st re-render because of the props change (BUT with old data, since setItems(null) is executed too late)
2-nd re-render after setItems(null) is finally executed
3-rd re-render after data is fetched
I understand what the problem with my approach is. But I don't see another way.
It is mandatory to use hooks.
Any ideas?
A quick fix would be to add a key prop to your List:
<List key={page} page={page} />
When the value for page changes, the List component will be unmounted, and a new one rendered. The new one will not have any previous data in it, so you will only get 2 renders instead of 3.
This means you don't have to set previous data to null anymore.
If you do this, you'll need to check that the component is still mounted in your useEffect before calling setItems, otherwise you'll be trying to set state on an unmounted component. You can write:
if(setItems) {
setItems(data)
}
Changing the key prop is a bit hacky though, so I would investigate other ways to solve your issue.
Maybe you should pass the data into the list instead of the list being responsible for fetching data, and have the page state and data both inside the parent component?
For now though, changing the key using the page should work.
use this method :
useEffect(() => {
fetch(`http://some_url_to_fetch_items.com?PAGE=${props.page}`)
.then((data) => {
setItems(data);
})
}, []);
return (
<ul>
{items? items.map(item => (
<li key={item.id}>{item}</li>
)):"Data is Loading..."}
</ul>
)

Resources