React. How to show data loading message on API call - reactjs

Trying to render an API call which returns an array of products.
How to show loading message on products render. (Currently the "loading" message is not being displayed)
useGetProducts.js
import { useEffect, useState } from "react";
import axios from "axios";
const useGetProducts = (API) => {
const [products, setProducts] = useState(null);
const [error, setError] = useState("");
const [loaded, setLoaded] = useState(false);
useEffect(() => {
(async () => {
try {
async function fetchData() {
const response = await axios(API);
setProducts(response.data)
}
fetchData();
} catch (error) {
setError(error.message);
} finally {
setLoaded(true);
}
})();
}, []);
return { products, error, loaded };
};
export default useGetProducts
ProductList.js
import React from "react";
import ProductItem from "./ProductItem";
import useGetProducts from "../hooks/useGetProducts";
import { Link } from "react-router-dom";
import { API } from '../constants/constants';
const ProductList = () => {
const data = useGetProducts(`${API}?limit=9&offset=0`);
return (
<section className="theme-section">
{data.loaded ?
<>
{data.products && data.products.map((product) => (
<div key={product.id}>
<ProductItem product={product} />
<Link to={`/product/${product.id}`}>ver detalle</Link>
<br /><br />
<hr />
</div>
))}
</>
: "loading"
}
</section>
)
}
export default ProductList;
Currently the message is not being displayed

What is happening here is that you're calling fetchData without waiting for it, which is immediately setting loaded to true.
I don't think there is a need for the fetchData function here, so either remove it or await it:
const [products, setProducts] = useState(null);
const [error, setError] = useState("");
const [loaded, setLoaded] = useState(false);
useEffect(() => {
(async () => {
const response = await axios(API);
setProducts(response.data)
} catch (error) {
setError(error.message);
} finally {
setLoaded(true);
}
})();
}, []);

set your loaded state to false when you enter in fetchData function and in finally method, update loaded state to true.
useEffect(() => {
(async () => {
try {
async function fetchData() {
setLoaded(false);
const response = await axios(API);
setProducts(response.data);
}
fetchData();
} catch (error) {
setError(error.message);
} finally {
setLoaded(true);
}
})();
}, []);

Related

custom file for api calls

import { useState, useCallback, useRef, useEffect } from 'react';
export const useHttpClient = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
const activeHttpRequests = useRef([]);
const sendRequest = useCallback(
async (url, method = 'GET', body = null, headers = {}) => {
setIsLoading(true);
const httpAbortCtrl = new AbortController();
activeHttpRequests.current.push(httpAbortCtrl);
try {
const response = await fetch(url, {
method,
body,
headers,
signal: httpAbortCtrl.signal
});
const responseData = await response.json();
activeHttpRequests.current = activeHttpRequests.current.filter(
reqCtrl => reqCtrl !== httpAbortCtrl
);
if (!response.ok) {
throw new Error(responseData.message);
}
setIsLoading(false);
return responseData;
} catch (err) {
setError(err.message);
setIsLoading(false);
throw err;
}
}, []);
const clearError = () => {
setError(null);
};
useEffect(() => {
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
activeHttpRequests.current.forEach(abortCtrl => abortCtrl.abort());
};
}, []);
return { isLoading, error, sendRequest, clearError };
};
Usage:
const fetchUsers = async () => {
try {
const responseData = await sendRequest(
'http://localhost:5000/api/users?page=1'
);
setLoadedUsers(responseData);
} catch (err) {}
};
Currently this is code that i got, i want to make it simplier so i dont need to write on every fetch the cleaning up functions. In this file it used controller and theres also passed abourt on end on useeffect but times when i start to switching pages really fast and dont even give server to load content it consoling me log for unmounted error.. Is there anyone that can help me about this? Maybe theres something wrong in this code or something?
The abort controller rejects only the fetch promise, it doesn't affect any others. Moreover, you're trying to change the state in the catch block without any check whether the component was unmounted or not. You should do these checks manually. The weird array of abort controllers is unnecessary, you can use one controller for all requests.
This code is ugly, but it just illustrates the approach... (Live demo)
export default function TestComponent(props) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
const [text, setText] = useState("");
const controllerRef = useRef(null);
const isMountedRef = useRef(true);
const sendRequest = useCallback(
async (url, method = "GET", body = null, headers = {}) => {
setIsLoading(true);
const httpAbortCtrl =
controllerRef.current ||
(controllerRef.current = new AbortController());
try {
const response = await fetch(url, {
method,
body,
headers,
signal: httpAbortCtrl.signal
});
//if (!isMountedRef.current) return; // nice to have this here
const responseData = await response.json();
if (!response.ok) {
throw new Error(responseData.message);
}
if (!isMountedRef.current) return;
setIsLoading(false);
return responseData;
} catch (err) {
if (isMountedRef.current) {
setError(err.message);
setIsLoading(false);
}
throw err;
}
},
[]
);
useEffect(() => {
return () => {
isMountedRef.current = false;
controllerRef.current && controllerRef.current.abort();
};
}, []);
const fetchUsers = async () => {
try {
const responseData = await sendRequest(props.url);
isMountedRef.current && setText(JSON.stringify(responseData));
} catch (err) {}
};
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{error ? error.toString() : text}</div>
<button onClick={fetchUsers}>Send request</button>
</div>
);
}
You can use a custom library for fetching, which automatically cancels async code (and aborts the request) when the related component unmounting. You can use it directly in your components. So behold the magic :) The simplest demo ever :
import React, { useState } from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpAxios from "cp-axios";
export default function TestComponent(props) {
const [text, setText] = useState("");
const cancel = useAsyncEffect(
function* () {
const response = yield cpAxios(props.url);
setText(JSON.stringify(response.data));
},
[props.url]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={cancel}>Cancel request</button>
</div>
);
}
Or a more practical example with fetching error handling (Live Demo):
import React, { useState } from "react";
import { useAsyncCallback, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CanceledError } from "c-promise2";
import cpAxios from "cp-axios";
export default function TestComponent(props) {
const [text, setText] = useState("");
const [isFetching, setIsFetching] = useState(false);
const fetchUrl = useAsyncCallback(
function* (options) {
try {
setIsFetching(true);
setText("fetching...");
const response = yield cpAxios(options).timeout(props.timeout);
setText(JSON.stringify(response.data));
setIsFetching(false);
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED);
setText(err.toString());
setIsFetching(false);
}
},
[props.url]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={() => fetchUrl(props.url)} disabled={isFetching}>
Fetch data
</button>
<button onClick={() => fetchUrl.cancel()} disabled={!isFetching}>
Cancel request
</button>
</div>
);
}

Using Axios in a React Function

I am trying to pull data from an Axios Get. The backend is working with another page which is a React component.
In a function however, it doesn't work. The length of the array is not three as it is supposed to be and the contents are empty.
I made sure to await for the axios call to finish but I am not sure what is happening.
import React, { useState, useEffect } from "react";
import { Container } from "#material-ui/core";
import ParticlesBg from "particles-bg";
import "../utils/collagestyles.css";
import { ReactPhotoCollage } from "react-photo-collage";
import NavMenu from "./Menu";
import { useRecoilValue } from "recoil";
import { activeDogAtom } from "./atoms";
import axios from "axios";
var setting = {
width: "300px",
height: ["250px", "170px"],
layout: [1, 3],
photos: [],
showNumOfRemainingPhotos: true,
};
const Collages = () => {
var doggies = [];
//const [dogs, setData] = useState({ dogs: [] });
const dog = useRecoilValue(activeDogAtom);
const getPets = async () => {
try {
const response = await axios.get("/getpets");
doggies = response.data;
//setData(response.data);
} catch (err) {
// Handle Error Here
console.error(err);
}
};
useEffect(() => {
const fetchData = async () => {
getPets();
};
fetchData();
}, []);
return (
<>
<NavMenu />
<ParticlesBg type="circle" margin="20px" bg={true} />
<br></br>
<div>
{doggies.length === 0 ? (
<div>Loading...</div>
) : (
doggies.map((e, i) => {
return <div key={i}>{e.name}</div>;
})
)}
</div>
<Container align="center">
<p> The length of dogs is {doggies.length} </p>
<h1>Knight's Kennel</h1>
<h2> The value of dog is {dog}</h2>
<h2>
Breeders of high quality AKC Miniature Schnauzers in Rhode Island
</h2>
<section>
<ReactPhotoCollage {...setting} />
</section>
</Container>
</>
);
};
export default Collages;
Try doing the following:
const [dogs, setData] = useState([]);
[...]
const getPets = async () => {
try {
const response = await axios.get("/getpets");
doggies = response.data;
setData(response.data);
} catch (err) {
// Handle Error Here
console.error(err);
}
};
const fetchData = async () => {
getPets();
};
useEffect(() => {
fetchData();
}, []);
No idea if it will actually work, but give it a try if you haven't.
If you don't use useState hook to change the array, it won't update on render, so you will only see an empty array on debug.
As far as I can tell you do not return anything from the getPets() function.
Make use of the useState Function to save your doggies entries:
let [doggies, setDoggies ] = useState([]);
const getPets = async () => {
try {
const response = await axios.get("/getpets");
return response.data;
} catch (err) {
// Handle Error Here
console.error(err);
}
return []
};
useEffect(() => {
setDoggies(await getPets());
});
I used setState inside the getPets function. Now it works.
const Collages = () => {
const [dogs, setData] = useState([]);
const dog = useRecoilValue(activeDogAtom);
const getPets = async () => {
try {
const response = await axios.get("/getpets");
setData(response.data);
} catch (err) {
// Handle Error Here
console.error(err);
}
};
useEffect(() => {
const fetchData = async () => {
getPets();
};
fetchData();
}, []);

Stop Rendering before data load using hooks

I am new to react hooks I write a react custom hook
Hook:
import { useState, useEffect } from 'react';
export const useFetch = (url, options) => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
fetchData();
}, []);
return { response, error };
};
And I also write a functional component and i want component render when data comes
here is my component
Component
import React, { useState, useEffect } from 'react';
import './index.scss';
import { List } from '../components';
import { useFetch } from '../../hooks';
export const Subscription = () => {
const res = useFetch('http://localhost:8080/test', {});
const [isLoading, setLoading] = useState(true);
useEffect(() => {
if (res.response.length > 0) {
console.log('this is the test');
setLoading(false);
}
});
const list = res.response;
return (
<div>
{isLoading && <div>Loading...</div>}
{!isLoading && (
<div className="list">
<List subscriptions={list} />
</div>
)}
</div>
);
};
but i am unable to render List component I didn't understand once data comes from backend why list note having data still it having null value and lists is not renderd
I got proper values from backend
useFetch return return { response, error }; ==> const response = useFetch('http://localhost:8080/test', {}); the response is an object containing { response, error }
Do this instead const {response} = useFetch('http://localhost:8080/test', {});
And you should handle loading in useFetch
UseFetch
import { useState, useEffect } from 'react';
export const useFetch = (url, options) => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
setLoading(false)
} catch (error) {
setError(error);
setLoading(false)
}
};
fetchData();
}, []);
return { response, error,loading };
};
Subscription
import React, { useState, useEffect } from 'react';
import './index.scss';
import { List } from '../components';
mport { useFetch } from '../../hooks';
export const Subscription = () => {
const {response: subscriptions, loading} = useFetch('http://localhost:8080/test', {});
return (
<div>
{isLoading && <div>Loading...</div>}
{!isLoading && (
<div className="list">
<List subscriptions={subscriptions} />
</div>
)}
</div>
);
};

Multiple fetch data axios with React Hooks

I would like to get global information from Github user and his repos(and get pinned repos will be awesome). I try to make it with async await but It's is correct? I've got 4 times reRender (4 times console log). It is possible to wait all component to reRender when all data is fetched?
function App() {
const [data, setData] = useState(null);
const [repos, setRepos] = useState(null);
useEffect(() => {
const fetchData = async () => {
const respGlobal = await axios(`https://api.github.com/users/${username}`);
const respRepos = await axios(`https://api.github.com/users/${username}/repos`);
setData(respGlobal.data);
setRepos(respRepos.data);
};
fetchData()
}, []);
if (data) {
console.log(data, repos);
}
return (<h1>Hello</h1>)
}
Multiple state updates are batched but but only if it occurs from within event handlers synchronously and not setTimeouts or async-await wrapped methods.
This behavior is similar to classes and since in your case its performing two state update cycles due to two state update calls happening
So Initially you have an initial render and then you have two state updates which is why component renders three times.
Since the two states in your case are related, you can create an object and update them together like this:
function App() {
const [resp, setGitData] = useState({ data: null, repos: null });
useEffect(() => {
const fetchData = async () => {
const respGlobal = await axios(
`https://api.github.com/users/${username}`
);
const respRepos = await axios(
`https://api.github.com/users/${username}/repos`
);
setGitData({ data: respGlobal.data, repos: respGlobal.data });
};
fetchData();
}, []);
console.log('render');
if (resp.data) {
console.log("d", resp.data, resp.repos);
}
return <h1>Hello</h1>;
}
Working demo
Figured I'd take a stab at it because the above answer is nice, however, I like cleanliness.
import React, { useState, useEffect } from 'react'
import axios from 'axios'
const Test = () => {
const [data, setData] = useState([])
useEffect(() => {
(async () => {
const data1 = await axios.get('https://jsonplaceholder.typicode.com/todos/1')
const data2 = await axios.get('https://jsonplaceholder.typicode.com/todos/2')
setData({data1, data2})
})()
}, [])
return JSON.stringify(data)
}
export default Test
Using a self invoking function takes out the extra step of calling the function in useEffect which can sometimes throw Promise errors in IDEs like WebStorm and PHPStorm.
function App() {
const [resp, setGitData] = useState({ data: null, repos: null });
useEffect(() => {
const fetchData = async () => {
const respGlobal = await axios(
`https://api.github.com/users/${username}`
);
const respRepos = await axios(
`https://api.github.com/users/${username}/repos`
);
setGitData({ data: respGlobal.data, repos: respGlobal.data });
};
fetchData();
}, []);
console.log('render');
if (resp.data) {
console.log("d", resp.data, resp.repos);
}
return <h1>Hello</h1>;
}
he made some mistake here:
setGitData({ data: respGlobal.data, repos: respGlobal.data(respRepos.data //it should be respRepos.data});
For other researchers (Live demo):
import React, { useEffect, useState } from "react";
import { CPromise, CanceledError } from "c-promise2";
import cpAxios from "cp-axios";
function MyComponent(props) {
const [error, setError] = useState("");
const [data, setData] = useState(null);
const [repos, setRepos] = useState(null);
useEffect(() => {
console.log("mount");
const promise = CPromise.from(function* () {
try {
console.log("fetch");
const [respGlobal, respRepos] = [
yield cpAxios(`https://api.github.com/users/${props.username}`),
yield cpAxios(`https://api.github.com/users/${props.username}/repos`)
];
setData(respGlobal.data);
setRepos(respRepos.data);
} catch (err) {
console.warn(err);
CanceledError.rethrow(err); //passthrough
// handle other errors than CanceledError
setError(err + "");
}
}, []);
return () => {
console.log("unmount");
promise.cancel();
};
}, [props.username]);
return (
<div>
{error ? (
<span>{error}</span>
) : (
<ul>
<li>{JSON.stringify(data)}</li>
<li>{JSON.stringify(repos)}</li>
</ul>
)}
</div>
);
}

Putting fetch function in a separate component

I'm trying to take out the fetchImages function from the following component and put it inside a new component:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import UnsplashImage from './UnsplashImage';
const Collage = () => {
const [images, setImages] = useState([]);
const [loaded, setIsLoaded] = useState(false);
const fetchImages = (count = 10) => {
const apiRoot = 'https://api.unsplash.com';
const accessKey =
'<API KEY>';
axios
.get(`${apiRoot}/photos/random?client_id=${accessKey}&count=${count}`)
.then(res => {
console.log(res);
setImages([...images, ...res.data]);
setIsLoaded(true);
});
};
useEffect(() => {
fetchImages();
}, []);
return (
<div className="image-grid">
{loaded
? images.map(image => (
<UnsplashImage
url={image.urls.regular}
key={image.id}
alt={image.description}
/>
))
: ''}
</div>
);
};
export default Collage;
For this, I created a new component called api.js, removed the entire fetchImage function from the above component and put it in to api.js like this:
api.js
const fetchImages = (count = 10) => {
const apiRoot = 'https://api.unsplash.com';
const accessKey =
'<API KEY>';
axios
.get(`${apiRoot}/photos/random?client_id=${accessKey}&count=${count}`)
.then(res => {
console.log(res);
setImages([...images, ...res.data]);
setIsLoaded(true);
});
};
export default fetchImages;
Next I took setIsLoaded(true); from api.js and paste it inside Collage component like this:
useEffect(() => {
fetchImages();
setIsLoaded(true);
}, []);
Now I can import fetchImages in to Collage component.
However, I don't know what should I do with this line inside the fetchImages function? This needs to go to Collage component, but res.data is not defined inside Collage component.
setImages([...images, ...res.data]);
How should I handle it?
There is many way to do that, but in your case.
You should use
const fetchImages = (afterComplete, count = 10) => {
const apiRoot = 'https://api.unsplash.com';
const accessKey = '<API KEY>';
axios
.get(`${apiRoot}/photos/random?client_id=${accessKey}&count=${count}`)
.then(res => {
console.log(res);
afterComplete(res.data);
});
};
export default fetchImages;
And in your Collage component:
const afterComplete = (resData) =>{
setImages([...images, ...resData]);
setIsLoaded(true);
}
useEffect(() => {
fetchImages(afterComplete);
}, []);
What you can do is create a custom hook ( sort of like a HOC)... Since I don't have an unsplash API key I'll give you an example with a different API but the idea is the same:
Here is your custom hook:
import { useState, useEffect } from 'react';
export const useFetch = url => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const fetchUser = async () => {
const response = await fetch(url);
const data = await response.json();
const [user] = data.results;
setData(user);
setLoading(false);
};
useEffect(() => {
fetchUser();
}, []);
return { data, loading };
};
Here is how you can use it in your component:
import { useFetch } from './api';
const App = () => {
const { data, loading } = useFetch('https://api.randomuser.me/');
return (
<div className="App">
{loading ? (
<div>Loading...</div>
) : (
<>
<div className="name">
{data.name.first} {data.name.last}
</div>
<img className="cropper" src={data.picture.large} alt="avatar" />
</>
)}
</div>
);
};
Here is a live demo: https://codesandbox.io/s/3ymnlq59xm

Resources