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
}
}
Related
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
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
Context
All of my components need to fetch data.
How I fetch
Therefore I use a custom hook which fetches the data using the useEffect hook and axios. The hook returns data or if loading or on error false. The data is an object with mostly an array of objects.
How I render
I render my data conditional with an ternary (?) or the use of the short circuit (&&) operator.
Question
How can I destructure my data dependent if my useFetch hook is returning false or the data in a way i can reuse the logic or an minimal implementation to the receiving component?
What I have tried
moving the destructuring assignment into an if statement like in return. Issue: "undefined" errors => data was not available yet
moving attempt 1 to function. Issue: function does not return variables (return statement does not work either)
//issue
fuction Receiver() {
const query = headerQuery();
const data = useFetch(query);
const loaded = data.data //either ```false``` or object with ```data```
/*
The following part should be in an easy condition or passed to an combined logic but I just dont get it
destructuring assignment varies from component to component
ex:
const {
site,
data: {
subMenu: {
description,
article_galleries,
image: {
caption,
image: [{url}],
},
},
},
} = data;
*/
return loaded?(
<RichLink
title={title}
text={teaserForText}
link={link}
key={id}
></RichLink>):<Loading />
(
//for context
import axios from "axios";
import {
useHistory
} from "react-router-dom";
import {
useEffect,
useState
} from "react";
function useFetch(query) {
const [data, setData] = useState(false);
const [site, setSite] = useState(""); // = title
const history = useHistory();
useEffect(() => {
axios({
url: "http://localhost:1337/graphql",
method: "post",
data: {
query: query,
},
})
.then((res) => {
const result = res.data.data;
setData(result);
if (result === null) {
history.push("/Error404");
}
setSite(Object.keys(result)[0]);
})
.catch((error) => {
console.log(error, "error");
history.push("/Error");
});
}, [query, history, setData, setSite]);
return {
data: data,
site: site
};
}
export default useFetch;
)
You can return the error, data and your loading states from your hook. Then the component implementing the hooks can destructure all of these and do things depending upon the result. Example:
const useAsync = () => {
// I prefer status to be idle, pending, resolved and rejected.
// where pending status is loading.
const [status, setStatus] = useState('idle')
const [data, setData] = useState([])
const [error, setError] = useState(null)
useEffect(() => {
setStatus('pending')
axios.get('/').then(resp => {
setStatus('resolved')
setData(resp.data)
}).catch(err => {
setStatus('rejected') // you can handle error boundary
setError(err)
})
}, []}
return {status, data, error}
}
Component implementing this hook
const App = () => {
const {data, status, error} = useAsync()
if(status === 'idle'){
// do something
}else if(status === 'pending'){
return <Loader />
}else if(status === 'resolved'){
return <YourComponent data ={data} />
}else{
return <div role='alert'>something went wrong {error.message}</div>
}
}
the hooks can be enhanced more with the use of dynamic api functions.
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.
I have a React component using hooks like this:
const myComponent = (props) => {
useEffect(() => {
FetchData()
.then(data => {
setState({data: data});
}
// some other code
}, []);
//some other code and render method...
}
fetchData is in charge to use axios and get the data from an API:
const FetchData = async () => {
try {
res = await myApiClient.get('/myEndpoint);
} catch (err) {
console.log('error in FetchData');
res = err.response
}
}
and finally myApiClient is defined externally. I had to use this setup in order to be able to use different APIs...
import axios from "axios";
axios.defaults.headers.post["Content-Type"] = "application/json";
const myApiClient = axios.create({
baseURL: process.env.REACT_APP_API1_BASEURL
});
const anotherApiClient = axios.create({
baseURL: process.env.REACT_APP_API2_BASEURL
});
export {
myApiClient,
anotherApiClient
};
with this setup I am getting the 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 googled a bit and I saw some suggestions on how to clean up requests from useEffect, like this, but my axios is defined externally. So how can I send the cancellation using this setup?
Also, the application is using redux, not sure if it is in some way involved.
Any other suggestion to avoid the error is welcome.
You can use defer from rxjs for this:
const FetchData = () => {
try {
return myApiClient.get("/myEndpoint");
} catch (err) {
console.log("error in FetchData");
return err.response;
}
};
const myComponent = (props) => {
useEffect(() => {
const subscription = defer(FetchData()).subscribe({
next: ({
data
}) => {
setState({
data: data
});
},
error: () => {
// error handling
},
complete: () => {
// cancel loading state etc
}
});
return () => subscription.unsubscribe();
}, []);
}
Alway check if you are dealing with fetch or any long operations.
let _isMounted = false;
const HooksFunction = props => {
const [data, setData] = useState({}); // data supposed to be object
const fetchData = async ()=> {
const res = await myApiClient.get('/myEndpoint');
if(_isMounted) setData(res.data); // res.data supposed to return an object
}
useEffect(()=> {
_isMounted = true;
return ()=> {
_isMounted = false;
}
},[]);
return (
<div>
{/*....*/}
<div/>
);
}