React how to set context from a function. Invalid hook call - reactjs

I'm not able to retrieve a context value from a function not in a Component. I receive following exception:
Uncaught (in promise) 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...
I've declared my context.
export const ErrorContext = createContext({})
export const UseErrorContext = () => useContext(ErrorContext)
Set up a provider within my App.js
<ErrorContext.Provider value={{ errorMessage }}>
</ErrorContext.Provider>
And like to set the value from a function like so. But this results in Exception above. This function is in a seperate file and called from different components.
export const MyFunction = async (id) => {
const { errorMessage } = UseErrorContext();
errorMessage = "SOME ERROR MESSAGE";
}

You won't be able to do it this way:
You can't call a hook outside the render phase in react.
Besides, even if you call it this way, the assignment is local and wouldn't affect the context value.
To achieve your desired behavior, you need:
A state to hold the error message along with a state setter
The context value should change each time the error changes
You should grab the state setter from a component calling your hook
Pass your state setter as a callback to your async function
Something like this:
// context
export let ErrorContext = createContext({})
export let useErrorContext = () => useContext(ErrorContext)
// provider
let [errorMessage, setErrorMessage] = useState();
let value = useMemo(() => ({errorMessage, setErrorMessage}), [errorMessage])
<ErrorContext.Provider value={value}>
{children}
</ErrorContext.Provider>
// component
let {setErrorMessage} = useErrorContext();
export const myFunction = async (id, setErrorMessage) => {
setErrorMessage("SOME ERROR MESSAGE");
}
// later, in an event:
myFunction(id, setErrorMessage);
PS: This solution requires you to do a Provider per error message so you can have multiple, or you should change the abstraction.
Another solution which may seem like a promotion to my own library, but it is not, just to show how easily it solves this:
yarn add -D async-states react-async-states
import {useAsyncState, createSource} from "react-async-states"
// myErrorMessage should be unique, or else the library will warn
let myErrorMessageSource= createSource("myErrorMessage");
// in your component
let {state, setState} = useAsyncState(myErrorMessageSource);
// then use the state.data to display it
// in your async function, and your component would react to it
myErrorMessageSource.setState("SOME ERROR MESSAGE");
This is just a basic use case of the library, please read the docs before opt for it.

as the error says :
Hooks can only be called inside of the body of a function component
what I suggest is to create your custom hook
A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.
now this is possible
export const useMyFunction = async (id) => {
const { errorMessage } = UseErrorContext();
...
}
make sure to only call other Hooks unconditionally at the top level of your custom Hook.
same when calling your custom hook from another component

Related

Recoil useRecoilValue outside component

I want to build a JSON file based on a bunch of recoil values, so I created a function that fetches all the recoilVaules and returns a JSON out of it, and I call it from a component.
The thing is that it gives me an error for using useRecoilValue outside a component.
besides moving the function inside the component (which will make the component look bad because there are a lot of values) or passing all the recoilValue as parameters to the function (same), what more can I do?
This is the general idea -
const ReactCom = () => {
getJson();
....
}
const getJson = () => {
let jsonFile = useRecoilValue(someAtom);
return jsonFile;
}
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
This should work
const ReactCom = () => {
const json = useGetJson();
....
}
const useGetJson= () => {
let jsonFile = useRecoilValue(someAtom);
return jsonFile;
}

How to use useCallback inside custom hook

got little issue here. I'm exporting a custom hook, which uses other hooks, but I get "Unhandled Rejection (Error): Invalid hook call..." Error. Looks like I'm not doing something wrong... I'm using hook inside hook, so why is this error being thrown?
const customHook = async () => {
const { values } = useValues()
const testCallback = useCallback(async () => {
const value = values
return value
}, [value])
return testCallback
}
export default customHook
I think the issue can be resolved by just changing the case of your component's name, try renaming it to "CustomHook". There's a rule with react hooks that you can not call use hook inside a function or component whose name is in Camel Case.

How to use react-query useQuery inside onSubmit event?

im noob using react query and react with typesript, i don't know how to solve this:
React Hook "useQuery" is called in function "onSubmit" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter.
export const LoginForm = () => {
const { handleSubmit, control } = useForm<IFormInput>({defaultValues: defaultValues, resolver: yupResolver(schema)});
const onSubmit = ({email, password}: IFormInput) => {
const {data, isLoading, error} = useQuery('loginUser', () => startLogin({email, password}));
console.log(data);
console.log(error);
};
...
...
...
}
export const startLogin = ({email, password}: IFormInput) => (
axios.post(loginEndpoint, {email, password}).then(res => res.data)
);
I’d like to add that for logging in a user, you probably don’t want a query but a mutation. Logging someone in is likely not a GET request and it has side effects (it makes the user be logged in), so useMutation is for that. You can define useMutation on the top of your functional component (in accordance with the rules of hooks) and invoke the returned mutate function in the callback:
export const LoginForm = () => {
const { mutate, isLoading } = useMutation(variables => startLogin(variables))
const onSubmit = ({email, password}: IFormInput) => {
mutate({ email, password })
};
First of all, the problem is just an eslint react plugin (rules-of-hooks) complaining about your use of a hooks.
You can tell it to shutup by adding the following comment above the line where the error is happening:
// eslint-disable-line react-hooks/rules-of-hooks
However...
this is only going to prevent you from understanding the correct way of using hooks in react. Before you proceed with the above, this is the correct way to handle your use case.
First move the hook outside of the function, and instead of useQuery, use useMutation:
export const LoginForm = () => {
const { handleSubmit, control } = useForm<IFormInput>({defaultValues: defaultValues, resolver: yupResolver(schema)});
const { data, error, mutate: loginUser } = useMutation(startLogin);
const onSubmit = ({email, password}: IFormInput) => {
loginUser({email, password});
console.log(data);
console.log(error);
};
}
Now that that's done, as I mentioned before, reason you got the complaint from eslint is because the rules-of-hooks plugin has plain no nonsense rule that assumes any function starting with use* must be a react hook. It also assumes that such a function is only used inside a react component hence the part of the message that says:
React component names must start with an uppercase letter.
The reason this rule exists is explained here. Essentially it comes down to the following:
rules-of-hooks assumes that any custom react hook will make use of one of the existing react hooks (useEffect, useState, useMemo, etc...), and since these hooks affect the state of the component, it is assumed that your custom hook does soo to (even though you may not have written the useQuery hook).
React functional components rely on order of hooks to maintain state. If that order is broken at any point during the render of the component, the component is unable to maintain its internal state.
Therefore, specify all potential mutations at the top of your components.
Your onSubmit() doesn't look like being executed inside a React Component, and you also need to rename your component to a PascalCase.
In most cases, rename your component to PascalCase will fix the issue.

Does this "custom react hook" breaks the hooks law?

I am using many useCallbacks in useEffects in the same format syntax and feel like shortening them. So, I map them in this hook. effects is an array of useCallback functions.
import React from 'react';
const useEffects = (effects: Function[]) =>
effects.map((effect) =>
React.useEffect(() => {
effect();
}, [effect])
);
export default useEffects;
Hooks can't be defined inside an array.
I wrote an article recently about the rendering order of hooks. https://windmaomao.medium.com/understanding-hooks-part-4-hook-c7a8c7185f4e
It really goes down to below snippet how Hooks is defined.
function hook(Hook, ...args) {
let id = notify()
let hook = hooks.get(id)
if(!hook) {
hook = new Hook(id, element, ...args)
hooks.set(id, hook)
}
return hook.update(...args)
}
When a hook is registered, it requires an unique id, as in line notify(). Which is just a plain i++ placed inside the Component where the hook is written inside.
So if you have a fixed physical location of the hook, you have a fixed id. Otherwise the id can be random, and since the render Component function is called every render cycle, a random id is not going to find the right hook in the next render cycle.
That is why if can not be written before the hook statement either. Just try
const Component = () => {
const b = true
if (!b) return null
const [a] = useState(1)
}
You should get similar error.

Why does a hook state change cause component to render?

In an example app, in a function component:
const [searchApi, results, errorMessage] = useResults();
Further in the component, other components are rendered and they get passed the results/errorMessage. A textInput can also call the searchApi function on completion,
The corresponting useResults is declared inside useResults.js:
export default () => {
const [results, setResults] = useState([]);
const [errorMessage, setErrorMessage] = useState('');
const searchApi = async searchTerm => {
//makes a request, depending on the success it either uses setResults or setErrorMessage
};
return [searchApi, results, errorMessage];
};
Now, if searchApi sets the state of the results or errorMessage inside the exported function itself, since I take it they may be considered as local variables, and the results and errorMessage inside the main component merely get the returned values before they even changed, then how come a change of state inside the function changes the variables outside of it? In other words, why does the component just re-render?
As long as searchApi function calls either setResults or setErrorMessage, the react hook called will notify the component that contains the hook to re-render. That is also true when it comes to custom hooks that are using useState.
This behaviour makes sense, because as you said, results and errorMessage are some local variables that are not directly connected to the state. They are a snapshot of how the states looked like when the render function kicked in. If one of them changes, the only way the component will know about the change is by re-rendering (and getting the new values by calling the useState hook).

Resources