React set 2 states simulatenously within useeffect - reactjs

I have a component that I use to display a list of data entries like this (simplified):
// resource is the rest endpoint,
// items is the parents components
// state that holds the data entries and setItems is the corresponding setter
export default function LoadedList({resource,items, setItems,CustomFormat}){
const [loadingOrError,setLoadingOrError] =useState(false)
useEffect(()=>{
axios.get(baseURL+resource)
.then((e)=>{
setItems(e.data)
setLoadingOrError(false)
})
.catch((e)=>{
setItems([{text:"Error"}])
setLoadingOrError(true)
})
setItems([{text:"Loading...-"}])
setLoadingOrError(true)
},[])
return(
<div className="list">
{
items.map((item)=>
loadingOrError?
<DefaultFormat item={item} />
:
<CustomFormat item={item}/>
)
}
</div>
)
}
The basic idea is, that while the component is loading item or if it fails, the default format should be used to display the corresponding message.
Once the items have successfully loaded, the format from the parent should be used to format the entries.
The problem is, that I have found out that setItems and setLoading are not changed simulatneously. The way it appears to work is that it first setItems then rerenders all the entries and only then changes loadingOrError to true. So is there a way to set both of those simulatenously? Or just without rerendering everything inbetween?

Instead of trying to update both simultaneously, why don't you try keeping track of the loading and error state separately, and then do something like this:
// resource is the rest endpoint,
// items is the parents components
// state that holds the data entries and setItems is the corresponding setter
export default function LoadedList({resource, items, setItems, CustomFormat}){
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(()=>{
setLoading(true);
axios.get(baseURL+resource)
.then((e)=>
setItems(e.data)
)
.catch((e)=>
setError("Error")
)
.finally(() => setLoading(false));
},[])
if(loading) {
return "Loading ...";
}
if(error) {
return error;
}
return(
<div className="list">
{items.map((item, index) => <CustomFormat key={index} item={item}/>)}
</div>
)
}
That should display Loading... until all items are loaded.
If you insist on wanting to leave everything as it is, and just achieve what you originally asked about updating both at the same time, you would probably need to define a function that executes the API call one level up, together with the loading state, error state and data state handling, have all those state together under the same state hook, and then pass down the API function to be used in the child's useEffect.
const [dataState, setDataState] = useState({
data: null,
loading: false,
error: ""
});
...
setDataState({data: data, loading: false});
Besides this, I recommend two things:
You should check that the component is still mounted when the request finishes and right before setting the state. Otherwise you will get an error. That's very simple to achieve with a an additional variable to keep track of the mount state.
It might be beneficial to create a custom hook for handling requests, since that's probably something you will do a lot, and it will look very similar in every case. I find the step-by-step guide in this post very clear.
Taken from that post:
useFetch custom hook
import { useState, useEffect } from 'react';
const useFetch = (url = '', options = null) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
let isMounted = true;
setLoading(true);
fetch(url, options)
.then(res => res.json())
.then(data => {
if (isMounted) {
setData(data);
setError(null);
}
})
.catch(error => {
if (isMounted) {
setError(error);
setData(null);
}
})
.finally(() => isMounted && setLoading(false));
return () => (isMounted = false);
}, [url, options]);
return { loading, error, data };
};
export default useFetch;

Related

React useEffect upon adding a dependacy array nothing renders and funtion doesnt work

This code works fine with finally preventing memory leak warning, however when I use fetchingPokemons in the depandancy array nothing renders on the screen. I would like to use it in dependancy array because I believe Its good practice to fill the dependancy array.
const usePokemons = (): IusePokemons => {
const [isLoading, setLoading] = useState(false);
const [data, setData] = useState<IPokemon[]>();
const [error, setError] = useState('');
const abortController = new AbortController();
const fetchingPokemons = async () => {
setLoading(true);
try {
const response = await fetch(urls.pokemonDataUrl, {
signal: abortController.signal,
});
const json = await response.json();
setData(await json.results);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
useEffect(
() => {
fetchingPokemons();
return () => {
abortController.abort();
};
},
//if I put fetchingPokemons inside dependancy array
//then fetched pokemons dont show up on screen why is that?
[fetchingPokemons],
);
return {data, error, isLoading};
};
export default usePokemons;
You do not always need to fill the dependency array with something, but only variables on which the effect depends.
The issue in your case is that fetchingPokemons is redefined each time the component renders, which keeps triggering the effect.
In order to avoid that problem, you can wrap the fetchingPokemons function using the useCallback hook, which would memoize it and ensures that you have a single instance, thus not triggering the effect on every render:
const fetchingPokemons = useCallback(async () => {
setLoading(true);
try {
const response = await fetch(urls.pokemonDataUrl, {
signal: abortController.signal,
});
const json = await response.json();
setData(await json.results);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
}, []);
You don't need to fill the dependency array every time. An empty dependency array makes sure that the useEffect will run on the initial render. If you are not using any state variable or prop inside the useEffect, you don't need to fill out the dependency array.
useEffect(() => fetchSomething(), []) //Will run once
useEffect(() => fetchSomething(props.id), [props.id]) //Will run every time props.id changes
In the second example, it makes much sense you would want to execute fetchSomething everytime props.id changes.
Example
const {useEffect, useState} = React
const App = () => {
const [counter, setCounter] = useState(0)
useEffect(() => {
console.log("runs once")
}, [])
useEffect(() => {
console.log("rouns every time counter changes", counter)
}, [counter])
return <button onClick={() => setCounter(c => c + 1)}> Click me </button>
}
ReactDOM.render(<App />, document.getElementById('react'))
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
dependency array is ment to run when the value of the variable changes(just like a watcher)... well i dont see a reason why you should add a function to the dependency array...
what i will suggest is that...
if you want the useEffect function to be triggered when a certain data attribute in your app changes then input the value there instead of the fetchingPokemons
Else if you want the useEffect function to run once then leave the array empty
The dependency array should be filled only with state variables created via React.useState(), because they force the uesEffect to run when the state changes. You can choose only from your state variables, that is: isLoading, data and error to handle their change. Putting your custom arrow function there is not going to work under any circumstances.

React wait for fetch data as part of custom hook

In my React functional component, I have the following code;
const user = useFetch('api/userinfo', {});
Essentially, this is a custom hook call and internally it has a fetch call to the API and sets the data (below is relevant code inside usefetch);
const [data, setData] = useState(initialData);
//....fetch call
setData(json); // once data is fetched
In my main component, since my grid depends on this data, how do I make the code wait to proceed to the Grid jsx till data is fetched? I was planning to use async..await. But not sure if it is possible to do that here with custom hooks?
With below code, seems like the hooks is getting invoked multiple times for some reason;
export default function useFetch(initialUrl, initialData) {
const [url] = useState(initialUrl);
const [loadingData, setLoadingData] = useState(true);
const [data, setData] = useState(initialData);
useEffect(() => {
setLoadingData(true);
fetch(url)
.then(response => {
if (response.status === 200) {
response.json().then(json => {
setData(json);
setLoadingData(false);
});
})
}, [url]);
return [loadingData, data];
}
A couple options for you:
Use another state variable (ie some boolean) and use that to keep track of whether or not the data comes back from the API. Then conditionally render some 'loading' element
Check to see if the data exists and conditionally render based on its existence.
Here's how you can do it with your custom hook:
// defining useFetch hook
const useFetch = (url) => {
// state to keep track of loading
const [loadingData, setLoadingData] = useState(false);
// state for data itself
const [data, setData] = useState(null);
// effect to fetch data
useEffect(() => {
const fetchData = async () => {
try {
// set data to loading
setLoadingData(true);
// request to load data, you can use fetch API too
const { data } = await axios.get(url);
// set data in state and loading to false
setLoadingData(false);
setData(data);
} catch (error) {
console.log("error", error);
}
};
fetchData();
}, [url]);
// return the data and loading state from this hook
return [loadingData, data];
};
Now, you can use this hook in your component like:
const MyComponent = (props) => {
const [isDataLoading, data] = useFetch('/api/some-url');
// now check if data is loading, if loading then return a loader/spinner
if (isDataLoading || !data) return <p>Data is loading...</p>
// otherwise render your actual component
return (
<div>
<h1>This is my component with data</h1>
</div>
);
}

ReactNative: how to refresh on data

I'm new to React Native code building.
Below is my React Native code to get data from Firebase.
const page_one = () => {
const [isLoading, setIsLoading] = useState(true)
const [placeList, setPlaceList] = useState([])
const [message, setMessage] = useState(false)
const db = firebase.firestore().collection('places')
const onLoad = async () => {
const place_ref = await firebase.firestore().collection('places').get()
if (place_ref.empty) {
setMessage(true)
return
}
const places = []
try {
place_ref.forEach(doc => {
const entity = doc.data()
entity.id = doc.id
places.push(entity)
})
setPlaceList(places)
setMessage(false)
setIsLoading(false)
return
} catch (error) {
console.log("Error:\n", error.message)
return
}
}
}
useEffect(() => {
onLoad()
console.log('place List')
}, [isLoading])
return (<View></View>)
}
I need to refresh the current component every time I render, to get newly added data from firebase. How to make possible this.
As of now component is not loading when I rendering the component 2nd time. it fetches the old data only, not loading the latest data still I refreshing the whole application.
I need to fetch the latest data whenever I render the component.
I tried with below useEffect hook:
useEffect(() => {
onLoad()
console.log('place List')
}, [isLoading, placeList])
But it calls the firebase request n number of times till I existing the current component.
I want to call the firebase request only once when ever I visiting the current component.
please help me..
As far as I understand you need to refresh whenever this component gets focused
So for that, write like this
useEffect(() => {
const unsubscribe = navigation.addListener("focus", () => {
onLoad() // Gets fired whenever this screen is in focus
});
return unsubscribe;
}, [navigation]);
Also don't forget to destructure the props to get the navigation prop
Like this
const page_one = ({ navigation }) => {
...Code Inside
}

How can I make react's useEffect to stop rerender in an infinite loop?

So I am working on a small personal project that is a todo app basically, but with a backend with express and mongo. I use useEffect() to make an axios get request to the /all-todos endpoint that returns all of my todos. Then inside of useEffect()'s dependency array I specify the todos array as dependency, meaning that I want the useEffect() to cause a rerender anytime the tasks array gets modified in any way. Take a look at this:
export default function () {
const [todos, setTodos] = useState([]);
const currentUser = JSON.parse(localStorage.getItem('user'))._id;
useEffect(() => {
function populateTodos () {
axios.get(`http://localhost:8000/api/all-todos/${currentUser}`)
.then(res => setTodos(res.data))
.catch(err => console.log(err));
}
populateTodos();
}, [todos]);
console.log(todos);
return (
<div className="App">
...
</div>
);
}
So I placed that console.log() there to give me a proof that the component gets rerendered, and the problem is that the console.log() gets printed to the console forever, until the browser gets slower and slower.
How can I make it so that the useEffect() gets triggered obly when todos gets changed?
You should execute the hook only if currentUser changes:
export default function () {
const [todos, setTodos] = useState([]);
const [error, setError] = useState(null);
const currentUser = JSON.parse(localStorage.getItem('user'))._id;
useEffect(() => {
function populateTodos () {
axios.get(`http://localhost:8000/api/all-todos/${currentUser}`)
.then(res => setTodos(res.data))
.catch(err => setError(err));
}
populateTodos();
}, [currentUser]);
console.log(todos);
if (error) return (
<div className="App">
There was an error fetching resources: {JSON.stringify(error)}
</div>
)
return (
<div className="App">
...
</div>
);
}

Hooks in react are not updating my state correctly

I'm trying to load some data which I get from an API in a form, but I seem to be doing something wrong with my state hook.
In the code below I'm using hooks to define an employee and employeeId.
After that I'm trying to use useEffect to mimic the componentDidMount function from a class component.
Once in here I check if there are params in the url and I update the employeeId state with setEmployeeId(props.match.params.employeeId).
The issue is, my state value didn't update and my whole flow collapses.
Try to keep in mind that I rather use function components for this.
export default function EmployeeDetail(props) {
const [employeeId, setEmployeeId] = useState<number>(-1);
const [isLoading, setIsLoading] = useState(false);
const [employee, setEmployee] = useState<IEmployee>();
useEffect(() => componentDidMount(), []);
const componentDidMount = () => {
// --> I get the correct id from the params
if (props.match.params && props.match.params.employeeId) {
setEmployeeId(props.match.params.employeeId)
}
// This remains -1, while it should be the params.employeeId
if (employeeId) {
getEmployee();
}
}
const getEmployee = () => {
setIsLoading(true);
EmployeeService.getEmployee(employeeId) // --> This will return an invalid employee
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}
return (
<div>
...
</div>
)
}
The new value from setEmployeeId will be available probably in the next render.
The code you're running is part of the same render so the value won't be set yet.
Since you're in the same function, use the value you already have: props.match.params.employeeId.
Remember, when you call set* you're instructing React to queue an update. The update may happen when React decides.
If you'd prefer your getEmployee to only run once currentEmployeeId changes, consider putting that in its own effect:
useEffect(() => {
getEmployee(currentEmployeeId);
}, [currentEmployeeId])
The problem seems to be that you are trying to use the "updated" state before it is updated. I suggest you to use something like
export default function EmployeeDetail(props) {
const [employeeId, setEmployeeId] = useState<number>(-1);
const [isLoading, setIsLoading] = useState(false);
const [employee, setEmployee] = useState<IEmployee>();
useEffect(() => componentDidMount(), []);
const componentDidMount = () => {
// --> I get the correct id from the params
let currentEmployeeId
if (props.match.params && props.match.params.employeeId) {
currentEmployeeId = props.match.params.employeeId
setEmployeeId(currentEmployeeId)
}
// This was remaining -1, because state wasn't updated
if (currentEmployeeId) {
getEmployee(currentEmployeeId);
//It's a good practice to only change the value gotten from a
//function by changing its parameter
}
}
const getEmployee = (id: number) => {
setIsLoading(true);
EmployeeService.getEmployee(id)
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}
return (
<div>
...
</div>
)
}
The function returned from useEffect will be called on onmount. Since you're using implicit return, that's what happens in your case. If you need it to be called on mount, you need to call it instead of returning.
Edit: since you also set employee id, you need to track in the dependency array. This is due to the fact that setting state is async in React and the updated state value will be available only on the next render.
useEffect(() => {
componentDidMount()
}, [employeeId]);
An alternative would be to use the data from props directly in the getEmployee method:
useEffect(() => {
componentDidMount()
}, []);
const componentDidMount = () => {
if (props.match.params && props.match.params.employeeId) {
setEmployeeId(props.match.params.employeeId)
getEmployee(props.match.params.employeeId);
}
}
const getEmployee = (employeeId) => {
setIsLoading(true);
EmployeeService.getEmployee(employeeId);
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}

Resources