React wait for fetch data as part of custom hook - reactjs

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>
);
}

Related

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
}

React custom hooks taking dynamic data properties

So I've read these blog posts about using custom hooks to fetch data, so for instance we have a custom hook doing the API call, setting the data, possible errors as well as the spinny isFetching boolean:
export const useFetchTodos = () => {
const [data, setData] = useState();
const [isFetching, setIsFetching] = useState(false);
const [error, setError] = useState();
useEffect(() => {
setIsFetching(true);
axios.get('api/todos')
.then(response => setData(response.data)
.catch(error => setError(error.response.data)
.finally(() => setFetching(false);
}, []);
return {data, isFetching, error};
}
And then at the top level of our component we would just call const { data, error, fetching } = useFetchTodos(); and all great we render our component with all the todos fetched.
The thing I don't understand is how would we send dynamic data / parameters to the hook based on the internal state of the component, without breaking the rules of hooks?
For instance, imagine we have a useFetchTodoById(id) hook defined the same way as the above one, how would we pass that id around? Let's say our TodoList component which renders our Todos is the following:
export const TodoList = (props) => {
const [selectedTodo, setSelectedTodo] = useState();
useEffect(() => {
useFetchTodoById(selectedTodo.id) --> INVALID HOOK CALL, cannot call custom hooks from useEffect,
and also need to call our custom hooks at the "top level" of our component
}, [selectedTodo]);
return (<ul>{props.todos.map(todo => (
<li onClick={() => setSelectedTodo(todo.id)}>{todo.name}</li>)}
</ul>);
}
I know for this specific usecase we could pass our selectedTodo through props and call our useFetchTodoById(props.selectedTodo.id) at the top of our component, but I'm just illustrating the issue with this pattern I ran into, we won't always have the luxury of receiving the dynamic data that we need in the props.
Also -- how would we apply this pattern for POST/PUT/PATCH requests which take dynamic data properties?
You should have a basic useFetch hook the accepts a url, and fetches whenever the url changes:
const useFetch = (url) => {
const [data, setData] = useState();
const [isFetching, setIsFetching] = useState(false);
const [error, setError] = useState();
useEffect(() => {
if(!url) return;
setIsFetching(true);
axios.get(url)
.then(response => setData(response.data))
.catch(error => setError(error.response.data))
.finally(() => setFetching(false));
}, [url]);
return { data, isFetching, error };
};
Now you can create other custom hook from this basic hook:
const useFetchTodos = () => useFetch('api/todos');
And you can also make it respond to dynamic changes:
const useFetchTodoById = id => useFetch(`api/todos/${id}`);
And you can use it in the component, without wrapping it in useEffect:
export const TodoList = (props) => {
const [selectedTodo, setSelectedTodo] = useState();
const { data, isFetching, error } = useFetchTodoById(selectedTodo.id);
return (
<ul>{props.todos.map(todo => (
<li onClick={() => setSelectedTodo(todo.id)}>{todo.name}</li>)}
</ul>
);
};

React set 2 states simulatenously within useeffect

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;

Destructurize fetched data react

Context
All of my components need to fetch data.
How I fetch
Therefore I use a custom hook which fetches the data using the useEffect hook and axios. The hook returns data or if loading or on error false. The data is an object with mostly an array of objects.
How I render
I render my data conditional with an ternary (?) or the use of the short circuit (&&) operator.
Question
How can I destructure my data dependent if my useFetch hook is returning false or the data in a way i can reuse the logic or an minimal implementation to the receiving component?
What I have tried
moving the destructuring assignment into an if statement like in return. Issue: "undefined" errors => data was not available yet
moving attempt 1 to function. Issue: function does not return variables (return statement does not work either)
//issue
fuction Receiver() {
const query = headerQuery();
const data = useFetch(query);
const loaded = data.data //either ```false``` or object with ```data```
/*
The following part should be in an easy condition or passed to an combined logic but I just dont get it
destructuring assignment varies from component to component
ex:
const {
site,
data: {
subMenu: {
description,
article_galleries,
image: {
caption,
image: [{url}],
},
},
},
} = data;
*/
return loaded?(
<RichLink
title={title}
text={teaserForText}
link={link}
key={id}
></RichLink>):<Loading />
(
//for context
import axios from "axios";
import {
useHistory
} from "react-router-dom";
import {
useEffect,
useState
} from "react";
function useFetch(query) {
const [data, setData] = useState(false);
const [site, setSite] = useState(""); // = title
const history = useHistory();
useEffect(() => {
axios({
url: "http://localhost:1337/graphql",
method: "post",
data: {
query: query,
},
})
.then((res) => {
const result = res.data.data;
setData(result);
if (result === null) {
history.push("/Error404");
}
setSite(Object.keys(result)[0]);
})
.catch((error) => {
console.log(error, "error");
history.push("/Error");
});
}, [query, history, setData, setSite]);
return {
data: data,
site: site
};
}
export default useFetch;
)
You can return the error, data and your loading states from your hook. Then the component implementing the hooks can destructure all of these and do things depending upon the result. Example:
const useAsync = () => {
// I prefer status to be idle, pending, resolved and rejected.
// where pending status is loading.
const [status, setStatus] = useState('idle')
const [data, setData] = useState([])
const [error, setError] = useState(null)
useEffect(() => {
setStatus('pending')
axios.get('/').then(resp => {
setStatus('resolved')
setData(resp.data)
}).catch(err => {
setStatus('rejected') // you can handle error boundary
setError(err)
})
}, []}
return {status, data, error}
}
Component implementing this hook
const App = () => {
const {data, status, error} = useAsync()
if(status === 'idle'){
// do something
}else if(status === 'pending'){
return <Loader />
}else if(status === 'resolved'){
return <YourComponent data ={data} />
}else{
return <div role='alert'>something went wrong {error.message}</div>
}
}
the hooks can be enhanced more with the use of dynamic api functions.

Can't handle react spinner loading using useState

I use a function component, so i have to use UseState to handle component states.
I'm trying to show a spinner when loading data with axios :
import { Spinner } from 'react-bootstrap';
const MandatesPage = props => {
const [mandates, setMandates] = useState([]);
const [loading, setLoading] = useState(false); // to handle spinner hide show
useEffect(() => {
setLoading(true); // here loading is true
console.log(loading)
axios
.get(`${config.api}/mandates`)
.then(response => response.data["hydra:member"],setLoading(false)) // here loading is false
.then(data => setMandates(data))
.catch(error => console.log(error.response));
}, []);
...
if (loading) return
return (
<Spinner animation="border" variant="primary" />
);
}
return (
..... // return the other logic of my app
)
}
my problem is the spinner is not shown and i put console.log(loading) after setLoading(true) but i got false value.
Of course loading is still false, because the setting is async and will only be true on the next render.
For the next render, the loading spinner will be returned, since loading will be true than.
If the axios calls needs short than 16 - 32ms, which is the normal frame for each render in react, the loading spinner will not be shown, because loading will already be set back to false.
The problem is that you're trying a asynchronous operation in a synchronous way. You should be holding until your API response gets back, something more like this:
useEffect(() => {
async function fetchMyAPI() {
let url = 'http://something/';
let config = {};
const response = await myFetch(url);
console.log(response);
}
fetchMyAPI();
}, []);
Applying to your example:
useEffect(() => {
setLoading(true);
async function fetchOnAxios() {
const response = await axios.get(`${config.api}/mandates`)
// Down below inside this function
// you can set the loading based on the response
}
fetchOnAxios()
}, []);
I deeply recommend this article for further reading, it has examples and everything.

Resources