How to execute a function that contains a react hook inside - reactjs

I'm building an app with react and nodeJS. I want to make an API call and use a react hook inside this function to get a token from Auth0 to authenticate with the API.
async function CallApi(userId) {
const { getTokenSilently } = useAuth0();
const token = await getTokenSilently();
try {
const response = await fetch(process.env.API_URL + "users", {
headers: {
Authorization: `Bearer ${token}`,
user_id: userId
}
});
var result = JSON.stringify(await response.json());
} catch (error) {
console.error(error);
}
return result;
};
export default CallApi;
Now I want to call this function in another file:
async componentDidMount() {
this.setState({ readError: null });
try {
var profile = await CallApi("own");
this.setState({
username: profile.username,
email: profile.email
})
} catch (error) {
this.setState({ readError: error.message });
}
}
Now if I want open this site I getting the error Unhandled Rejection (Error): Invalid hook call. Hooks can only be called inside of the body of a function component.
What I am doing wrong and how can I achieve to get a token from this hook?

Because use are using a hook call useAuth0==> you need to call it in a body of functional component or a customer hook which is have pre-fix use ==> you CallApi is not a hook ==> you got the error.
Create useCallApi hook.
function useCallApi(userId) {
const [result, setResult] = c()
const { getTokenSilently } = useAuth0();
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const fetchResult = useCallback( async(userId) =>{
const token = await getTokenSilently();
try {
setLoading(true)
const response = await fetch(process.env.API_URL + "users", {
headers: {
Authorization: `Bearer ${token}`,
user_id: userId
}
});
setLoading(false)
var result = JSON.stringify(await response.json());
setResult(result)
} catch (error) {
setLoading(false)
setError(error)
console.error(error);
}
},[userId])
useEffect(() =>{
if(userId){
fetchResult()
}
},[userId])
return {userInfor: result, loading, error,fetchResult};
};
export default useCallApi;
Now I want to call this function in another file, but you need to
convert your existing component to functional component and use
useCallApi hook.
IF you pass a string when you declare useCallAPi like this useCallAPi("own"), it will call the api dicrectly.
You can also const {result, fetchResult} = useCallAPi("own") and call fetchResult("own") anywhere you want to fetch result
//other import
import {useCallAPi} from "path_to/useCallApi.js"
// you need to convert your class compoenent to this compoenent
function AFunctionalComponent(userId) {
const [readError, setReadError] = useState(null)
// result include username and email walready
const {result} = useCallAPi("own")
return(
// your ui render herer
)
};
Updated: If you want to set local state base on result in the functional component you can try this
//other import
import {useCallAPi} from "path_to/useCallApi.js"
// you need to convert your class compoenent to this compoenent
function AFunctionalComponent(userId) {
const [readError, setReadError] = useState(null)
const [useInfo, setUserInfo] = useState(null)
// result include username and email walready
const {result} = useCallAPi("own")
useEffect(() =>{
if(result){
setUserInfo({
username: result. username,
email: result.email
})
}
},[result])
return(
// your ui render herer
)
};

Took me forever to figure this out, but finally resulted going back to go ol' callbacks. Only thing that worked for me (because I HAD to call the hook from non-react context, all other's threw the know React Hook Call (outside functional component) error.
Btw, this setup makes any hook accessible from ANY (instantiated) context, even vanilla-js, like it my example!
Non-React or React Context:
class WhateverClass {
private xyzHook: (XyzHookContextI) | undefined
public setHookAccessor (xyzHook: XyzHookContextI): void {
this.xyzHook = xyzHook
}
executeHook (): void {
const hookResult = this.xyzHook?.specificHookFunction()
...
}
}
export const Whatever = new WhateverClass() // singleton
Your hook (or your wrapper for an external Hook)
export interface XyzHookContextI {
specificHookFunction: () => Promise<string>
}
const XyzHookContext = createContext<XyzHookContextI>(undefined as any)
export function useXyzHook (): XyzHookContextI {
return useContext(XyzHookContextI)
}
export function XyzHook (props: PropsWithChildren<{}>): JSX.Element | null {
async function specificHookFunction (): Promise<void> {
...
}
const context: XyzHookContextI = {
specificHookFunction
}
// and here comes the magic in wiring that hook up with the non function component context via callback
Whatever.setHookAccessor(context)
return (
< XyzHookContext.Provider value={context}>
{props.children}
</XyzHookContext.Provider>
)
}
Voila, now you can use ANY react code (via hook) from any other context (class components, vanilla-js, …)!
(…hope I didn't make to many name change mistakes :P)

https://reactjs.org/warnings/invalid-hook-call-warning.html
To avoid confusion, it’s not supported to call Hooks in other cases:
🔴 Do not call Hooks in class components.
🔴 Do not call in event handlers.
🔴 Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.

Related

Typescript with React: Using custom hook in useEffect

I'm trying to use a hook inside of a useEffect call to run only once (and load some data).
I keep getting the error that I can't do that (even though I've done the exact same thing in another app, not sure why 1 works and the other doesn't), and I understand I may be breaking the Rules of Hooks... so, what do I do instead? My goal was to offload all the CRUD operation logic into a simple hook.
Here's MenuItem, the component trying to use the hook to get the data.
const MenuItem = () => {
const [ID, setID] = useState<number | null>(null);
const [menu, setMenu] = useState<Item[]>([]);
const { getMenu, retrievedData } = useMenu();
//gets menu items using menu-hook
useEffect(() => {
getMenu();
}, []);
//if menu is retrieved, setMenu to retrieved data
useEffect(() => {
if (retrievedData.length) setMenu(retrievedData);
}, []);
//onClick of menu item, displays menu item description
const itemHandler = (item: Item) => {
if (ID === null || ID !== item._id) {
setID(item._id);
} else {
setID(null);
}
};
return ...
};
And here's getMenu, the custom hook that handles the logic and data retrieval.
const useMenu = () => {
const backendURL: string = 'https://localhost:3001/api/menu';
const [retrievedData, setRetrievedData] = useState<Item[]>([]);
const getMenu = async () => {
await axios
.get(backendURL)
.then((fetchedData) => {
setRetrievedData(fetchedData.data.menu);
})
.catch((error: Error) => {
console.log(error);
setRetrievedData([]);
});
};
return { getMenu, retrievedData };
};
export default useMenu;
And finally here's the error.
Invalid hook call. Hooks can only be called inside of the body of a function component.
I'd like to add I'm also using Typescript which isn't complaining right now.
There's a few things you can do to improve this code, which might help in future. You're right that you're breaking the rule of hooks, but there's no need to! If you move the fetch out of the hook (there's no need to redefine it on every render) then it's valid not to have it in the deps array because it's a constant.
I'd also make your useMenu hook take care of all the details of loading / returning the loaded value for you.
const fetchMenu = async () => {
const backendURL: string = 'https://localhost:3001/api/menu';
try {
const { data } = await axios.get(backendURL);
return data.menu;
} catch (error: AxiosError) {
console.log(error);
return [];
};
}
export const useMenu = () => {
const [items, setItems] = useState<Item[]>([]);
useEffect(() => {
fetchMenu.then(result => setItems(result);
}, []);
return items;
};
Now you can consume your hook:
const MenuItem = () => {
const [ID, setID] = useState<number | null>(null);
// Now this will automatically be an empty array while loading, and
// the actual menu items once loaded.
const menu = useMenu();
// --- 8< ---
return ...
};
A couple of other things -
Try to avoid default exports, because default exports are terrible.
There are a lot of packages you can use to make your life easier here! react-query is a good one to look at as it will manage all the lifecycle/state management around external data
Alternatively, check out react-use, a collection of custom hooks that help deal with lots of common situations like this one. You could use the useAsync hook to simplify your useMenu hook above:
const backendURL: string = 'https://localhost:3001/api/menu';
const useMenu = () => useAsync(async () => {
const { data } = await axios.get(backendURL);
return data.menu;
});
And now to consume that hook:
const MenuItem = () => {
const { value: menu, loading, error } = useMenu();
if (loading) {
return <LoadingIndicator />;
}
if (error) {
return <>The menu could not be loaded</>;
}
return ...
};
As well as being able to display a loading indicator while the hook is fetching, useAsync will not give you a memory leak warning if your component unmounts before the async function has finished loading (which the code above does not handle).
After working on this project for some time I've also found another solution that is clean and I believe doesn't break the rule of hooks. This requires me to set up a custom http hook that uses a sendRequest function to handle app wide requests. Let me make this clear, THIS IS NOT A SIMPLE SOLUTION, I am indeed adding complexity, but I believe it helps since I'll be making multiple different kinds of requests in the app.
This is the sendRequest function. Note the useCallback hook to prevent unnecessary rerenders
const sendRequest = useCallback(
async (url: string, method = 'GET', body = null, headers = {}) => {
setIsLoading(true);
const httpAbortCtrl = new AbortController();
activeHttpRequests.current.push(httpAbortCtrl);
try {
const response = await fetch(url, {
method,
body,
headers,
signal: httpAbortCtrl.signal,
});
const responseData = await response.json();
activeHttpRequests.current = activeHttpRequests.current.filter(
(reqCtrl) => reqCtrl !== httpAbortCtrl
);
if (!response.ok) throw new Error(responseData.message);
setIsLoading(false);
return responseData;
} catch (error: any) {
setError(error);
setIsLoading(false);
throw error;
}
},
[]
);
Here's the new useMenu hook, note I don't need to return getMenu as every time sendRequest is used in my app, getMenu will automatically be called.
export const useMenu = () => {
const { sendRequest } = useHttpClient();
const [menu, setMenu] = useState<MenuItem[]>([]);
const [message, setMessage] = useState<string>('');
useEffect(() => {
const getMenu = async () => {
try {
const responseData = await sendRequest(`${config.api}/menu`);
setMenu(responseData.menu);
setMessage(responseData.message);
} catch (error) {}
};
getMenu();
}, [sendRequest]);
return { menu, message };
};
Good luck

useSelector Hook - Invalid hook call. Hooks can only be called inside of the body of a function component

Why i got this error?
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component.
Here's my code in useSession.js:
import { useSelector } from 'react-redux';
export const useSession = () => {
const session = useSelector(state => state.session)
return session
}
And code in Auth.js
import { useSession } from './useSession';
export const getServerSideProps = options => gssp => {
const { signedIn, redirectTo } = options;
return async ctx => {
const session = useSession();
if (signedIn && !session) {
return {
redirect: {
destination: redirectTo || '/login',
permanent: false,
}
}
}
const result = await gssp(ctx);
return {
...result,
props: {
...result.props,
session,
},
}
}
};
You are breaking the rules of hooks. Namely Don’t call Hooks from regular JavaScript functions. You can only use a hook from synchronous render of a react functional component. But here you are calling a hook from a plain javascript function that could be executed at any time.
To fix this you'll have to move the hook to the root level of your component.
For example:
function MyComp() {
const session = useSelector(state => state.session)
return <></>
}
If you want to encapsulate that logic into something reusable, you can make a custom hook.
export const useSession = () => { // note the name starts with `use`.
const session = useSelector(state => state.session)
return session
}
Which then must obey the rules of hooks itself:
function MyComp() {
const session = useSession()
return <></>
}

How to GET data in a custom hook and dispatch it to context

I'm fairly new to the context API and react hooks beyond useState and useEffect so please bare with me.
I'm trying to create a custom useGet hook that I can use to GET some data from the backend then store this using the context API, so that if I useGet again elsewhere in the app with the same context, it can first check to see if the data has been retrieved and save some time and resources having to do another GET request. I'm trying to write it to be used generally with various different data and context.
I've got most of it working up until I come to try and dispatch the data to useReducer state and then I get the error:
Hooks can only be called inside the body of a function component.
I know I'm probably breaking the rules of hooks with my call to dispatch, but I don't understand why only one of my calls throws the error, or how to fix it to do what I need. Any help would be greatly appreciated.
commandsContext.js
import React, { useReducer, useContext } from "react";
const CommandsState = React.createContext({});
const CommandsDispatch = React.createContext(null);
function CommandsContextProvider({ children }) {
const [state, dispatch] = useReducer({});
return (
<CommandsState.Provider value={state}>
<CommandsDispatch.Provider value={dispatch}>
{children}
</CommandsDispatch.Provider>
</CommandsState.Provider>
);
}
function useCommandsState() {
const context = useContext(CommandsState);
if (context === undefined) {
throw new Error("Must be within CommandsState.Provider");
}
return context;
}
function useCommandsDispatch() {
const context = useContext(CommandsDispatch);
if (context === undefined) {
throw new Error("Must be within CommandsDispatch.Provider");
}
return context;
}
export { CommandsContextProvider, useCommandsState, useCommandsDispatch };
useGet.js
import { API } from "aws-amplify";
import { useRef, useEffect, useReducer } from "react";
export default function useGet(url, useContextState, useContextDispatch) {
const stateRef = useRef(useContextState);
const dispatchRef = useRef(useContextDispatch);
const initialState = {
status: "idle",
error: null,
data: [],
};
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case "FETCHING":
return { ...initialState, status: "fetching" };
case "FETCHED":
return { ...initialState, status: "fetched", data: action.payload };
case "ERROR":
return { ...initialState, status: "error", error: action.payload };
default:
return state;
}
}, initialState);
useEffect(() => {
if (!url) return;
const getData = async () => {
dispatch({ type: "FETCHING" });
if (stateRef.current[url]) { // < Why doesn't this also cause an error
const data = stateRef.current[url];
dispatch({ type: "FETCHED", payload: data });
} else {
try {
const response = await API.get("talkbackBE", url);
dispatchRef.current({ url: response }); // < This causes the error
dispatch({ type: "FETCHED", payload: response });
} catch (error) {
dispatch({ type: "ERROR", payload: error.message });
}
}
};
getData();
}, [url]);
return state;
}
EDIT --
useCommandsState and useCommandsDispatch are imported to this component where I call useGet passing the down.
import {
useCommandsState,
useCommandsDispatch,
} from "../../contexts/commandsContext.js";
export default function General({ userId }) {
const commands = useGet(
"/commands?userId=" + userId,
useCommandsState,
useCommandsDispatch
);
Why am I only getting an error for the dispatchRef.current, and not the stateRef.current, When they both do exactly the same thing for the state/dispatch of useReducer?
How can I refactor this to solve my problem? To summarise, I need to be able to call useGet in two or more places for each context with the first time it's called the data being stored in the context passed.
Here are various links to things I have been reading, which have helped me to get this far.
How to combine custom hook for data fetching and context?
Updating useReducer 'state' using useEffect
Accessing context from useEffect
https://reactjs.org/warnings/invalid-hook-call-warning.html
I think your problem is because you are using useRef instead of state for storing state. If you useRef for storing state you need to manually tell react to update.
I personally would not use reducer and just stick to the hooks you are familiar with as they fulfill your current requirements. I also think they are the best tools for this simple task and are easier to follow.
Code
useGetFromApi.js
This is a generalized and reusable hook - can be used inside and outside of the context
export const useGetFromApi = (url) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!url) return;
const getData = async () => {
try {
setLoading(true);
setData(await API.get('talkbackBE', url));
} catch ({ message }) {
setError(message);
} finally {
setLoading(false); // always set loading to false
}
};
getData();
}, [url]);
return { data, error, loading };
};
dataProvider.js
export const DataContext = createContext(null);
export const DataProvider = ({ children, url}) => {
const { data, error, loading } = useGetFromApi(url);
return (
<DataContext.Provider value={{ data, error, loading }}>
{children}
</DataContext.Provider>
);
};
useGet.js
Don't need to check if context is undefined - React will let you know
export const useGet = () => useContext(DataContext);
Usage
Most parent wrapping component that needs access to data. This level doesn't have access to the data - only it's children do!
const PageorLayout = ({children}) => (
<DataProvider url="">{children}</DataProvider>
)
A page or component that is nested inside of the context
const NestedPageorComponent = () => {
const {data, error, loading } = useGet();
if(error) return 'error';
if(loading) return 'loading';
return <></>;
}
Hopefully this is helpful!
Note I wrote most of this on Stack in the editor so I was unable to test the code but it should provide a solid example

How to get imported functions to set state of functional component?

I have a react class component with rather lengthy onSubmit function that I have put into another file in order to keep the code a bit tidier.
I tried to convert the class component to a functional one, replacing all of my state and setState functions with useState but now my useState state updaters are returning undefined inside the imported function. Am I able to update state using an imported function with a functional component? The function worked fine when it was imported into a class component and my state updater was setState();
//Imported function in utils.js
export const loginUser = async function (email, password) {
try {
const login = await axios.post('http://localhost:5000/api/v1/auth/login', {
email,
password
});
const options = {
headers: {
Authorization: `Bearer ${login.data.token}`
}
};
const getUser = await axios.get(
'http://localhost:5000/api/v1/auth/me',
options
);
const user = getUser.data.data;
setAuthenticated(true);
setUser(getUser.data.data);
setEmail('');
setPassword('');
localStorage.setItem('user', JSON.stringify(user));
console.log(localStorage.getItem('user'));
} catch (err) {
console.log(err);
}
};
// Functional component with imported function
import React, { useState } from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
import { Login } from './Login';
const { loginUser } = require('../utils/utils');
export const Splash = () => {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
const [authenticated, setAuthenticated] = useState(false);
const [msg, setMsg] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const _handleEmail = (e) => {
setEmail(e.target.value);
};
const _handlePass = (e) => {
setPassword(e.target.value);
};
const _handleSubmit = async (e) => {
e.preventDefault();
loginUser(email, password);
if (user) {
console.log(user);
this.props.onHandleUser(user);
}
};
return (
<div className='splashStyle'>
{!authenticated && (
<Login
handleEmail={_handleEmail}
handlePass={_handlePass}
handleSubmit={_handleSubmit}
isAuthenticated={authenticated}
/>
)}
</div>
);
};d
EDIT: My issue that setAuthenticated, setUser, setEmail, and setPassword are coming undefined in utils.js
Thanks!
One way of achieving that would be passing all the set methods as a paramters to loginUser function.
But a better way of doing this will be like:
create two separate files
1 for login api call like :
login.js
function login(email, password){
const login = await axios.post('http://localhost:5000/api/v1/auth/login', {
email,
password
});
return login.data;
}
another for getting data
getProfile.js
function getProfile(token){
const options = {
headers: {
Authorization: `Bearer ${token}`
}
};
const getUser = await axios.get(
'http://localhost:5000/api/v1/auth/me',
options
);
return getUser.data;
}
Now do you setting state stuff in actuall component submit call function like
const _handleSubmit = async (e) => {
e.preventDefault();
const token = await login(email, password);
const user = await getProfile(token);
if (user) {
console.log(user);
props.onHandleUser(user);
setAuthenticated(true);
setUser(getUser.data.data);
setEmail('');
setPassword('');
localStorage.setItem('user', JSON.stringify(user));
console.log(localStorage.getItem('user'));
}
};
You need to pass the setAuthenticated function to the loginUser function before calling it in that.
return an onSubmiHandler function from your login user hook.
const doLogin = (email , password) => {
/// your code here
}
return {doLogin}
then use the doLogin function inside your main component
//inside functional component
const {doLogin} = loginUser();
onSubmit = () => doLogin(email, password)
for more you can see how to use custom hooks from here
https://reactjs.org/docs/hooks-custom.html
To start loginUser can't know about the setState you insert there try to pass it as arguments and it will fix it 😁
another problem I see is that you use the this keyword and in the functional component you use the just props.
and just for you to know don't pass null as the initial value pass an empty string, number, etc..
Update
this is how you also pass a setState as argument
loginUser((e)=>setEmail(e))

React HOC to handel child component API failure

I am using a component from a library which accepts an accessToken as a props. Like
<AnalyticsChart accesstoken={this.state.token} /> and based on the token, it makes an API call and renders an UI based on the result. Now, if the accesstoken has expired, it doesn't have a callback props to notify the the parent component (over which I have complete control).
SO, is there a way to build an Higher Order Component which could listen for API requests made by the child components, or just maybe watch for DOM changes to see if nothing is rendered for a long time?
Example-
export default MyComponent extends Component{
render(){
<AnalyticsComponent accesstoken={this.props.token} />
}
}
You can have a withFetch HOC to inject your modified fetch function or a simple custom hook.
1. HOC Implementation
// UNTESTED
function withFetch(Component) {
return function(props) {
const [error, setError] = React.useState(false);
// modified fetch
const doFetch = React.useCallback((url, options) => {
try {
fetch(url, options)
// proceed if successful
} catch(error) {
setError(true);
// TODO: add other states to store error message
}
}, [params])
// run effect every time there's fetch error
React.useEffect(() => {
if (error) {
// TODO: do something
}
}, [error]);
return <Component fetch={doFetch} {...props} />
}
}
const EnhancedComponent = withFetch(MyComponent)
return <EnhancedComponent accessToken="some token" />
2. Custom Hook
function hasFetchError(url, options) {
const [error, setError] = React.useState(false);
React.useEffect(() => {
async function doFetch() {
try {
await fetch(url, options)
// do something with response
} catch(error) {
setError(true);
}
}
doFetch();
}, [url, options])
return error;
}
// Usage:
function MyComponent(props) {
const error = hasFetchError(props.url, props.options);
if (error) {
// do something
}
}

Resources