I'm trying to dispatch a Redux action from a custom hook with useDispatch but I'm getting an "Invalid hook call" error. If I'm understanding the rules of hooks correctly, I don't think I'm breaking any of the rules as I'm calling useDispatch from a custom hook. What am I missing??
CODE
Main selector for functional component
export const getFlattenedDevicesList = createSelector(
getDevicesList,
(deviceList) => {
return (
deviceList
.map(makeOrganizationKey)
.map(makeProjectKey)
.map(makeDeviceProfileKey)
.map(DispatchLSU) // custom hook
);
}
);
Custom hook
export function DispatchLSU(resource) {
const dispatch = useDispatch();
console.log(resource.device_id);
const deviceId = resource.device_id;
// useEffect(() => {
// dispatch(getHealthLsu(deviceId));
// });
dispatch(getHealthLsu(deviceId));
const result = {
...resource,
};
return result;
};
The goal is to dispatch the action for each entry in deviceList
then insert new data from API call.
CodePudding user response:
Hooks rule: Only Call Hooks from React Functions
This code is breaking hooks rules, Only Call Hooks from React Functions.
You are probably calling getFlattenedDevicesList
with useSelector
then inside of this you call DispatchLSU
who call useDispatch
.
So you have a hook inside a hook that takes a callback:
useSelector
-> getFlattenedDevicesList
-> DispatchLSU
-> useDispatch
How to call a selector and dispatch with hooks ?
If you want to call dispatch
and your selector
inside a hook. Here is how to achieve it
export function DispatchLSU() {
const dispatch = useDispatch();
const resource = useSelector(getFlattenedDevicesList);
const deviceId = resource.device_id;
useEffect(() => {
dispatch(getHealthLsu(deviceId));
}, []);
const result = {
...resource,
};
return result;
};
Additional notes
- Convention is to name custom hooks with
useXXX
- Instead of dispatching inside the hook method, you can use
useDispatch
oruseCallback
to avoid making a dispatch when you render - Instead of having a selector that return all resources you can have another one that return only the
deviceId
CodePudding user response:
Your issue is that getFlattenedDevicesList
is a normal function and a normal function can not call a hook. Hooks can only be called from the top level of either 1. a react component or 2. another hook. You can convert getFlattenedDevicesList
to a hook by using another hook inside it, but then you can only do so at the top level.
i.e.
const getFlattenedDevicesList = () => {
// you can only use hooks here
if (true) {
// you can't use hooks here
// or anywhere that's not at top level of getFlattenedDevicesList
}
}
The entrypoint or root of where you start calling that hook from, along with the entire hook chain it may or may not be calling, must be either a component or a hook.
Example:
const example = () => {}
That is a normal function, not a hook.
const useExample = () => {}
That is a normal JS function, not a hook.
const useExample = () => {
const [value, setValue] = useState()
}
That's a custom hook since it's using another hook, react will figure out that it needs to be a hook as well.
const example = () => {
const [value, setValue] = useState()
}
That's also exactly the same custom hook as above and will work exactly the same as the hook above, it's just called different.
If you want to chain hooks:
const useHook1 = () => {
const [value, setValue] = useState()
return {value, setValue}
}
const useHook2 = () => {
const {value, setValue} = useHook1()
return {value}
}
That's valid because any hook can also call another hook, just like you did when using useState
.
However if you now want to use useHook2
somewhere that has to be called from another hook or a functional component.
i.e.
const Component = (): JSX.Element => {
const {value} = useHook2()
}
Is valid.
However if you try to call useHook1
or useHook2
from any normal function or anything that's not a hook or a function component it will break.
To fix it you can do something like:
const useDispatch = () => {
const dispatch = (resource) => {
// do whatever here
}
return dispatch
}
const useExample = () => {
const dispatch = useDispatch()
dispatch(// resource)
}
Note: The naming of the hook is irrelevant, you can call it anything you want. React will figure out it's a hook based on the fact that it's using other hooks or something like useState
. However it is strongly recommended that you do prefix it with use
, i.e. useSomeHook
, because 1. it makes it easy for other developers to see what is a hook and what's a normal function and 2. a lot of tools like eslint is built around a use prefix in order to apply hook rules and validations so if you don't name it with "use" those things will not work correctly.