Custom API data fetching hook keeps rerendering - javascript

I've created a custom hook which I want to use to fetch data with. Now I've come a long way with some help from several blog articles, but there's just one thing I want to improve on. I have a custom hook which fetches data using a useEffect hook. This way the data is fetched upon render, and when for example query params change. Now the useEffect has a caveat. When I include a dependency array with anything in it, it's all fine, but I get a warning that the hook is dependent on a value. I don't like warnings so I add the value to the dependency array, but for some reason then it just keeps rerendering. Below is my useApi hook:
import { useState, useEffect } from "react";
import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
import api from "./config/api-config";
const useApi = (axiosParams: AxiosRequestConfig) => {
const [response, setResponse] = useState<AxiosResponse>();
const [error, setError] = useState<AxiosError>();
const [loading, setLoading] = useState(axiosParams.method === "GET");
const fetchData = async () => {
try {
const result = await api.request(axiosParams);
setResponse(result);
} catch (err: any) {
setError(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
console.log("Render useApi");
axiosParams.method === "GET" && fetchData();
}, [axiosParams.method, fetchData]);
return { response, error, loading, fetchData };
};
export default useApi;
And this is where I'm using it:
import { FC, useState } from "react";
import { Wrapper } from "./home.style";
import { HomeProps } from "./home.types";
import useApi from "../../../api/useApi";
const Home: FC<HomeProps> = () => {
const [query, setQuery] = useState<String>();
const { response, loading, error, fetchData } = useApi({
method: "GET",
url: "/books/v1/volumes",
params: {
q: "",
},
});
return <Wrapper></Wrapper>;
};
export default Home;
I've tried using a callback hook for the fetchData function, but then the issue with the dependency array moves from the useEffect to the useCallback. Does anyone know how I should handle this?

To answer as an answer:
Infinite reexecution of useApi hook is happening due to the object passed as a parameter is recreated on each component rerendering.
const { ... } = useApi({
method: "GET",
url: "/books/v1/volumes",
params: {
q: "",
},
});
It will work fine in case a parameter is a plain string or a number. But in case of normal object or array, for example, you need to preserve a reference to them. You can either move this config out of functional component scope, i.e. just place it above it (if it is not meant to be modified), either preserve it with useMemo or useState hook.
// const useApiConfig = useMemo<AxiosRequestConfig>(() => {
const useApiConfig = useMemo(() => {
return {
method: "GET",
url: "/books/v1/volumes",
params: { q: "" }
};
}, [])
/* ... */
const { response, loading, error, fetchData } = useApi(useApiConfig);
Additionaly, due to fetchData is in depsArray of useEffect hook - it is important to wrap fetchData into useCallback due to without it fetchData will be recreated on each rerender and useEffect will be triggered. And due to reexport of the fetchData from the hook - component that is using it will also have an issues with rerender.
Usually (when reexport is not needed) method like fetchData is just placed inside of the useEffech hook itself (as a const function).
const useApi = (axiosParams: AxiosRequestConfig) => {
const [response, setResponse] = useState<AxiosResponse>();
const [error, setError] = useState<AxiosError>();
const [loading, setLoading] = useState(axiosParams.method === "GET");
const fetchData = useCallback(async () => {
try {
const result = await api.request(axiosParams);
setResponse(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}, [axiosParams]);
useEffect(() => {
console.log("Render useApi");
axiosParams.method === "GET" && fetchData();
}, [axiosParams.method, fetchData]);
return { response, error, loading, fetchData };
};

Related

Axios request keeps returning twice undefined and twice the data

I'm trying to fetch an api on a custom reactjs hook using Axios. I keep getting twice the response as undefined and after that twice as a successful fetch with the data. The undefined breaks my app.
Btw I'm fetching from the randomuser api.
import axios from "axios";
import { useState, useEffect } from "react"
export const useFetch = (url) => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [error, setError] = useState('')
const getData = () => {
setLoading(true)
try {
axios.get(url)
.then(response => setData(response.data));
setLoading(false)
} catch (error) {
setError(error)
}
};
useEffect(() => {
getData()
}, [url])
return {loading, data, error}
}
Trying to use it here and map over it
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useFetch } from '../custom_hooks/useFetch';
const PersonDetails = () => {
const { loading, data , error } = useFetch('https://randomuser.me/api?results=20');
const { results } = data;
const { id } = useParams();
const [person, setPerson] = useState({})
useEffect(() => {
const newPerson = results?.find(person => person.login.uuid === parseInt(id))
setPerson(newPerson)
console.log(newPerson)
}, [])
return (
<div>
{person.name.first}
</div>
)
}
export default PersonDetails
This is the thing I actually Im trying to do, but now because it is undefined, I get that cannot read properties of undefined...
When the effect runs you:
setLoading(true)
Send the Ajax request
setLoading(false)
Later, then the Ajax response arrives you:
setData(response.data)
Since you depend on loading to determine if data is set or not, it breaks.
There are two things you could do:
Move setLoading(false) inside the then callback so it doesn't get set until after you have setData(response.data)
Get rid of loading entirely and base your logic off data being undefined or having a different value.
you should define the getData function inside the useeffect or pass it in dependency array and wrap the function by usecallback to avoid unnecessary rerenders.
you should use abortcontroller in case of cancelations and to have cleanup function in useeffect. (in this case it's better to define getdata body in useeffect)
useEffect(() => {
const controller = new AbortController();
const getData = async () => {
setLoading(true)
try {
await axios.get(url, {signal: controller.signal})
.then(response => setData(response.data));
} catch (error) {
setError(error)
}
}
getData()
return()=>controller.abort()
},[url]}
you can read more about fetching data with hooks in following url and where to setloading and other needed states.
https://www.robinwieruch.de/react-hooks-fetch-data/
Just in case, this solution helped me : https://github.com/axios/axios/issues/2825#issuecomment-883635938
"The problem in my case was caused by React development server.
The strict mode in react caused the issue!
I had to remove the strict mode
This solved the problem of sending double requests!
The strict mode checks are only run in development mode.
Doc: https://reactjs.org/docs/strict-mode.html
"

React-component loops

There is a react-component:
import React, { useState, useCallback } from 'react';
import { useHttp } from '../../hooks/http.hooks';
function Main() {
const {loading, error, request} = useHttp();
const [news, setNews] = useState(0);
const topNews = useCallback(async function() {
const data = await request('http://localhost:5500/api/news/top/2');
return data;
}, []);
console.log(topNews());
return (
<div>Hello world</div>
);
}
export default Main;
And a custom hook:
import { useState } from 'react';
export const useHttp = () => {
const [loading, setLoading] = useState();
const [error, setError] = useState();
async function request(url, { method = 'GET', body = null, headers = {} } = {}) {
setLoading(true);
try {
const response = await fetch(url, { method, body, headers });
const data = await response.json();
if (!response.ok) {
throw new Error(data.msg || 'unhandled error');
}
return data;
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}
return { loading, request, error }
}
Starting it throw error:
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
And so many Promises in console:
As I understood, when loading is changing Main is rendering, because I added a useCallback() but it's not working. How to get rid of looping right?
Inside your React Component, request should be called inside useEffect instead of useCallback. If you keep it that way, the loop looks like this :
Component render
request is called
request update component states
states change force render, so go back to bullet #2
You should change your code to something like this :
import React, { useState, useEffect } from 'react';
import { useHttp } from '../../hooks/http.hooks';
function Main() {
const {loading, error, request} = useHttp();
const [news, setNews] = useState([]);
useEffect(() => {
const data = await request('http://localhost:5500/api/news/top/2');
setNews(data);
}, [])
console.log(news);
return (
<div>Hello world</div>
);
}
export default Main;
See useEffect for more details and advanced usage.
Move the async function call to useEffect, you currently call it on every render:
function Main() {
const {loading, error, request} = useHttp();
const [news, setNews] = useState(0);
useEffect(() => {
async function topNews() {
const data = await request('http://localhost:5500/api/news/top/2');
return data;
}
setNews(topNews());
}, [])
return (
<div>{JSON.stringify(news,null,2)}</div>
);
}

Not able to use value returned by React hook

I am trying to use a custom hook to make HTTP requests and then use a reducer to update the state in the component.
The hook runs correctly and I can get the response from the request but not able to use the response data in dispatch function.
Below is the code:
HTTP hook:
import React, { Fragment, useState, useEffect, useReducer } from 'react';
import axios from 'axios';
export const useHttpRequest = (initialData, initialURL) => {
const [url, setUrl] = useState(initialURL);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
console.log('in a http hook');
setIsError(false);
try {
const res = await axios(url);
console.log(res);
const responseData = res.data.data.data;
return responseData;
} catch (error) {
setIsError(true);
}
};
fetchData();
}, [url]);
return { isError, setUrl };
};
A function call in the state:
const { isError, setUrl } = useHttpRequest();
const getCategoryData = async () => {
setLoading();
try {
const Data = await setUrl('/api/v1/category');
dispatch({
type: SET_CATEGORYDATA,
payload: Data
});
} catch (err) {}
};
A function call in components, where the function is passed through useContext
useEffect(() => {
getCategoryData();
}, []);
You cannot await on a setState (setUrl) function.
You return in your fetch data which is not used later.
You need to first change your mindset on how you think in react hooks and when you need to use them.
As far as I understand you want to fetch some data from server, update the store on successful retrieval, and show an error when the request fails.
You should do this all the way or don't do this at all. You can put the dispatch in the hook or you can forget about the hook and write a reusable fetchData function and handle setHasError in your component's useEffect.
There are many ways to solve this but this is my preferred solution:
import React, { Fragment, useState, useEffect, useReducer } from 'react';
import axios from 'axios';
export const useHttpRequest = (url, updateStore) => {
const [hasError, setHasError] = useState(false);
const fetchData = async (url) => {
setHasError(false);
try {
const res = await axios(url);
const responseData = res.data.data.data;
updateStore(responseData);
} catch (error) {
setHasError(true);
}
};
useEffect(() => {
if (url) {
fetchData(url);
}
}, [url]);
return { fetchData, hasError };
};
// in case you want to fetch the data on component render
const { fetchData, hasError } = useHttpRequest(url, (data) => dispatch({
type: SET_CATEGORYDATA,
payload: data
}));
// in case you want to fetch it in a callback
const clickButton = () => {
fetchData(someCustomUrl);
}
Finally, you can generalize your dispatchers so you don't need to send the whole function to the hook and only send the dispatcher name.

Define a function inside useEffect or outside?

Why the fetchData function is defined inside the useEffect and not outside ?
Link:
https://github.com/zeit/next.js/blob/canary/examples/with-graphql-faunadb/lib/useFetch.js
import { useState, useEffect } from 'react'
export default function useFetch(url, options) {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url, options)
const json = await res.json()
setData(json)
} catch (error) {
setError(error)
}
}
fetchData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url])
return { data, error }
}
I would have done that:
import { useState, useEffect } from 'react'
export default function useFetch(url, options) {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
// Defined outside of useEffect
// `u` instead of `url` for not overlapping
// with the one passed in useFetch()
const fetchData = async (u) => {
try {
const res = await fetch(u, options)
const json = await res.json()
setData(json)
} catch (error)
setError(error)
}
}
useEffect(() => {
// Using url as an argument
fetchData(url)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url])
return { data, error }
}
It seems easier to read and better organized. I'm thinking it's maybe an anti-pattern or something else ?
I typically define the functions inside the useEffect, there are several reasons for that
By definining the function outside of the use effect, you either need to disable exhaustive-deps and risk accidentally having a stale function or you need to useCallback to make the function not-update every render
If the function is only used in the useEffect, you don't need to recreate the function on every render as that's just wasted cycles
It's easier to work with cleanup on the asynchronous functions by defining it within useEffect as you can define variables that are able to be modified within the effect.
On that last one, for instance, you can do some actions to prevent state being called when the effect cleans up.
You could also use AbortController with fetch to cancel the fetch.
import { useState, useEffect } from 'react'
export default function useFetch(url, options) {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
let isUnmounted = false;
const fetchData = async () => {
try {
const res = await fetch(url, options)
const json = await res.json()
if(!isUnmounted) setData(json)
} catch (error) {
if(!isUnmounted) setError(error)
}
}
fetchData()
return ()=>{isUnmounted = true;}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url])
return { data, error }
}
Technically fetchData should be a dependency for useEffect according to React Hooks rules. But if you add it, it will give you an error saying it will cause useEffect to run on every re-render as the function is recreated IF this hook is defined inside a component.
But since it is defined outside a component, my understanding is that the function will not be recreated. Then just add the fetchData as a dependency.
If this useEffect was used inside a component you could either just pass the function inside the useEffect or add the dependency and cover your fetchData with a useCallback.

React: How optimize a custom hook that shares data?

I have a Custom Hook similarly with the below:
import { useEffect, useState } from 'react';
import axios from 'axios';
const myCustomHook = () => {
const [countries, setCountries] = useState([]);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
(async () =>
await axios
.get("MY_API/countries")
.then(response => setCountries(response.data))
.finally(() => setLoading(false)))();
}, []);
return countries;
};
export default myCustomHook;
The hook works great but I am using it in three different areas of my application despite the fact that all the countries are the same wherever the hook is used.
Is there a good pattern to call the axios request just once instead of three times?
EDIT - Final Code After Solution
import { useEffect, useState } from 'react';
import axios from 'axios';
let fakeCache = {
alreadyCalled: false,
countries: []
};
const myCustomHook = (forceUpdate = false) => {
const [isLoading, setLoading] = useState(true);
if (!fakeCache.alreadyCalled || forceUpdate) {
fakeCache.alreadyCalled = true;
(async () =>
await axios
.get("MY_API/countries")
.then(response => setCountries(response.data))
.finally(() => setLoading(false)))();
}
return countries;
};
export default myCustomHook;
One solution to this would be to introduce a custom "cacheing layer" (between your hook and the axios request) that:
caches countries data returned from the first successful request and,
return the same cached data on subsequent requests
There are a number of ways this could be implemented - one possibility would be to define a getCountries() function that implements that cacheing logic in a separate module, and then call that function from your hook:
countries.js
import axios from 'axios';
// Module scoped variable that holds cache data
let cachedData = undefined;
// Example function wraps network request with cacheing layer
export const getCountries = async() => {
// We expect the data for countries to be an array. If cachedData
// is not an array, attempts to populate the cache with data
if (!Array.isArray(cachedData)) {
const response = await axios.get("MY_API/countries");
// Populate the cache with data returned from request
cachedData = response.data;
}
return cachedData;
}
myCustomHook.js
import { useEffect, useState } from 'react';
import { getCountries } from "/countries";
const myCustomHook = () => {
const [countries, setCountries] = useState([]);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
(async() => {
try {
setLoading(true);
// Update hook state with countries data (cached or fresh)
setCountries(await getCountries());
} finally {
setLoading(false)
}
}, []);
});
}
export default myCustomHook;
Each instance of the component runs independently of each other.
Even though the 1st instance state has countries populated the 2nd instance is not aware of it and is still empty.
You could instead consume countries as a prop and if empty invoke the effect, otherwise just return the countries from props.

Categories

Resources