In my application I created a custom hook to make API requests:
const useRequest = (promise) => {
const [data, setData] = useState({});
const [loading, setLoading] = useState(false);
const [error, setError] = useState({});
useEffect(() => {
let ignore = false;
const fetchProduct = async () => {
try {
setLoading(true);
const response = await promise;
if (!ignore) setData(response);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchProduct();
return () => {
ignore = true;
};
}, [promise]);
return { data, loading, error };
};
export default useRequest;
And in my component I use it as:
const { data, loading, error } = useRequest(api.user.getAll());
where api.user.getAll() is a function that returns a promise. The component renders and it displays the data however it keeps re-rendering and refetching data multiple times. What might be the problem that is causing this? Are there multiple instances of promises being created that trigger the hook to run again?
where api.user.getAll() is a function that returns a promise. [...] Are there multiple instances of promises being created that trigger the hook to run again?
Yes, every time your component gets rendered, it calls api.user.getAll(). Since new Promise (or an async function) will always return a fresh Promise object (and hook deps use strict equality for comparison), the effect is re-run.
My go-to hammer for requests like this these days is swr; maybe look into it?
Related
Hello i'm newbie here...
I found my friend's code when he using useState instead of using useEffect to fetch the API.
I tried it and it worked, the code didn't cause an error and infinite loops.
here is for the code
import { useState } from "react";
import { IN_THEATER, POSTER } from "../../../constant/movies";
import { GlobalGet } from "../../../utilities/fetch";
const Service = () => {
const [movieData, setMovieData] = useState({ data: null, poster: null });
const fetchMovieData = async () => {
try {
let movieRes = await GlobalGet({ url: `${IN_THEATER}` });
return movieRes;
} catch (error) {
console.log(error);
}
};
const fetchPoster = async () => {
try {
let posterRes = await GlobalGet({ url: `${POSTER}` });
return posterRes;
} catch (error) {
console.log(error);
}
};
const fetchData = async () => {
setMovieData({
data: await fetchMovieData(),
poster: await fetchPoster(),
});
};
useState(() => { //<=here it is
fetchData();
}, []);
return {
movieData,
};
};
export default Service;
And my question is, why it could be happen ? why using useState there doesn't cause an infinite loops ?
The useState() function can accept an initializer function as its first argument:
const [state, setState] = useState(initializerFunction)
When a function is passed to useState(), that function is only called once before the component initially mounts. In your case below, the initializer function is an anonymous arrow function:
useState(() => { // <-- Initializer function invoked once
fetchData();
}, []);
Here, the initializer function is () => { fetchData(); }, which is invoked once before the initial mount, so the fetchData() method is only called once. The array that is passed as the second argument [] is useless and doesn't do anything in this case as it's ignored by useState(). The above useState would behave the same if you did useState(fetchData);. Because fetchData() is only called once on the initial mount, any state updates of your component don't cause the fetchData() function to execute again as it's within the initializer function.
With that said, useState() shouldn't be used for fetching data on mount of your component, that's what useEffect() should be used for instead.
Generally it's possible to fetch data from outside of the useEffect hook.
Somewhere in the body of your component...
const [fetchedData, setFetchedData] = useState(false)
const someFetchFunc = asyunc (url) => {
setFetchedData(!fetchedData)
const res = await fetch(url)
const data = await res.json()
setMovieData(data)
}
!fetchedData && someFetchFunc()
But this is an antipattern. In this case developer lacks a whole toolset of dealing with possible issues. What if fetching fails?
So, it's generally a good idea to handle all the side effects like fetching in a place that was intended for that. It's useEffect hook)
I have the following code snippet. Why is my limit always 0 in my fetchData? If I were to console.log(limit) outside of this function it has the correct number. Also If I dont use useState but a variable instead let limit = 0; then it works as expected
I also added limit as a dependency in useEffect but it just keeps triggering the function
const [currentData, setData] = useState([]);
const [limit, setLimit] = useState(0);
const fetchData = async () => {
console.log(limit);
const { data } = await axios.post(endpoint, {
limit: limit,
});
setData((state) => [...state, ...data]);
setLimit((limit) => limit + 50);
};
useEffect(() => {
fetchData();
window.addEventListener(`scroll`, (e) => {
if (bottomOfPage) {
fetchData();
}
});
}, []);
When you pass an empty dependency array [] to useEffect, the effect runs only once on the initial render:
If you pass an empty array ([]), the props and state inside the effect
will always have their initial values.
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run. This isn’t handled as a special
case — it follows directly from how the dependencies array always
works.
useEffect docs
The initial state of limit is 0 as defined in your useState call. Adding limit as a dependency will cause the effect to run every time limit changes.
One way to get around your issue is to wrap the fetchData method in a useCallback while passing the limit variable to the dependency array.
You can then pass the function to the dependency array of useEffect and also return a function from inside of useEffect that removes event listeners with outdated references.
You should also add a loading variable so that the fetchData function doesn't get called multiple times while the user is scrolling to the bottom:
const [currentData, setData] = useState([]);
const [limit, setLimit] = useState(0);
const [loading, setLoading] = useState(false);
const fetchData = useCallback(async () => {
console.log(limit);
// Prevent multiple endpoint calls when scrolling near the end with a loading state
if (loading) {
return;
}
setLoading(true);
const { data } = await axios.post(endpoint, { limit });
setData((state) => [...state, ...data]);
setLimit((limit) => limit + 50);
setLoading(false);
}, [limit, loading]);
// Make the initial request on the first render only
useEffect(() => {
fetchData();
}, []);
// Whenever the fetchData function changes update the event listener
useEffect(() => {
const onScroll = (e) => {
if (bottomOfPage) {
fetchData();
}
};
window.addEventListener(`scroll`, onScroll);
// On unmount ask it to remove the listener
return () => window.removeEventListener("scroll", onScroll);
}, [fetchData]);
I am using React-native and in it, I have a custom Hook called useUser that gets the user's information from AWS Amplify using the Auth.getUserInfro method, and then gets part of the returned object and sets a state variable with it. I also have another Hook called useData hook that fetches some data based on the userId and sets it to a state variable.
useUser custom-Hook:
import React, { useState, useEffect } from "react";
import { Auth } from "aws-amplify";
const getUserInfo = async () => {
try {
const userInfo = await Auth.currentUserInfo();
const userId = userInfo?.attributes?.sub;
return userId;
} catch (e) {
console.log("Failed to get the AuthUserId", e);
}
};
const useUserId = () => {
const [id, setId] = useState("");
useEffect(() => {
getUserInfo().then((userId) => {
setId(userId);
});
}, []);
return id;
};
export default useUserId;
import useUserId from "./UseUserId";
// ...rest of the necessary imports
const fetchData = async (userId) = > { // code to fetch data from GraphQl}
const useData = () => {
const [data, setData] = useState();
useEffect(() => {
const userId = useUser();
fetchData(userId).then( // the rest of the code to set the state variable data.)
},[])
return data
}
When I try to do this I get an error telling me
*Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.*
I think the problem is that I am calling the Hook useUser inside of the use effect, but using it inside the function will cause the problem described here, and I can't use it outside the body of the fetchData since the useData itself is a hook, and it can be only used inside a functional component's or Hook's body. So I don't know how to find a way around this problem.
Correct, React hooks can only be called from React function components and other React hooks. The useEffect hook's callback isn't a React hook, it's a callback. According to the Rules of Hooks, don't call hooks inside loops, conditions, or nested functions.
I suggest refactoring the useData hook to consume the userId as an argument, to be used in the dependency array of the useEffect.
const fetchData = async (userId) => {
// code to fetch data from GraphQl
};
const useData = (userId) => {
const [data, setData] = useState();
useEffect(() => {
fetchData(userId)
.then((....) => {
// the rest of the code to set the state variable data.
});
}, [userId]);
return data;
};
Usage in Function component:
const userId = useUser();
const data = useData(userId);
If this is something that is commonly paired, abstract into a single hook:
const useGetUserData = () => {
const userId = useUser();
const data = useData(userId);
return data;
};
...
const data = useGetUserData();
Though you should probably just implement as a single hook as follows:
const useGetUserData = () => {
const [data, setData] = useState();
useEffect(() => {
getUserInfo()
.then(fetchData) // shortened (userId) => fetchData(userId)
.then((....) => {
// the rest of the code to set the state variable data.
setData(....);
});
}, []);
return data;
};
You can't call hook inside useEffect, Hook should be always inside componet body not inside inner function/hook body.
import useUserId from "./UseUserId";
// ...rest of the necessary imports
const fetchData = async (userId) => {
// code to fetch data from GraphQl}
};
const useData = () => {
const [data, setData] = useState();
const userId = useUser();
useEffect(() => {
if (userId) {
fetchData(userId).then(setData);
}
}, [userId]);
return data;
};
I had a basic useFetch hook implementation that defined a fetchData function which would setData to some JSON if successful, then I would called it on useEffect with no dependencies and the hook returned the stateful data value. I found that this was not ideal, because I wanted to fetch things dynamically on events.
So Instead, I changed the useFetch hook to simply return the fetchData function reference along with the data, and no longer call fetchData inside the hook.
const useFetch = () => {
const [data, setData] = useState([]);
const fetchData = async (url) => {
try {
const response = await fetch(url);
if (response.ok) {
const jsonData = await response.json();
setData(jsonData);
} else {
throw new Error(response.status);
}
} catch (err) {
console.error(err);
}
};
return { fetchData, data };
};
This however introduced problems where I use this hook. I've never used this pattern before so I don't really know what I'm doing, but I'm unable to do stuff with the data value after calling the fetch function. Here's basically how I'm using the hook in another functional component:
const [displayedItems, setDisplayedItems] = useState([]);
const { fetchData, data } = useFetch();
useEffect(() => {
fetchData(urlGoesHere);
}, []);
useEffect(() => {
setDisplayedItems(data);
console.log(displayedItems);
}, [data]);
This is ugly, and it doesn't work. I tried putting them all in one useEffect, but that also doesn't work. Sometimes, when I live reload in CRA, I can see data being logged, but typically when my page loads, data just stays undefined. So I basically created another problem by changing useFetch from actually using the fetch function (which has the downside of not being able to be called in my regular functions and event callbacks), and now I can't seem to even render anything.
I'm pretty new to React and async stuff so I'd appreciate a response that takes that into consideration. If there's a much better pattern for the kind of thing I'm trying to do, I'd love to hear it, but I'd like to keep it in vanilla react land, so no libraries, etc. Again, the reason I'm returning a reference to the fetch function is because I want to be able to use it in callbacks and stuff.
Thanks all!
Edit: It sort of works if I check for the truthiness of data in the second useEffect, but anyhow, can this implementation be better?
useEffect(() => {
fetchData(urlGoesHere);
}, []);
useEffect(() => {
if (data) {
setDisplayedItems(data);
console.log(displayedItems);
}
}, [data]);
Does this mean I have to use 2 whole useEffects every time I want to fetch something on load?
You can return more properties than just the data from the hook in order to help you make an informed choice about what and when to render. A common pattern in many simple useFetch hooks like the one you've asked about is to return data, error, and isLoading, so that you can declaratively render the UI you intend to based on a combination of those states, still eagerly fetching the data after the component first renders.
If you need more control over when the data is fetched (e.g. you mentioned in reaction to an "event"), just use the hook in a child component, and conditionally render that child component based on your "event". Below is a typical example of a simplistic useFetch hook (which you can see working in the code snippet):
Here's a link to just the hook in the TypeScript Playground, which you can use to access and copy the transpiled JavaScript if you can't or don't want to use TypeScript.
<div id="root"></div>
<script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.16.4/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">
/**
* The following line is here because this Stack Overflow snippet uses the
* UMD module for React. In your code, you'd use the commented `import` lines
* below it.
*/
const {useEffect, useState} = React;
// import ReactDOM from 'react-dom';
// import {useEffect, useState} from 'react';
// import type {ReactElement} from 'react';
type FetchData<T> = {
data: T | undefined;
error: Error | undefined;
isLoading: boolean;
};
function useFetch <T = unknown>(url: string): FetchData<T> {
const [data, setData] = useState<T | undefined>();
const [error, setError] = useState<Error | undefined>();
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(() => {
const ac = new AbortController();
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch(url, {signal: ac.signal});
if (!response.ok) throw new Error(String(response.status));
const result = await response.json() as T;
setData(result);
setError(undefined);
}
catch (ex: unknown) {
setError(ex instanceof Error ? ex : new Error(String(ex)));
}
setIsLoading(false);
};
fetchData();
return () => ac.abort();
}, [url, setData, setError, setIsLoading]);
return {data, error, isLoading};
}
type User = { username: string };
function Example (): ReactElement {
const url = 'https://jsonplaceholder.typicode.com/users';
const {data, error, isLoading} = useFetch<User[]>(url);
return (
<div>
<h1>Users</h1>
{
isLoading
? (<div>Loading users...</div>)
: null
}
{
error
? (<div>There was an error loading the data ({error.message})</div>)
: null
}
{
data
? (
<ul>
{data.map(({username}, index) => (
<li key={`${index}-${username}`}>{username}</li>
))}
</ul>
)
: null
}
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
</script>
I'm trying to create a simple fetch with hooks from an AWS database. At the moment it errors out and the only reason I can see is because it breaks the rules of hooks but I'm not sure how. It's at the top level of this functional component and it's not called inside an event handler.
The result of this call (an array of user data), needs to be exported as a function and called in another file.
If anyone can spot something I have missed and can highlighted how I'm breaking the rules of hooks I'd be grateful!
Thanks!
const FetchUsers = () => {
const [hasError, setErrors] = useState(false);
const [Users, setUsers] = useState({});
async function fetchData() {
const res = await fetch(
"USERSDATABASE"
);
res
.json()
.then(res => setUsers(res))
.catch(err => setErrors(err));
}
useEffect(() => {
fetchData();
}, []);
return Users;
};
export { FetchUsers };
consumed here....
class UsersManager {
constructor() {
this.mapUserCountries = {};
}
init() {
const mappedUsers = FetchUsers();
mappedUsers.forEach(user => {
const c = user.country;
if (!this.mapUserCountries[c])
this.mapUserCountries[c] = { nbUsers: 0, users: [] };
this.mapUserCountries[c].nbUsers++;
this.mapUserCountries[c].users.push(user);
});
}
getUsersPerCountry(country) {
return this.mapUserCountries[country];
}
}
export default new UsersManager();
The problem is that you are calling the FetchUsers inside a Class component, and the FetchUsers is executing a React Hook. This is not allowed by React.
First - Hooks don't work inside class based components.
Second - All custom hooks should start with use, in your case useFetchUsers. By setting use as prefix, react will track your hook for deps and calling in correct order and so on.