React-component loops - javascript

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>
);
}

Related

Custom API data fetching hook keeps rerendering

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 };
};

Using react + hooks, how can i call/use "navigate()" after a async redux dispatch properly?

When using "await" on "dispatch(saveItem(item))" it's not supposed to have any effct ,
meanwhile if i don't use the "await" both functions will run in the same time resulting a saved item but not a component rerender.
Although the state changes in the redux store the view doesn't,
whilst using the await actually waits for the dispatch to complete and then runs the navigation.
My main question is how to properly navigate after a redux dispatch?
import { useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
import { useForm } from '../hooks/useForm';
import { getById } from '../services/itemService';
import { saveItem } from '../store/actions/itemActions';
export function ItemEdit() {
const dispatch = useDispatch();
const navigate = useNavigate();
const [item, handleChange, setItem] = useForm(null);
const itemId = useParams().id;
useEffect(async () => {
await loadItem();
}, []);
const loadItem = async () => {
try {
const item = await getById(itemId)
setItem(item);
} catch(err) {
setErrMsg(err.name + ': ' + err.message);
}
};
const onSaveItem = async (ev) => {
ev.preventDefault();
await dispatch(saveItem(item));
navigate('/item')
}
return (
<form onSubmit={onSaveItem}>
<button>Save</button>
</form>
);
}
You can try it this way:
dispatch(saveItem(item))
.unwrap()
.then(() => navigate('/item'))
.catch(error => 'handle error')
It works for me.

Firebase Storage Context with Storage Data

I am using Firebase and React and learning how to use React Contexts. I created a context that queries Firebase to get data from there (user's files URL's for example). The context works fine, however, I can't get the context to become asynchronous. I haven't seen any examples of this and am not sure if it's possible. My code is below.
StorageContext.js:
import React, { useContext, useEffect, useState } from 'react';
import { auth } from './FirebaseConfiguration';
import fire from './FirebaseConfig';
import { useAuth } from './AuthContext';
const StorageContext = React.createContext();
export function useStorage() {
return useContext(StorageContext);
}
export function StorageProvider({ children }) {
const { currentUser } = useAuth();
const [fileURLs, setFilesURL] = useState([]);
async function getUserData() {
var storage = fire.storage();
var storageRef = storage.ref(currentUser.uid);
storageRef
.listAll()
.then(function (result) {
result.items.forEach(function (imageRef, i) {
let temp = filesUploaded;
temp.push(imageRef.name);
setFilesUploaded(temp);
});
console.log(filesUploaded);
// console.log(getData(filesUploaded));
getData(filesUploaded);
})
.catch(function (error) {
console.log(error);
});
}
const value = { getUserData };
return (
<StorageContext.Provider value={value}>{children}</StorageContext.Provider>
);
}
Dashboard.js:
import React, { useState, useEffect } from 'react';
import { useStorage } from './Contexts/StorageContext';
export default function Dashboard() {
const { getUserData } = useStorage();
async function getData() {
await getUserData().then((data) => {
console.log(data);
});
}
useEffect(() => {
getData();
}, []);
return (
<div>
console.log('data');
</div>
The useEffect in Dashbaord.js runs fine, the problem is that getUserData() returns immediately even though it should be waiting until (and thus the .then((data) => { console.log(data) } is empty.
Is it possible to run a Context Asynchronously? Or is there another problem that I am missing?
Thanks
The reason it returns immediately is that you use then and not await. Rewrite your function to this and it should work:
async function getUserData() {
var storage = fire.storage();
var storageRef = storage.ref(currentUser.uid);
const result = await storageRef.listAll();
result.items.forEach(function (imageRef, i) {
let temp = filesUploaded;
temp.push(imageRef.name);
setFilesUploaded(temp);
});
console.log(filesUploaded);
// console.log(getData(filesUploaded));
getData(filesUploaded);
}

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.

React useState not changing

I am trying to set up authenticated routes in react and have a useState hook to change to true once the user is authenticated. When I get the user data from my server, I can update the current user information but the useState hook for my authentication will not change from true to false and vice versa.
import { useState, useEffect, useContext } from "react";
import { UserContext } from "./UserContext";
import ApiHandler from "../ApiHandler/ApiHandler";
const api = new ApiHandler();
export const useAuth = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const userContext = useContext(UserContext);
const { currentUser, setCurrentUser } = userContext;
useEffect(() => {
api
.get("/is-loggedin")
.then(res => {
setCurrentUser(res.data.currentUser);
setIsLoggedIn(true);
})
.catch(err => {
setCurrentUser(null);
setIsLoggedIn(false);
});
}, [setCurrentUser]);
return { isLoggedIn, currentUser };
};
I think the problem is useEffect 2nd argument dependencies array does not have correct dependencies.
useEffect(() => { /* rest of the code */ }, [setCurrentUser]);
change to
useEffect(() => { /* rest of the code */ }, [setCurrentUser, isLoggedIn]);
Declare your method inside the hook, and then call. Add the three lines having comments. Also currentUser is the dependency because if user changes then will you check if it's logged-in. Your mistakes were
1)- passing setCurrentUser (it will never change as it's a function/handler) in dependency.
2)- neither declared the method nor called it, instead just passing statements to useEffect, which only runs once.
import { useState, useEffect, useContext } from "react";
import { UserContext } from "./UserContext";
import ApiHandler from "../ApiHandler/ApiHandler";
const api = new ApiHandler();
export const useAuth = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const userContext = useContext(UserContext);
const { currentUser, setCurrentUser } = userContext;
useEffect(() => {
function getStatus() { //method body starts
api.get("/is-loggedin")
.then(res => {
setCurrentUser(res.data.currentUser);
setIsLoggedIn(true);
})
.catch(err => {
setCurrentUser(null);
setIsLoggedIn(false);
});
}; //method body closed.
getStatus(); //this is the call
}, [currentUser]);
return { isLoggedIn, currentUser };
};
Just put an empty array to useEffect
useEffect(() => {
api
.get("/is-loggedin")
.then(res => {
setCurrentUser(res.data.currentUser);
setIsLoggedIn(true);
})
.catch(err => {
setCurrentUser(null);
setIsLoggedIn(false);
});
}, []);

Categories

Resources