How to properly unmount a form in useEffect cleanup function [duplicate] - reactjs

How to clean up react request in react hooks. I read that in need to enter in my hook AbortController but I don't know how. I using next.js. What are best methods to eliminate this problem ? And I get this warning:
Warning: can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
This is my custom hook to fetch data:
import { useState, useEffect, useCallback } from 'react'
import { MOVIE_API_URL, MOVIE_KEY } from '../../config'
export const useMovieDetailsFetch = (movieId) => {
const [state, setState] = useState({})
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
const fetchData = useCallback(async () => {
setError(false)
setLoading(true)
try {
const movieDetailsEndpoint = `${MOVIE_API_URL}movie/${movieId}?api_key=${MOVIE_KEY}`
const result = await (await fetch(movieDetailsEndpoint)).json()
const creditsEndpoint = `${MOVIE_API_URL}movie/${movieId}/credits?api_key=${MOVIE_KEY}`
const creditsResult = await (await fetch(creditsEndpoint)).json()
// Filtring in crew for directors only
const movieDirectors = creditsResult.crew.filter(
(member) => member.job === 'Director'
)
setState({
...result,
movieDirectors,
actors: creditsResult.cast,
})
} catch (error) {
setError(true)
}
setLoading(false)
}, [movieId])
useEffect(() => {
fetchData()
}, [fetchData])
return [state, loading, error]
}

Using an abort controller, in its rawest form:
const controller = new AbortController();
const { signal } = controller;
...
fetch(url, { signal });
...
// abort
controller.abort();
To abort an in-flight fetch in effect hook
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
fetch(url, { signal });
return () => {
controller.abort(); // abort on unmount for cleanup
};
}, []);
I found this article very informative when I needed to develop a way to cancel fetch requests.
Edit
The signal needs to be added to the fetch requests options object. You can also define the async fetchData function inside the effect (this is normal), so it's all enclosed in the effect hook's callback scope.
export const useMovieDetailsFetch = (movieId) => {
const [state, setState] = useState({})
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
const fetchData = async () => {
setError(false);
setLoading(true);
try {
const movieDetailsEndpoint = `${MOVIE_API_URL}movie/${movieId}?api_key=${MOVIE_KEY}`;
const result = await (
await fetch(movieDetailsEndpoint, { signal })
).json();
const creditsEndpoint = `${MOVIE_API_URL}movie/${movieId}/credits?api_key=${MOVIE_KEY}`;
const creditsResult = await (
await fetch(creditsEndpoint, { signal })
).json();
// Filtring in crew for directors only
const movieDirectors = creditsResult.crew.filter(
(member) => member.job === 'Director'
);
setState({
...result,
movieDirectors,
actors: creditsResult.cast,
});
} catch (error) {
setError(true);
}
setLoading(false);
}
fetchData();
return () => controller.abort();
}, [movieId]);
return [state, loading, error];
}

Related

Unmounted component - React Native

I have to functions/const to get data from API:
const [isLoadingRoom, setLoadingRoom] = useState(true);
const [isLoadingLobby, setLoadingLobby] = useState(true);
const [rooms, setRooms] = useState([]);
const [lobbies, setLobbies] = useState([]);
const getRooms = async () => {
let isMounted = true;
async function fetchData() {
const response = await fetch(link);
const json = await response.json();
// 👇️ only update state if component is mounted
if (isMounted) {
setRooms(json);
setLoadingRoom(false);
}
}
fetchData();
return () => {
isMounted = false;
}
}
const getLobbies = async () => {
let isMounted = true;
async function fetchData() {
const response = await fetch(link);
const json = await response.json();
// 👇️ only update state if component is mounted
if (isMounted) {
setLobbies(json);
setLoadingLobby(false);
}
}
fetchData();
return () => {
isMounted = false;
}
}
useEffect(() => {
const roomInterval = setInterval(() => {
getRooms();
getLobbies();
}, 5000);
return () => clearInterval(roomInterval);
}, []);
The API gets data every 5 second, but after a while I get this message:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I have tried different approaches to fetch the API with const, functions, async etc. but I get this error message anyway.. Any tips?
useRef rather than normal variable:
const isMountedRef = useRef(true);
useEffect(() => {
const roomInterval = setInterval(() => {
getRooms();
getLobbies();
}, 5000);
return () => {
clearInterval(roomInterval);
isMountedRef.current = false;
};
}, []);
and change check conditions to
if(isMountedRef.current){
// execute setState
}
Hope it helps. feel free for doubts

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

How to test component that uses custom hook with React-testing-library?

I have a custom hook to make async calls with setting errors, loadings etc.
import { useEffect, useState } from 'react';
const useMakeAsyncCall = ({ asyncFunctionToRun = null, runOnMount = false }) => {
const [response, setResponse] = useState(null);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const fetchData = async () => {
setLoading(true);
try {
const res = await asyncFunctionToRun();
const json = await res.json();
setResponse(json);
setLoading(false);
} catch (error) {
setError(error);
setLoading(false);
}
};
useEffect(() => {
if (runOnMount && asyncFunctionToRun !== null) fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [runOnMount]);
return { response, error, loading, fetchData };
};
export default useMakeAsyncCall;
In component I am using it like this
const { error, isLoading, fetchData } = useMakeAsyncCall({
asyncFunctionToRun: () => signUpUser(),
runOnMount: false,
});
const signUpUser = () => {
...some requests to firebase
};
const handleSumbit = (e) => {
e.preventDefault();
fetchData();
};
Now I am trying to test this logic.
it('does things', async () => {
const { container, getByTestId } = render(<Component/>);
const form = getByTestId('form');
fireEvent.submit(form);
expect(container.firstChild).toMatchSnapshot();
});
And I'm getting this error Warning: An update to Component inside a test was not wrapped in act(...) and it is pointing to setError and setLoading inside my hook. How to go about fixing it and testing this functionality?

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