How to fix this "React Hook useEffect has a missing dependency" warning? - reactjs

Here's my file:
// useFetcher.js
import { useEffect, useState } from "react";
export default function useFetcher(action) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
async function loadData() {
try {
setLoading(true);
const actionData = await action();
setData(actionData);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}
useEffect(() => {
loadData();
}, [action]);
return [data, loading, error];
}
On line 21, eslint complaints that:
React Hook useEffect has a missing dependency: 'loadData'. Either include it or remove the dependency array.eslint(react-hooks/exhaustive-deps)
How can I fix this?

The best way here would be to define your async function inside the useEffect:
export default function useFetcher(action) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
useEffect(() => {
async function loadData() {
try {
setLoading(true);
const actionData = await action();
setData(actionData);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}
loadData();
}, [action]);
return [data, loading, error];
}
More info in the docs.

Add the loadData function to the array in your useEffect. This will get rid of the complaint:
useEffect(() => {
loadData();
}, [action, loadData]);

Related

Calling a custom hook from an onClick by passing it arguments

I created a custom hook to make my api calls with axios.
When I call this hook passing it different parameters, it returns 3 states.
I created a page with a form.
When I submit this form I call a function "onSubmitform"
I would like to be able to execute this custom hook in this function.
How can I do ?
Maybe a custom hook is not suitable in this case?
-- file useAxios.js --
import { useState, useEffect } from "react";
import axios from "axios";
const useAxios = (axiosParams) => {
const [response, setResponse] = useState(undefined);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const handleData = async (params) => {
try {
const result = await axios.request(params);
setResponse(result.data);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
handleData(axiosParams);
}, []);
return { response, error, loading };
};
export default useAxios;
-- file Page.js --
import useAxios from "../hooks/useAxios";
function Page() {
const { response } = useAxios();
const onSubmitForm = () => {
// Here I want to call the custom hook by passing it different parameters.
}
}
You can add an option to execute the request manually and avoid the fetch on mount:
const useAxios = (axiosParams, executeOnMount = true) => {
const [response, setResponse] = useState(undefined);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const handleData = async (params) => {
try {
const result = await axios.request(params);
setResponse(result.data);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (executeOnMount) handleData(axiosParams);
}, []);
return { response, error, loading, execute: handleData };
};
Then use it:
const { response, execute } = useAxios(undefined, false);
const onSubmitForm = (data) => {
execute(params) // handleData params
}
A hook is a function (returning something or not) which should be called only when the components (re)renders.
Here you want to use it inside a callback responding to an event, which is not the same thing as the component's render.
Maybe you are just looking for a separate, "simple", function? (for example something similar to what you have in your "useEffect")

useEffect dependency causes infinite loop

I created a custom hook which I use in App.js
The custom hook (relevant function is fetchTasks):
export default function useFetch() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [tasks, setTasks] = useState([]);
const fetchTasks = async (url) => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("falied!");
}
const data = await response.json();
const loadedTasks = [];
for (const taskKey in data) {
loadedTasks.push({ id: taskKey, text: data[taskKey].text });
}
setTasks(loadedTasks);
} catch (err) {
console.log(err.message);
}
setLoading(false);
};
return {
loading,
setLoading,
error,
setError,
fetchTasks,
tasks,
};
}
Then in my App.js:
function App() {
const { loading, setLoading, error, setError, fetchTasks, tasks } =
useFetch();
useEffect(() => {
console.log("fetching");
fetchTasks(
"https://.....firebaseio.com/tasks.json"
);
}, []);
My IDE suggests adding the fetchTasks function as a dependency to useEffect. But once I add it, an infinite loop is created. If I omit it from the dependencies as shown in my code, it will work as expected, but I know this is a bad practice. What should I do then?
Because that every time you call useFetch(). fetchTasks function will be re-created. That cause the reference to change at every render then useEffect() will detected that dependency fetchTasks is re-created and execute it again, and make the infinite loop.
So you can leverage useCallback() to memoize your fetchTasks() function so the reference will remains unchanged.
import { useCallback } from 'react'
export default function useFetch() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [tasks, setTasks] = useState([]);
const fetchTasks = useCallback(
async (url) => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("falied!");
}
const data = await response.json();
const loadedTasks = [];
for (const taskKey in data) {
loadedTasks.push({ id: taskKey, text: data[taskKey].text });
}
setTasks(loadedTasks);
} catch (err) {
console.log(err.message);
}
setLoading(false);
};,[])
return {
loading,
setLoading,
error,
setError,
fetchTasks,
tasks,
};
}
function App() {
const { loading, setLoading, error, setError, fetchTasks, tasks } =
useFetch();
useEffect(() => {
console.log("fetching");
fetchTasks(
"https://.....firebaseio.com/tasks.json"
);
}, [fetchTasks]);
instead of return fetchTasks function return this useCallback fetchTasksCallback function from useFetch hook which created only one instance of fetchTasksCallback.
const fetchTasksCallback = useCallback(
(url) => {
fetchTasks(url);
},
[],
);
function App() {
const { loading, setLoading, error, setError, fetchTasksCallback, tasks } =
useFetch();
useEffect(() => {
console.log("fetching");
fetchTasksCallback(
"https://.....firebaseio.com/tasks.json"
);
}, [fetchTasksCallback]);
the problem is this fetchTasks every time create a new instance that way dependency list feels that there is a change and repeats the useEffect code block which causes the infinite loop problem

Refactored all logic into hooks but now stuck using a conditional

I've refactored all the business logic by hiding it behind hooks.
Before:
function App({productId}) {
const [item, setItem] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setLoading(true);
(async () => {
const found = await useContext(Store).find(productId);
setItem(found);
setIsLoading(false);
})();
}, [item, productId]);
}
After:
function App({productId}) {
const [item, isLoading] = useProductId(productId);
}
But how suddenly someone brought up a problem when productId is null.
I know I can't do:
function App({productId}) {
if(!productId) {
return (<Loading />);
}
const [item, isLoading] = useProductId(productId);
}
Is my only recourse to go back and change all the hooks to accept null? Because technically null isn't an error state nor is it a loading state.
Use productId as the dependencies of useEffect, which means the useEffect will be called when the productId changes. Then check productId, if it's null or invalid value, just return to end function call.
function App({ productId }) {
const [item, setItem] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (!productId) return;
setLoading(true);
(async () => {
const found = await useContext(Store).find(productId);
setItem(found);
setIsLoading(false);
})();
}, [productId]);
}

Custom hook to execute an axios request [React, Typescript]

I am trying to create a reusable custom hook (useRequest) where I can fetch data with axios, display it and have a loading state. In case of an error I want it to be caught by useRequest.
I'm having trouble catching eventual errors and passing the axios request to useRequest.
Currently I'm only getting null for the error message.
EDIT: I use generated api which uses axios. So to make my fetch request it would look something like this:
import {GeneratedApi} from '/generatedApi'
const generatedApi = new GeneratedApi(configuration) //configuration is for editing the headers etc.
const response = await generatedApi.getData();
setData(response.data);
My code:
import axios, { AxiosResponse } from "axios";
import { useEffect, useState } from "react";
const useRequest = (promise: Promise<AxiosResponse<any>>) => {
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setError(null);
await promise;
setLoading(false);
setError(null);
} catch (error) {
setLoading(false);
setError("Error: " + JSON.stringify(error));
}
};
fetchData();
}, [promise]);
return [loading, error];
};
export default function App() {
const [data, setData] = useState<any | null>(null);
const [loading, error] = useRequest(async () => {
const response = await axios.get("https://jsonplaceholder.typicode.com/todos");
setData(response.data);
return response;
});
if (loading) {
return <p>Loading ...</p>;
} else if (data) {
return <p>{data}</p>;
} else {
return <p>Error: {error}</p>;
}
}
You can pass a function, wrapped in useCallback hook, which would invoke your api call:
import axios, { AxiosResponse } from "axios";
import { useCallback, useEffect, useState } from "react";
const url = "https://jsonplaceholder.typicode.com/todos"
const useRequest = (apiCall: () => Promise<AxiosResponse<any>>, setData: (data: any) => void) => {
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setError(null);
const response = await apiCall()
setData(response.data)
setLoading(false);
setError(null);
} catch (error) {
setLoading(false);
setData(null)
setError("Error: " + JSON.stringify(error));
}
};
fetchData();
}, [apiCall, setData]);
return [loading, error];
};
export default function App() {
const [data, setData] = useState<any | null>(null);
const fun = useCallback(() => axios.get(url), [])
const [loading, error] = useRequest(fun, setData);
if (loading) {
return <p>Loading ...</p>;
} else if (data) {
return <p>{'data'}</p>;
} else {
return <p>Error: {error}</p>;
}
}
Konstantin Samarin's answer helped to point me in the right direction.
My current solution has a missing dependency(callback) and might not be ideal. Adding the dependency causes infinite rerenders.
EDIT 1: Added isMounted reference to avoid setting state on an unmounted component. Moved [data, setData] to the custom hook.
import axios, { AxiosResponse } from "axios";
import { useCallback, useEffect, useState } from "react";
interface RequestReponse {
loading: boolean,
error: string | null
}
function useRequest<T>(callback: any, dependencies: any[]): [boolean, (string | null), (T | null)] {
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<T | null>(null);
const navigate = useNavigate();
useEffect(() => {
let isMounted = true;
async function doRequest() {
try {
setError(null);
setData(null);
await callback();
if (isMounted) {
setLoading(false);
setData(null);
}
} catch (error) {
setLoading(false);
setError(error)
}
}
doRequest();
return () => {
isMounted = false;
};
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
return [loading, error, data];
}
export default function App() {
const generatedApi = new GeneratedApi();
const [loading, error, data] = useRequest(() => generatedApi.getData(), [])
if (loading) {
return <p>Loading ...</p>;
} else if (data) {
return <p>{data}</p>;
} else {
return <p>Error: {error}</p>;
}
}

How to call custom hook inside of form submit button?

I am create custom hook that fetch requests network.I want to call custom hook when form submit button clicked but depending on hook rules i can't do that. how to can implement this scenario?
this custom hook:
const useRequest = (url, method, dependencies, data = null) => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const res = await request[method](url, data);
setResponse(res);
setLoading(false);
} catch (e) {
setError(e);
setLoading(false);
}
};
fetchData();
}, dependencies);
return { response, error, loading };
};
Move fetchData function out of useEffect and export it:
const useRequest = (url, method, dependencies, data = null) => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const fetchData = async () => {
setLoading(true);
try {
const res = await request[method](url, data);
setResponse(res);
setLoading(false);
} catch (e) {
setError(e);
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, dependencies);
return { response, error, loading, fetchData };
};
Than when you can call it anywhere in your code.

Resources