Why is my custom useFetch hook causing an infinite loop? - javascript

This is the react custom hook that I use to fetch data throughout my App. I did not encounter problems when I used this hook to fetch data based on user's input query.
useFetch.jsx
import React, { useState, useEffect } from "react";
import apiKey from "../apiKey";
function useFetch() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
async function fetchData(
url,
options = {
method: "GET",
headers: {
"X-RapidAPI-Key": apiKey,
"X-RapidAPI-Host": "unogsng.p.rapidapi.com",
},
}
) {
setLoading(true);
try {
const response = await fetch(url, options);
const jsonData = await response.json();
setData(jsonData);
} catch (err) {
setError(err);
}
setLoading(false);
}
return [data, error, loading, fetchData];
}
export default useFetch;
This is the homepage of my App. I want the homepage to display the newest shows that are released today. I encountered an infinite loop when I tried to use useEffect to fetch the API.
Home.jsx
import React, { useState, useEffect } from "react";
import useFetch from "../hooks/useFetch";
const Home = () => {
const [newShows, setNewShows] = useState([]);
const [data, error, loading, fetchData] = useFetch();
const today = new Date();
const date = today.toISOString().substring(0, 10);
const url = `https://unogsng.p.rapidapi.com/search?newdate=${date}&limit=10`;
console.log(date);
useEffect(() => {
fetchData(url);
console.log(data);
}, [data]);
return <h1>Home</h1>;
};
export default Home;
I tried using url as dependency but it did not fetch the API on mount.
useEffect(() => {
fetchData(url);
console.log(data);
}, [url]);
It does't work with no dependency either.
useEffect(() => {
fetchData(url);
console.log(data);
}, []);

Fetch the data only when the component mounts; so use an empty dependency array for useEffect. (You might want to put url in the dependency array if it may change.)
useEffect(() => {
fetchData(url);
}, []);
Then, display the data conditionally in the JSX:
return <>
<h1>Home</h1>
{data ? <pre>{JSON.stringify(data, null, 4)}</pre> : <p>Loading</p>}
</>;

Related

useEffect keeps rendering

In a react app, when creating a component, I use useEffect to handle a HTTP request via a custom hook (which fetch via a useCallback). Then, to parse the parameters for the request, I have a layer for services which return the expected values.
As a result, this workflow keeps re-rendering in a loop and the apps gets stacked.
Component:
import React, { Fragment, useContext, useEffect, useState } from 'react';
import { NavLink } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { getProject } from '../../../services/Project.service';
import { AuthContext } from '../../../shared/context/auth.context';
import { NavOptions } from '../../../shared/constants/NavOptions';
import { useHttpClient } from '../../../shared/hooks/http.hook';
import SideNavigation from '../../../shared/components/Navigation/SideNavigation/SideNavigation';
import NavLinks from '../../../shared/components/Navigation/NavLinks/NavLinks';
import './Dashboard.css';
const Dashboard = (props) => {
console.log('Dashboard...');
const { isLoading, error, sendRequest, clearError } = useHttpClient();
const [project, setProject] = useState();
const auth = useContext(AuthContext);
const projectId = useParams().projectId;
const getProject = async () => {
console.log('getProject...');
console.log('auth', auth.token);
const response = await sendRequest(getProject(projectId, auth.token));
if (response.status === 201) {
const responseData = await response.json();
console.log('project:', responseData);
setProject(responseData);
} else {
console.log('getting buildings failed!');
const error = await response.json();
}
};
useEffect(() => {
projectId && getProject();
}, []);
const { t, i18n } = useTranslation();
let content = (
<div className="bim-y-dashboard">
.
.
.
</div>
);
return (
<Fragment>
<SideNavigation>
<NavLinks options={NavOptions.PROJECT} projectId />
</SideNavigation>
<MainContent>{content}</MainContent>
</Fragment>
);
};
export default Dashboard;
Custom hook:
export const useHttpClient = () => {
const auth = useContext(AuthContext);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
const activeHttpRequests = useRef([]);
const sendRequest = useCallback(
async (url, method = 'GET', body = null, headers = {}) => {
console.log('sendRequest...');
console.log('url', url);
console.log('method', method);
console.log('body', body);
console.log('headers', headers);
},[]);
const clearError = () => {
setError(null);
};
useEffect(() => {
return () => {
activeHttpRequests.current.forEach(abortCtrl => abortCtrl.abort());
};
}, []);
return { isLoading, error, sendRequest, clearError };
};
Service:
export const getProject = (projectId, token) => {
console.log('getProject...');
return (`/projects/id/${projectId}`, 'GET', null, {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + token,
});
}
What am I missing to avoid this constant re-rendering?
Thanks in advance.
I have the feeling your issue is in code you have not shared, as the above looks legit.
Things you can try:
Comment out const auth = useContext(AuthContext); in your custom hook to make sure the culprit is not in your context
Make sure const response = await sendRequest(..) returns what you are expecting
Add a new hook, something like const [data, setData] = useState(null), then after you get a response, set it in your hook setData(response)
Finally, in your useEffect, update your condition as projectId && !data && getProject();
That will ensure your re rendering issue is neither on your auth context nor in your fetch request.
Hope that helps; if not, please update the question with the full code of your component and I'll take a second look :)
RESOLVED:
Found this post: Link
It explains that because of sendRequest method, it keeps re-rendering.

react custom hook causing infinite loop

I am relatively new to react hooks and I am trying to create this custom hook to handle CRUD operations for my API.
This is the hook file:
import React, { useState, useEffect } from "react";
const useApi = (url, headers = { method: "GET" }, payload = null) => {
const [isLoading, setIsLoading] = useState(true);
const [apiData, setApiData] = useState(null);
const [serverError, setServerError] = useState(null);
const [api, setApi] = useState({});
const list = async () => {
try {
const resp = await fetch(url);
const data = await resp?.json();
setApiData(data);
setIsLoading(false);
} catch (error) {
setServerError(error);
} finally {
setIsLoading(false);
}
};
const create = async () => {
try {
const resp = await fetch(url, (headers = { method: "POST" }), payload);
const data = await resp?.json();
setApiData(data);
setIsLoading(false);
} catch (error) {
setServerError(error);
} finally {
setIsLoading(false);
}
};
setApi({
...api,
list: list,
create: create
});
return { isLoading, apiData, serverError, api };
};
export default useApi;
However, when I call api.list() in my main component inside a useEffect() hook, I get an infinite loop.
Sample component call:
import { useEffect } from "react";
import useApi from "./useApi";
export default function App() {
const {
isLoading: loading,
apiData: students,
serverError: error,
api
} = useApi("https://59f0f160ce72350012bec011.mockapi.io/students");
console.log(loading, students, error, api);
useEffect(() => {
api.list();
}, [api]);
return (
<div className="App">
<h1>list</h1>
{loading ? "loading" : students.map((x) => x.name)}
</div>
);
}
Here's the sandbox for it:
https://codesandbox.io/s/cocky-chebyshev-d9q89?file=/src/App.js:0-492
Can anyone help me understand the issue?
Thank you in advance!
This is what is causing the infinite loop:
setApi({
...api,
list: list,
create: create
});
You are not supposed to call setState() during a render.
In your case, you don't need to useState for the api object, you can just return it on every render:
return {
isLoading,
apiData,
serverError,
api: { list, create }
};
Here is a link to the fixed sandbox
Also, another warning: this code will repeatedly call api.list().
useEffect(() => {
api.list();
}, [api]);
Since api changes on every render, it will repeatedly call api.list().
This is the object that changes on every render:
return { isLoading, apiData, serverError, api };
You can ensure that you only call api.list() one time by using a ref.
import { useRef } from 'react'
// In the component
const gotRef = useRef(false)
useEffect(() => {
if (!gotRef.current) {
api.list();
gotRef.current = true
}
}, [api]);

useEffect hook wont run codeblock

My useEffect hook dont seem to be running the block of code within its scope. Not exactly sure why, when i debug it the useEffect hook gets hit but the code within it never runs. I have placed a break point and it never lands on it.
import {useState, useEffect} from "react";
const useApiResult = (request) => {
const [results, setResults] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
loadData(request);
}, [request]);
const loadData = async (request) => {
console.log(request)
try
{
const response = await fetch(request);
if(response.ok) {
setResults(await response.json());
setError(null);
} else {
setError(await response.text());
}
}
catch (err) {
setError(err);
}
}
return [results, error];
};
export default useApiResult;
Where I call useApiResult
import { useMemo } from 'react';
import { getPokemons } from '../requests';
import useApiResult from '../customHooks/useApiResults';
const usePokemons = () => {
const request = useMemo(() => getPokemons(), []);
return useApiResult(request);
}
export default usePokemons;
getpokemons.js
export const BASE_URL = "https://pokeapi.co/api/v2";
const createUrl = (base, path) => `${base}${path}`;
export const getPokemons = () => [
createUrl(BASE_URL, "/pokemon?offset=300&limit=10"),
{
method: "GET",
}
];
My steps to catch issue:
When I looked in your sandbox I noticed error: results is null
I also found const [results, error] = usePokemons(); in PokemonList component, so I thought that this error is not necessary, I just removed it. Because you never used it in component.
I still got results is null I just add condition render via &&. It looks like:
results &&
results.map((key, i) => (
I got new error: results.map is not a function. It means that results is not array. When I saw at results I noticed that it has inside array with the same name. So I just add resuts after a dot.
results &&
results.results.map((key, i) => (
Here we go:)

Unable to copy array using setstate hook

I am fetching data from backend using axios whenever I am trying to update hooks it is not updating.
The data is JSON from where I am extracting data and trying to set element. It might sound silly but can somebody tell me what is dependent array?
I keep getting this
Line 18: React Hook useEffect has a missing dependency: 'elements'. Either include it or remove the dependency array react-hooks/exhaustive-deps
Here is code
import React, { useEffect, useState } from 'react';
import './App.css';
import axios from 'axios';
function App() {
const [elements, setElements] = useState([]);
useEffect(() => {
const res = async () => {
const result = await axios.get('/data');
const data = result.data;
console.log(data);
setElements(elements => [...elements, data]);
console.log(elements);
};
res();
}, []);
console.log(elements.map(element => console.log(element)));
return <div className='App'>Hello</div>;
}
export default App;
Just console.log outside your effect. You're already using the updater version of useState
setElements(elements => [...elements, data])
The missing dependecy warning is coming from console.log(elements)
import React, { useEffect, useState } from 'react';
import './App.css';
import axios from 'axios';
function App() {
const [elements, setElements] = useState([]);
useEffect(() => {
const res = async () => {
const result = await axios.get('/data');
const data = result.data;
console.log(data);
setElements(elements => [...elements, data]);
};
res();
}, []);
console.log(elements);
return <div className='App'>Hello</div>;
}
export default App;
Missing dependency warning is because you use console.log(elements) inside the useEffect.
And your elements log is not showing latest result because state is not changed (yet)
Just add a useEffect to keep track of elements changes like below.
function App() {
const [elements, setElements] = useState([]);
useEffect(() => {
const res = async () => {
const result = await axios.get('/data');
const data = result.data;
console.log(data);
setElements(elements => [...elements, data]);
};
res();
}, []);
useEffect(() => console.log(elements), [elements])
return <div className='App'>Hello</div>;
}
export default App;
To answer your question;
The dependency array is their to let React know when the useEffect in this case should be triggered. So the useEffect i added, only triggers when its dependency elements is changed.
In your case you are puting the array data inside elements, setElements(elements => [...elements, data]); so it will be array inside array.
Try the below :
function App() {
const [elements, setElements] = useState([]);
useEffect(() => {
const res = async () => {
const result = await axios.get('/data');
const data = result.data;
console.log(data);
setElements([...elements, data]);
};
res();
}, []);
useEffect(() => console.log(elements), [elements])
return <div className='App'>Hello</div>;
}
export default App;

How to fire the function useEffect after clicking event?

Recently am learning React hooks and am now doing a search app which have to call API then return the list movies correspond to what i type in the search box.
My code here:
useFetch.js
import { useState, useEffect } from 'react'
export const useFetch = (url, initialState) => {
const [data, setData] = useState(initialState)
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchMovies() {
const response = await fetch(url)
const data = await response.json()
setData(data.Search)
setLoading(false)
}
fetchMovies()
}, [url])
return { data, loading }
}
App.js
import React, { useState } from 'react'
import Search from './components/Search'
import Result from './components/Result'
import Loading from './components/Loading'
import { useFetch } from './utils/useFetch'
export default function App() {
const [key, setKey] = useState('Iron man')
const onSearch = (key) => {
setKey(key)
}
const {data, loading} = useFetch(`https://www.omdbapi.com/?s=${key}&apikey=${API_KEY}`)
return (
<>
<Search handleSearch={onSearch}/>
<Loading isLoading={loading} />
<Result movies={data}/>
</>
)
}
As far as i understand after clicking button search function call API will be fired and return the result as expect. I can't put
const {data, loading} = useFetch(`https://www.omdbapi.com/?s=${key}&apikey=${API_KEY}`)
inside onSearch function. Follow the code function call API is automatically called whenever the app start and return undefined as result.
Can anyone help me out and explain why?
You are correct in your understanding of how hooks can only be called at the top level in a react component. Make the following changes and the API won't get called the first time around but will get called subsequently.
Use url state variable and extract generateUrl logic outside the component:
function generateUrl(key) {
return `https://www.omdbapi.com/?s=${key}&apikey=${API_KEY}`
}
function MyComponent() {
const [url, setUrl] = React.useState('');
//...
}
Check for url presence in useFetch hook by wrapping fetchMovies() call in an if condition. This way, API won't trigger since default value of url is empty.
import { useState, useEffect } from 'react'
export const useFetch = (url, initialState) => {
const [data, setData] = useState(initialState)
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchMovies() {
const response = await fetch(url)
const data = await response.json()
setData(data.Search)
setLoading(false)
}
if(url) {
fetchMovies()
}
}, [url])
return { data, loading }
}
Finally, modify onSearch
const onSearch = (key) => {
setUrl(generateUrl(key))
}
Perhaps you could expose setUrl through something like:
return { data, loading, onSearch: (key) => setUrl(generateUrl(key)) }

Categories