Invalid hook call when trying to fetch data using useCallback - reactjs

I'm trying to call useState inside an async function like:
const [searchParams, setSearchParams] = useState({});
const fetchData = () => useCallback(
() => {
if (!isEmpty(searchParams)) {
setIsLoading(true); // this is a state hook
fetchData(searchParams)
.then((ids) => {
setIds(ids); // Setting the id state here
}).catch(() => setIsLoading(false));
}
},
[],
);
There are two states I am trying to set inside this fetchData function (setIsLoading and setIds), but whenever this function is executed am getting the error:
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
What is this Rule of hooks I am breaking here?
Is there any way around to set these states from the function?
PS: I only used the useCallback hook here for calling this function with lodash/debounce
Edit: The function is called inside useEffect like:
const debouncedSearch = debounce(fetchSearchData, 1000); // Is this the right way to use debounce? I think this is created every render.
const handleFilter = (filterParams) => {
setSearchParams(filterParams);
};
useEffect(() => {
console.log('effect', searchParams); // {name: 'asd'}
debouncedSearch(searchParams); // Tried without passing arguments here as it is available in state.
// But new searchParams are not showing in the `fetchData`. so had to pass from here.
}, [searchParams]);

The hook rule you are breaking concerns useCallback because you are returning it as the result of your fetchData;
useCallback should be called on top level; not in a callback, like this:
const fetchData = useCallback(
() => {
if (!isEmpty(searchParams)) {
setIsLoading(true); // this is a state hook
fetchData(searchParams)
.then((ids) => {
setIds(ids); // Setting the id state here
}).catch(() => setIsLoading(false));
}
},
[],
);
The code you wrote is equivalent to
const fetchData = () => { return React.useCallback(...
or even
function fetchData() { return React.useCallback(...
To read more about why you can't do this, I highly recommend this blog post.
edit:
To use the debounced searchParams, you don't need to debounce the function that does the call, but rather debounce the searched value. (and you don't actually the fetchData function that calls React.useCallback at all, just use it directly in your useEffect)
I recommend using this useDebounce hook to debounce your search query
const [searchParams, setSearchParams] = React.useState('');
const debouncedSearchParams = useDebounce(searchParams, 300);// let's say you debounce using a delay of 300ms
React.useEffect(() => {
if (!isEmpty(debouncedSearchQuery)) {
setIsLoading(true); // this is a state hook
fetchData(debouncedSearchParams)
.then((ids) => {
setIds(ids); // Setting the id state here
}).catch(() => setIsLoading(false));
}
}, [debouncedSearchParams]); // only call this effect again if the debounced value changes

Related

useEffect on infinite loop using async fetch function

I am trying to understand why the following useEffect is running in an infinite loop. I made the fetchSchedule helper function to call the getSchedule service (using Axios to query the API endpoint). Reason I did not define this function inside the useEffect hook is because I would like to alternatively also call it whenever the onStatus function is invoked (which toggles a Boolean PUT request on a separate endpoint).
The eslinter is requiring fetchSchedule be added to the array of dependencies, which seems to be triggering the infinite loop.
The way it should work is fetching the data from the database on first render, and then only each time either the value prop is updated or the onStatus button is toggled.
So far my research seems to point that this may have something to do with the way useEffect behaves with async functions and closures. I’m still trying to understand Hooks and evidently there’s something I’m not getting in my code…
import React, { useEffect, useCallback } from 'react';
import useStateRef from 'react-usestateref';
import { NavLink } from 'react-router-dom';
import { getSchedule, updateStatus } from '../../services/scheduleService';
import Status from './status';
// import Pagination from './pagination';
const List = ({ value }) => {
// eslint-disable-next-line
const [schedule, setSchedule, ref] = useStateRef([]);
// const [schedule, setSchedule] = useState([]);
const fetchSchedule = useCallback(async () => {
const { data } = await getSchedule(value);
setSchedule(data);
}, [value, setSchedule]);
const onStatus = (id) => {
updateStatus(id);
fetchSchedule();
console.log('fetch', ref.current[0].completed);
};
useEffect(() => {
fetchSchedule();
}, [fetchSchedule]);
return (...)
Update March 2021
After working with the repo owner for react-usestateref, the package now functions as originally intended and is safe to use as a replacement for useState as of version 1.0.5. The current implementation looks like this:
function useStateRef(defaultValue) {
var [state, setState] = React.useState(defaultValue);
var ref = React.useRef(state);
var dispatch = React.useCallback(function(val) {
ref.current = typeof val === "function" ?
val(ref.current) : val;
setState(ref.current);
}, []);
return [state, dispatch, ref];
};
You would be fine if it weren't for this react-usestateref import.
The hook returns a plain anonymous function for setting state which means that it will be recreated on every render - you cannot usefully include it in any dependency array as that too will be updated on every render. However, since the function is being returned from an unknown custom hook (and regardless, ESLint would correctly identify that it is not a proper setter function) you'll get warnings when you don't.
The 'problem' which it tries to solve is also going to introduce bad practice into your code - it's a pretty way to avoid properly handling dependencies which are there to make your code safer.
If you go back to a standard state hook I believe this code will work fine. Instead of trying to get a ref of the state in onStatus, make it async as well and return the data from fetchSchedule as well as setting it.
const [schedule, setSchedule] = useState([]);
const fetchSchedule = useCallback(async () => {
const { data } = await getSchedule(value);
setSchedule(data);
return data;
}, [value]);
const onStatus = async (id) => {
updateStatus(id);
const data = await fetchSchedule();
};
useEffect(() => {
fetchSchedule();
}, [fetchSchedule]);
Alternatively, although again I wouldn't really recommend using this, we could actually write a safe version of the useStateRef hook instead:
function useStateRef(defaultValue) {
var [state, setState] = React.useState(defaultValue);
var ref = React.useRef(defaultValue);
ref.current = state;
return [state, setState, ref];
}
A state setter function is always referentially identical throughout the lifespan of a component so this can be included in a dependency array without causing the effect/callback to be recreated.

Using React State Hook, call function after setting multiple states

I'm trying to make a function that will reset multiple states and then make an API call, however I'm having trouble making the API call happen AFTER the three states have been set. My function looks like this:
const resetFilters = () => {
setYearFilter("");
setProgressFilter("");
setSearchFilter("");
callAPI();
};
I've tried using Promise.resolve().then(), and tried using async await, but it seems the useState setter function doesn't return a promise. Is there a way to make this all happen synchronously?
You could use useEffect to listen on changes and do each task sequentially
const resetFilters = () => {
setYearFilter("")
}
useEffect(() => {
setProgressFilter("")
}, [yearFilter])
useEffect(() => {
setSearchFilter("")
}, [progressFilter])
useEffect(() => {
callAPI()
}, [searchFilter])

React setState with callback in functional components

I have a very simple example I wrote in a class component:
setErrorMessage(msg) {
this.setState({error_message: msg}, () => {
setTimeout(() => {
this.setState({error_message: ''})
}, 5000);
});
}
So here I call the setState() method and give it a callback as a second argument.
I wonder if I can do this inside a functional component with the useState hook.
As I know you can not pass a callback to the setState function of this hook. And when I use the useEffect hook - it ends up in an infinite loop:
So I guess - this functionality is not included into functional components?
The callback functionality isn't available in react-hooks, but you can write a simple get around using useEffect and useRef.
const [errorMessage, setErrorMessage] = useState('')
const isChanged = useRef(false);
useEffect(() => {
if(errorMessage) { // Add an existential condition so that useEffect doesn't run for empty message on first rendering
setTimeout(() => {
setErrorMessage('');
}, 5000);
}
}, [isChanged.current]); // Now the mutation will not run unless a re-render happens but setErrorMessage does create a re-render
const addErrorMessage = (msg) => {
setErrorMessage(msg);
isChanged.current = !isChanged.current; // intentionally trigger a change
}
The above example is considering the fact that you might want to set errorMessage from somewhere else too where you wouldn't want to reset it. If however you want to reset the message everytime you setErrorMessage, you can simply write a normal useEffect like
useEffect(() => {
if(errorMessage !== ""){ // This check is very important, without it there will be an infinite loop
setTimeout(() => {
setErrorMessage('');
}, 5000);
}
}, [errorMessage])

React Hooks , function fetchData is not a react component?

I'm playing around with react hooks and I ran into a weird issue trying to fetch some data, when i'm creating a file to fetch data using hooks and axios
this works
import axios from 'axios';
const useResources = (resource) => {
const [resources, setResources ] = useState([]);
useEffect(
() => {
(async resource => {
const response = await axios.get(`randomapi.com/${resource}`);
setResources(response.data);
})(resource);
},
[resource]
);
return resources;
};
export default useResources;
but this doesn't
import { useState, useEffect } from 'react';
import Axios from 'axios';
const fetchData = () => {
const [data, setData] = useState('');
useEffect( async () => {
const response = await Axios('randomapi.com/word?key=*****&number={number_of_words}');
setData(response.data);
});
return data;
};
export default fetchData;
'React Hook useEffect contains a call to 'setData'. Without a list of dependencies, this can lead to an infinite chain of updates. To fix this, pass [] as a second argument to the useEffect Hook.'
Aren't they the same?
On first glance, they are similar, but they still have differences.
Let's check them:
useEffect(
() => {
// we create a function, that works like a "black box" for useEffect
(async resource => {
const response = await axios.get(`randomapi.com/${resource}`);
// we don't use `setResources` in dependencies array, as it's used in wrapped function
setResources(response.data);
// We call this function with the definite argument
})(resource);
// this callback doesn't return anything, it returns `undefined`
},
// our function depends on this argument, if resource is changed, `useEffect` will be invoked
[resource]
);
useEffect hook should receive a function, which can return another function to dispose of all dynamic data, listeners (e.g. remove event listeners, clear timeout callbacks, etc.)
Next example:
// this function returns a promise, but actually useEffect needs a function,
// which will be called to dispose of resources.
// To fix it, it's better to wrap this function
// (how it was implemented in the first example)
useEffect( async () => {
const response = await Axios('randomapi.com/word?key=*****&number={number_of_words}');
setData(response.data);
// we don't set up an array with dependencies.
// It means this `useEffect` will be called each time after the component gets rerendered.
// To fix it, it's better to define dependencies
});
So, we have 2 major errors:
1) Our useEffect callback returns a Promise instead of function, which implements dispose pattern
2) We missed dependencies array. By this reason component will call useEffect callback after each update.
Let's fix them:
useEffect(() => {// we wrap async function:
(async () => {
// we define resource directly in callback, so we don't have dependencies:
const response = await Axios('randomapi.com/word?key=*****&number={number_of_words}');
setData(response.data);
})()
}, []);
Finally, we have fixed our second example :)
I even made an example with custom fetchData hook and final version of useEffect: https://codesandbox.io/s/autumn-thunder-1i3ti
More about dependencies and refactoring hints for useEffect you can check here: https://github.com/facebook/react/issues/14920

useEffect lazy created cleanup function

I'm trying to create hook that is is using an effect in which side effect function returns the cleanup callback. However I want to call it only when component is unmounted, not on the rerender.
Normal approach when you call useEffect with empty deps array won't work here as the cleanup function is created only once, on the first call of the hook. But my clean up is created later, so there is no way to change it.
function useListener(data) {
const [response, updateResponse] = useState(null);
useEffect(
() => {
if (data) {
const removeListener = callRequest(data, resp => {
updateResponse(resp);
});
return removeListener;
}
},
[data]
);
return response;
}
This comes down to a following problem: In normal class component, the willComponentUnmount could make a decision based on a current component state but in case of useEffect, state is passed via closure to the cleanup and there is no way to pass the information later if the state has changed
You can use useRef to save and update your callback function
The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class. more
function useListener(data) {
const [response, updateResponse] = useState(null);
const cleanUpCallbackRef = useRef(() => {});
useEffect(
() => {
if (data) {
cleanUpCallbackRef.current = callRequest(data, resp => {
updateResponse(resp);
});
}
},
[data]
);
useEffect(() => {
return () => {
cleanUpCallbackRef.current();
}
}, []);
return response;
}
I create a simple example here

Resources