How can I escape React useEffect infinite loop? - javascript

I wrote a code that receives the api through React Hook and calls the api again in the child component by passing the id when clicking. However, there seems to be a problem in settingState with arrow function in useEffect or onClick of child component.
I would appreciate it if you could give me an answer on how to fix it.
Users.js
import axios from 'axios';
import React, { useState, useEffect } from 'react';
import UserInfo from './UserInfo';
function Users() {
const [users, setUsers] = useState(null)
const [loding, setLoding] = useState(false)
const [error, setError] = useState(false)
const [userId, setUserId] = useState(null)
const fetchUsers = async () => {
try {
setUsers(null)
setError(null);
setLoding(true)
const respnse = await axios.get('https://jsonplaceholder.typicode.com/users')
setUsers(respnse)
} catch (e) {
setError(e)
}
setLoding(false)
};
useEffect(() => {
fetchUsers();
}, []);
if (loding) return <div>loading...</div>
if (error) return <div>error....</div>
if (!users) return null;
return (
<>
<ul>
{
users.data.map(user =>
<li key={user.id} onClick={() => setUserId(user.id)} >
{user.username} ({user.name})
</li>
)
}
</ul>
<button onClick={fetchUsers}>
reload
</button>
{userId && <UserInfo id={userId} />}
</>
);
}
export default Users;
UserInfo.js
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function UserInfo({ id }) {
const [userInfo, setUserInfo] = useState(null)
async function getUsersAPI() {
try {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`)
setUserInfo(response)
} catch (e) {
}
}
useEffect(() => {
getUsersAPI()
}, [userInfo])
if (!userInfo) {
return null;
}
const { data } = userInfo
return (
<>
<h2>{data.username}</h2>
<p>
<b>email: </b>{data.email}
</p>
</>
);
}
export default UserInfo;

It seems like you want id to be a useEffect dependency instead of userInfo.
Otherwise every time userInfo changes the effect will run, it will call getUsersAPI which in turns sets the value of userInfo when axios resolves (thus causing an infinite loop).
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function UserInfo({ id }) {
const [userInfo, setUserInfo] = useState(null)
async function getUsersAPI() {
try {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`)
setUserInfo(response)
} catch (e) {}
}
useEffect(() => {
getUsersAPI()
}, [id])
if (!userInfo) {
return null;
}
const { data } = userInfo
return (
<>
<h2>{data.username}</h2>
<p>
<b>email: </b>{data.email}
</p>
</>
);
}
export default UserInfo;

this problem is due to the fact that you have set userInfo as a dependency for useEffect dependency array! every time this component renders, useEffect will call your API and it changes the value of userInfo, so you encounter with infinite loop!

The problem is in the UserInfo.js file.
In these lines:
useEffect(() => {
getUsersAPI()
}, [userInfo])
You fetch userInfo but then setUserInfo(response) again which cause useEffect to run again.
If might want to leave it as:
useEffect(() => {
getUsersAPI()
}, [])
So the useEffect only run once.
Or you want it to reflect your last userId from props then put userId to the dependency array:
useEffect(() => {
getUsersAPI()
}, [id])

Related

useContext is in a render loop once a context function is called by one of its children

Everytime i call the function on one of the useContext children to update the state of a variable it endlessly loops crashing react.
**
Context**
import { createContext, useState, useMemo } from "react";
import { onAuthStateChanged } from "firebase/auth";
import { auth } from "../../pages/firebaseConfig";
export const ScoreContext = createContext();
function ScoreProvider({ children }) {
const [score, setScore] = useState(0);
const [user, setUser] = useState(null);
onAuthStateChanged(auth, (user) => {
retrieveUserData(user);
setUser(user);
});
const retrieveUserData = async () => {
let email = await user.email;
const response = await fetch(
`http://localhost:3001/api/users/email/${email}`
);
const data = await response.json();
// console.log(data.payload.total_score);
setScore(data.payload.total_score);
return data.payload;
};
const [level, setLevel] = useState(null);
function updateLevel(i) {
setLevel(i);
console.log("hello world");
}
console.log(level);
return (
<ScoreContext.Provider
value={{
score: score,
update: retrieveUserData,
user: user,
// level: level,
updateLevel: updateLevel,
}}
>
{children}
</ScoreContext.Provider>
);
}
export default ScoreProvider;
Child
const onClick = () => { context.updateLevel(2); };
Tried adding a useMemo but didnt work. Tried wrapping the state inside the function but made code unreachable.

React Component keeps re-rendering

In my page I am calling an action in my useEffect to populate my reducer. I have a component level state called page which is a parameter in the action. So every time the value of page changes I would like the action to be called again because obviously I intend to get the data from different pages.
Sadly I run into errors in the console telling me the component has reached its limit for times of re-rendering.
Here is the relevant code:
const Home = props => {
const [page, setPage] = useState(1);
useEffect(() => {
props.getPopularMovies(page);
}, [page])
My props.getPopularMovies function is coming from my mapDispatchToProps function which is being passed into connect()
Entire Home Page:
import React, { useEffect, useState } from 'react'
import { connect } from "react-redux";
// Actions
import { getPopularMovies } from "../actions/movies.action";
const Home = (props) => {
const [page, setPage] = useState(1);
useEffect(() => {
props.getPopularMovies(page);
}, [page])
return (
<div>
{props.movies && props.movies.length > 0 && props.movies.data.results.map(movie => (
<p key={movie.id}>{movie.title}</p>
))}
<button onClick={setPage(page + 1)}>Next Page</button>
</div>
)
}
const mapStateToProps = state => {
return {
movies: state.movies.movies
}
}
export default connect(mapStateToProps, {
getPopularMovies
})(Home)
Action File:
import axios from "axios";
import { GET_MOVIES_FAIL, GET_MOVIES_SUCCESS } from "../constants/movies.constants"
export const getPopularMovies = (page) => async (dispatch) => {
try {
const config = {
params: {
api_key: process.env.REACT_API_KEY,
page
}
};
const movies = await axios.get('/movie/popular', config);
dispatch({
type: GET_MOVIES_SUCCESS,
payload: movies
})
} catch (err) {
dispatch({
type: GET_MOVIES_FAIL,
payload: err
})
}
}
When you assign the method setPage as an event handler to the onClick event, you are invoking it instead of assigning it. So, instead of this:
<button onClick={setPage(page + 1)}>Next Page</button>
try this:
<button onClick={() => setPage(page + 1)}>Next Page</button>

value in custom Context Provider accessed through custom hook is undefined

I'm learning React and am having trouble with a value defined in a custom context provider. I access the value in a component under the provider with a custom hook but it's reported as being undefined. I've gone through the questions on SO and have verified my syntax with the lesson in my book but can't find the problem.
This is my custom provider and custom hook:
import React, { createContext, useState, useEffect, useContext } from 'react';
const ApiContext = createContext();
export const useApi = () => useContext(ApiContext);
export const ApiProvider = ({ children }) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const [baseImageUrl, setBaseImageUrl] = useState();
const apiKey = 'api_key=SECRET';
const baseUrl = 'https://api.themoviedb.org/3';
const objToParams = (obj) => {
let params = '';
if(obj) {
const keys = Object.keys(obj);
for(let key of keys) {
params += `&${key}=${encodeURIComponent(obj[key])}`;
}
}
return params;
}
const api = {
get: async (path, params) => {
const resp = await fetch(baseUrl + path + '?' + apiKey + objToParams(params));
return await resp.json();
}
}
useEffect( () => {
try {
setLoading(true);
const config = api.get('/configuration');
console.log(config);
config.images && setBaseImageUrl(config.images.secure_base_url);
}
catch (error) {
console.error(error);
setError(error);
}
finally {
setLoading(false);
}
}, []);
if( loading ) {
return <p>Loading...</p>;
}
if( error ) {
return <pre>{JSON.stringify(error, null, 2)}</pre>;
}
return (
<ApiContext.Provider value={{ api, baseImageUrl }}>
{ children }
</ApiContext.Provider>
);
}
and this is the component where I access the value through the custom hook:
import React, { useState } from 'react';
import { ApiProvider, useApi } from './components/context/ApiProvider';
import Header from './components/Header';
import Main from './components/Main';
import Footer from './components/Footer';
import './App.css';
const App = () => {
const [searching, setSearching] = useState(false);
const [searchResults, setSearchResults] = useState([])
const [searchError, setSearchError] = useState();
const {api} = useApi();
const onSearch = (query) => {
try {
setSearching(true);
setSearchResults(api.get('/search/multi', {query: encodeURIComponent(query)} ));
console.log(searchResults);
}
catch (error) {
console.error(error);
setSearchError(error);
}
finally {
setSearching(false);
}
}
return (
<ApiProvider>
<div className="main-layout">
<Header onSearch={ onSearch }/>
<Main
searching={ searching }
searchError={ searchError }
searchResults={ searchResults }
/>
<Footer />
</div>
</ApiProvider>
);
}
export default App;
You can't consume the context in the component where you apply it.
<ComponentA>
<Context.Provider value={"somethong"} >
<ComponentB/>
</Context.Provider>
</ComponentA>
In the above example, only ComponentB can consume the value. ComponentA can't.
If you wan't to consume the value in your App component, it has to be the child (or grandchild ...) of the ContextProvider.
<Context.Provider value={"somethong"} >
<App/>
</Context.Provider>
If I understand your code correctly than you are trying to consume the context in your App, while also returning the provider for the same context.

How to wait for server response before calling Apollo Graph QL Query?

I'm attempting to call a Graph QL Query after receiving data from my useEffect hook. I need the data from the response to use in the Query. Hooks however cannot be called conditionally. If I take away the condition however, loadedAnime will be undefined. How do I get around this restraint?
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import AnimeBanner from "../components/AnimeBanner";
import { useHttpClient } from "../Hooks/http-hook";
import { GetAnimeData } from "../GraphQLFunctions";
import { useQuery } from "#apollo/react-hooks";
import gql from "graphql-tag";
const GET_ANIME_INFO = gql`
query GetAnimeInfo($name: String!) {
Media(search: $name) {
title {
romaji
english
native
userPreferred
}
episodes
id
bannerImage
}
}
`;
const Anime = (props) => {
//Logic for getting anime data from mongoDB (episodes, name, cover image)
const { isLoading, error, sendRequest } = useHttpClient();
const [loadedAnime, setloadedAnime] = useState();
const URLTitle = useParams().URLTitle;
useEffect(() => {
const fetchAnime = async () => {
try {
const responseData = await sendRequest(
"http://localhost:5000/api/anime/" + URLTitle
);
setloadedAnime(responseData.animeData[0]);
} catch (err) {
console.log(err);
}
};
fetchAnime();
}, [sendRequest, URLTitle]);
if (isLoading || error) {
return null;
}
//Logic for getting anime data from anilist (Descriptions, tags, banner, trailer, etc.)
const { apiData, apiLoading, apiError } = useQuery(GET_ANIME_INFO, {
variables: {
name: loadedAnime.anime_name,
},
});
if (apiLoading || apiError) {
return null;
}
return <AnimeBanner src={apiData.Media.bannerImage} />;
};
export default Anime;
Short Answer: You can checkout useLazyQuery instead of useQuery.
Documentation link: https://www.apollographql.com/docs/react/data/queries/#executing-queries-manually
When React mounts and renders a component that calls the useQuery hook, Apollo Client automatically executes the specified query. But what if you want to execute a query in response to a different event, such as a user clicking a button?
The useLazyQuery hook is perfect for executing queries in response to events other than component rendering. This hook acts just like useQuery, with one key exception: when useLazyQuery is called, it does not immediately execute its associated query. Instead, it returns a function in its result tuple that you can call whenever you're ready to execute the query
import React, { useState } from 'react';
import { useLazyQuery } from '#apollo/client';
function DelayedQuery() {
const [dog, setDog] = useState(null);
const [getDog, { loading, data }] = useLazyQuery(GET_DOG_PHOTO);
if (loading) return <p>Loading ...</p>;
if (data && data.dog) {
setDog(data.dog);
}
return (
<div>
{dog && <img src={dog.displayImage} />}
<button onClick={() => getDog({ variables: { breed: 'bulldog' } })}>
Click me!
</button>
</div>
);
}
You can either call the query after the await finishes or you can call your query in another useEffect once you update state after your api call. In general, something like this,
const [state, setState] = useState({})
useEffect(async () => {
const result = await get('/api/blah-blah-blah')
// run your query here now that the await has resolved
}, [someDependency])
or
const [state, setState] = useState({})
useEffect(async () => {
const result = await get('/api/blah-blah-blah')
setState(result)
}, [someDependency])
useEffect(() => {
if(state.id) {
// run the query
}
}, [state.someProp])

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