Tanstack/React query with a reusable function throwing errors - javascript

I'm using Tanstack query to fetch data from the back end. The purpose is to have a generic function which would authorize the user before fetching the data.
const queryClient = new QueryClient()
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
root.render(
<React.StrictMode>
<ChakraProvider theme={theme}>
<QueryClientProvider client={queryClient}>
<Router basename={process.env.PUBLIC_URL}>
<Auth0ProviderWithHistory>
<App />
</Auth0ProviderWithHistory>
</Router>
</QueryClientProvider>
</ChakraProvider>
</React.StrictMode>
)
Then I have this useFetch function
//useFetch.js
import axios, { Method } from "axios"
import { audience } from "../utils/dataUrls"
import { useAuth0 } from "#auth0/auth0-react"
const base = {
"Access-Control-Allow-Origin": process.env.REACT_APP_ACCESS_CORS || ""
}
const { getAccessTokenSilently, getAccessTokenWithPopup } = useAuth0()
const useFetch = async (url: string, method: Method, headers?: Record<string, any>, body?: unknown) => {
const tokenGetter =
process.env.REACT_APP_ENVIRONMENT === "local" ? getAccessTokenWithPopup : getAccessTokenSilently
const token = await tokenGetter({
audience: audience
})
const { data } = await axios.request({
url,
headers: { ...base, Authorization: `Bearer ${token}` },
method,
data: body
})
return data
}
export default useFetch
And finally, when I try to call the function using useQuery (Inside a functional component) like this -
const checkIfTokenExists = async () => {
const test = useQuery(["getExistingPAT"], await useFetch(`${personalAccessToken}`, "get"))
console.log(test)
}
// const { status, data, isFetching } = checkIfTokenExists()
// console.log(status, data, isFetching)
useEffect(() => {
checkIfTokenExists()
}, [])
I am getting the following error: Warning: Invalid hook call. Hooks can only be called inside of the body of a function component.
Any suggestions on how I could fix this please?

Please have a look at this Github issue where jrozbicki describes a good solution for this problem. It is not necessary to create a custom hook to handle the authorization logic.

Related

Component keeps making api calls with custom axios hook

I have a dashboard component which renders some widget. It fetches widget data from four different APIs.
Dashbaord.jsx
import { Box, Stack } from '#mui/material';
import { useAxios } from '../../api/use-axios';
import { NewWidget } from '../../components/widget/NewWidget';
import ApiConfig from '../../api/api-config';
const Dashboard = () => {
const { response: studentResponse } = useAxios({
url: ApiConfig.STUDENTS.base,
});
const { response: courseResponse } = useAxios({
url: ApiConfig.COURSES.base,
});
const { response: feesResponse } = useAxios({
url: ApiConfig.FEES.total,
});
return (
<Box padding={2} width="100%">
<Stack direction={'row'} justifyContent="space-between" gap={2} mb={10}>
<NewWidget type={'student'} counter={studentResponse?.data?.length} />
<NewWidget type={'course'} counter={courseResponse?.data?.length} />
<NewWidget type={'earning'} counter={feesResponse?.data} />
<NewWidget type={'teacher'} counter={studentResponse?.data?.length} />
</Stack>
</Box>
);
};
export default Dashboard;
It uses a custom hook useAxios to make API calls.
use-axios.jsx
import { useState, useEffect } from 'react';
import axios from 'axios';
axios.defaults.baseURL = 'http://localhost:3000';
export const useAxios = (axiosParams) => {
const [response, setResponse] = useState(undefined);
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
const fetchData = async (params) => {
try {
const result = await axios.request({
...params,
method: params.method || 'GET',
baseURL: 'http://localhost:3000',
headers: {
accept: 'application/json',
},
});
setResponse(result.data);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData(axiosParams);
}, [axiosParams]); // execute once only
return { response, error, loading };
};
api-config.js
export default {
COURSES: {
base: '/courses',
},
FEES: {
base: '/fees',
total: '/fees/total',
},
STUDENTS: {
base: '/students',
},
};
But somehow, It keeps rendering and also all the responses form APIs, it logs to undefiend.
I tried removing dependency axiosPamras from useEffect in useAxios, It stops making multiple requests but still it shows dependency warning and also response is still undefined.
Update:
undefined error is fixed, I wasn't passing authorization token. :(
But still when axiosParams added to dependency it keeps calling apis in loop
This is happening because of the way you're calling useAxios. You're passing an object literal each time, eg
const { response: studentResponse } = useAxios({
url: ApiConfig.STUDENTS.base,
});
Because you're calling with an object, equality of this is determined by reference - and passing an object literal is a new reference on each render, even though it's "the same" object as far as you're concerned. So the useEffect with axiosParams as its dependency will rerun each time, hence the repeated sending of requests.
The easiest solution in this case is probably to extract these objects to constants which are stored outside the component - they come from an ApiConfig object so it seems unlikely this will change while the application is running. And doing this will mean the reference will always be the same and thus not trigger your useEffect to rerun.
That is, put this outside the component:
const STUDENT_AXIOS_CONFIG = { url: ApiConfig.STUDENTS.base };
and the same for the other 2 sets of axios Params. Then inside the component do:
const { response: studentResponse } = useAxios(STUDENT_AXIOS_CONFIG);
and of course do the same for the other 2 calls.

useEffect keeps rendering

In a react app, when creating a component, I use useEffect to handle a HTTP request via a custom hook (which fetch via a useCallback). Then, to parse the parameters for the request, I have a layer for services which return the expected values.
As a result, this workflow keeps re-rendering in a loop and the apps gets stacked.
Component:
import React, { Fragment, useContext, useEffect, useState } from 'react';
import { NavLink } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { getProject } from '../../../services/Project.service';
import { AuthContext } from '../../../shared/context/auth.context';
import { NavOptions } from '../../../shared/constants/NavOptions';
import { useHttpClient } from '../../../shared/hooks/http.hook';
import SideNavigation from '../../../shared/components/Navigation/SideNavigation/SideNavigation';
import NavLinks from '../../../shared/components/Navigation/NavLinks/NavLinks';
import './Dashboard.css';
const Dashboard = (props) => {
console.log('Dashboard...');
const { isLoading, error, sendRequest, clearError } = useHttpClient();
const [project, setProject] = useState();
const auth = useContext(AuthContext);
const projectId = useParams().projectId;
const getProject = async () => {
console.log('getProject...');
console.log('auth', auth.token);
const response = await sendRequest(getProject(projectId, auth.token));
if (response.status === 201) {
const responseData = await response.json();
console.log('project:', responseData);
setProject(responseData);
} else {
console.log('getting buildings failed!');
const error = await response.json();
}
};
useEffect(() => {
projectId && getProject();
}, []);
const { t, i18n } = useTranslation();
let content = (
<div className="bim-y-dashboard">
.
.
.
</div>
);
return (
<Fragment>
<SideNavigation>
<NavLinks options={NavOptions.PROJECT} projectId />
</SideNavigation>
<MainContent>{content}</MainContent>
</Fragment>
);
};
export default Dashboard;
Custom hook:
export const useHttpClient = () => {
const auth = useContext(AuthContext);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
const activeHttpRequests = useRef([]);
const sendRequest = useCallback(
async (url, method = 'GET', body = null, headers = {}) => {
console.log('sendRequest...');
console.log('url', url);
console.log('method', method);
console.log('body', body);
console.log('headers', headers);
},[]);
const clearError = () => {
setError(null);
};
useEffect(() => {
return () => {
activeHttpRequests.current.forEach(abortCtrl => abortCtrl.abort());
};
}, []);
return { isLoading, error, sendRequest, clearError };
};
Service:
export const getProject = (projectId, token) => {
console.log('getProject...');
return (`/projects/id/${projectId}`, 'GET', null, {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + token,
});
}
What am I missing to avoid this constant re-rendering?
Thanks in advance.
I have the feeling your issue is in code you have not shared, as the above looks legit.
Things you can try:
Comment out const auth = useContext(AuthContext); in your custom hook to make sure the culprit is not in your context
Make sure const response = await sendRequest(..) returns what you are expecting
Add a new hook, something like const [data, setData] = useState(null), then after you get a response, set it in your hook setData(response)
Finally, in your useEffect, update your condition as projectId && !data && getProject();
That will ensure your re rendering issue is neither on your auth context nor in your fetch request.
Hope that helps; if not, please update the question with the full code of your component and I'll take a second look :)
RESOLVED:
Found this post: Link
It explains that because of sendRequest method, it keeps re-rendering.

React Typescript how to stop axios/fetch calls from making multiple calls at one time

I am trying to connect my site with the rapidAPI using axios/fetch. I have multiple components I need to make, so I need to keep my call numbers low. That being said, I am a little new to React calls and both my axios and fetch calls are making the same API call multiple times, sucking up my API calls in no time (500 in a few minutes of trying to fix, lol). I'm not sure how to change my code up to work with async/await, and could use some help if that is the best solution. I've tried to just use cancelTokens, but this doesn't do the trick either. Below is my code using cancelTokens with a timeout. I know this is NOT a good and efficient way to remedy this problem, and need help to fix what I feel is an easy fix that just hasn't clicked in my head yet. Thank you so much in advance! here is my Stock.tsx component, which in the end grabs the stock ticker price:
import React from "react";
import "../styles/Stock.css";
import axios from "axios";
import loader from "../graphics/loading.gif";
const { useState } = React;
function Stock() {
const [price, setPrice] = useState("0");
let options: any;
const cancelToken = axios.CancelToken;
const source = cancelToken.source();
options = {
cancelToken: source.token,
method: "GET",
url: "https://yh-finance.p.rapidapi.com/stock/v2/get-summary",
params: { symbol: "AAPL", region: "US" },
headers: {
"x-rapidapi-host": "yh-finance.p.rapidapi.com",
"x-rapidapi-key": "nonono:)",
},
};
axios
.request(options)
.then(function (response) {
console.log(response.data);
setPrice(response.data.price.regularMarketPrice.fmt.toString());
})
.catch(function (error) {
console.error("Error getting price: ", error);
});
//i know, this is bad
setTimeout(() => {
source.cancel("TIME");
}, 2000);
return (
<>
{price === "0" ? (
<div>
<img src={loader} alt="loading" className={"loader"} />
</div>
) : (
<div>AAPL: {price}</div>
)}
</>
);
}
export default Stock;
If your network request is supposed to be made just once when the component is mounted, then this is the use case for useEffect with an empty dependency array:
If you want to run an effect [...] only once (on mount [...]), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.
import React, { useEffect } from "react";
// Within the body of your React functional component:
useEffect(() => {
axios.request(options) // etc.
}, []); // 2nd argument is the dependency array, here empty
The key is the useEffect method.
useEffect is a function which it is executed when one of the variables have changed.
useEffect(() => {
axios...
}, []);
If the second param is an empty list (variables list), the method will be executed only once.
One improvement would be to use useEffect without the second parameter, and check if you have already the data. If not, you will be calling every time is rendering only if you don't have the data.
useEffect(() => {
if (!loading && !data) {
setLoading(true);
axios....then(() => {
....
setLoading(false);
}).catch(function (error) {
....
setLoading(false);
});
}
});
I hope I've helped you
Try this solution with refs
import React,{useRef,useEffect} from "react";
import "../styles/Stock.css";
import axios from "axios";
import loader from "../graphics/loading.gif";
const { useState } = React;
function Stock() {
const stockRef = useRef(false);
const [price, setPrice] = useState("0");
let options: any;
const cancelToken = axios.CancelToken;
const source = cancelToken.source();
options = {
cancelToken: source.token,
method: "GET",
url: "https://yh-finance.p.rapidapi.com/stock/v2/get-summary",
params: { symbol: "AAPL", region: "US" },
headers: {
"x-rapidapi-host": "yh-finance.p.rapidapi.com",
"x-rapidapi-key": "nonono:)",
},
};
const fetchData = () => {
if (stockRef && stockRef.current) return;
stockRef.current = true;
axios
.request(options)
.then(function (response) {
console.log(response.data);
setPrice(response.data.price.regularMarketPrice.fmt.toString());
})
.catch(function (error) {
console.error("Error getting price: ", error);
});
stockRef.current = false;
}
}
//i know, this is bad
setTimeout(() => {
source.cancel("TIME");
}, 2000);
useEffect(() => {
fetchData();
},[])
return (
<>
{price === "0" ? (
<div>
<img src={loader} alt="loading" className={"loader"} />
</div>
) : (
<div>AAPL: {price}</div>
)}
</>
);
}
export default Stock;

How to use a custom React hook to make a POST or DELETE request with Axios

I am trying to make a generic useAxios hook in React. I would like to be able to import this hook into other components to make Get, Post, and Delete requests. I have created the hook and it works fine for making Get requests, but I am stuck on how to make it work for Post/Delete requests.
The issue is that I would be making the Post/Delete request when a user clicks a Save or Delete button, but I cannot call a React hook from an event handler function or from useEffect.
Below is the generic hook I created:
import { useState, useEffect } from "react";
import axios from "axios";
export interface AxiosConfig<D> {
method?: 'get' | 'post' | 'delete' | 'put';
url: string;
data?: D;
params?: URLSearchParams;
}
export const useAxios = <T, D = undefined >(config: AxiosConfig<D>) => {
const [responseData, setResponseData] = useState<T>();
const [isLoading, setIsloading] = useState(true);
const [isError, setIsError] = useState(false);
useEffect(() => {
const controller = new AbortController();
const axiosRequest = async () => {
try {
const response = await axios({ ...config, signal: controller.signal })
setResponseData(response.data)
setIsloading(false);
} catch (error) {
setIsError(true);
setIsloading(false);
}
}
axiosRequest();
return () => {
controller.abort();
}
}, [config.url, config.method, config.data, config.params])
return {responseData, isLoading, isError}
}
And this is an example of a component where I would like to make a Delete request
import { useParams } from 'react-router';
import { useAxios } from '../../api/hooks/useAxios';
export interface IItem {
title: string;
info: string;
}
export default function Item() {
const { id } = useParams<{id?: string}>();
const {responseData: item, isLoading, isError} = useAxios<IItem>({
method: 'get',
url: `http://localhost:3000/items/${id}`
})
const handleDelete = () => {
//not sure what to do here. Need to make DELETE request
}
return (
<div>
{isLoading && <p className='loading'>Loading...</p>}
{isError && <p className='error'>Could Not Load Item</p>}
{item && (
<>
<h2>{item.title}</h2>
<p>{item.info}</p>
<button onClick={handleDelete}>Delete</button>
</>
)}
</div>
)
}
I could just make the axios request directly in the Item component and not use my useAxios hook, but then I would end up repeating code throughout the application.
Assuming your DELETE route is the same as the GET route, you'd just store the method type in a local state variable and change it:
const { id } = useParams<{id?: string}>();
const [method, setMethod] = useState('get');
const {responseData: item, isLoading, isError} = useAxios<IItem>({
method,
url: `http://localhost:3000/items/${id}`
});
const handleDelete = () => setMethod('delete');
However, I think you will realize that this only solves part of the problem, which is that you have tightly coupled your component's return JSX with the response type of the GET request (IItem).

How to properly read Axios Authorization token in NextJS for request that needs authentication?

Well, I have a component that requires a token in order to fetch data from DB. The user usually gets the token after a successfull login:
const loginAuth = (loginData, history) => async () => {
const res = await api.post(`/auth/login`, loginData);
if (res.statusText === `OK`) {
if (res.data.data) {
history.push(`/auth/validatetwofactorauth/${res.data.data._id}`);
return res.data;
}
setAuthToken(res.data.token);
history.push('/posts');
} else {
console.log('error');
// setError(res.data.error);
}
};
As you guys can see I'm calling a method with the name of sethAuthToken, said function looks like this:
export const setAuthToken = (token) => {
if (token) {
if (typeof window !== 'undefined') {
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
api.defaults.headers['Authorization'] = `Bearer ${token}`;
localStorage.setItem('xAuthToken', token);
}
} else {
delete api.defaults.headers.common['Authorization'];
delete api.defaults.headers['Authorization'];
localStorage.removeItem('xAuthToken');
deleteCookie('xAuthToken', '/');
}
};
After the token is received, the function above passes it to the api.default.headers.common; this is my api:
import axios from 'axios';
import { API_URL } from '../config';
const api = axios.create({
baseURL: `${API_URL}/api/v1`,
headers: {
'Content-Type': `application/json`
}
});
console.log('API', api.defaults);
// So far everything works great since I can actually see the token being
// retrieved in the previous console log()
api.interceptors.response.use(
(res) => res,
(err) => {
return Promise.reject(err);
}
);
export default api;
So far, everything works great!.
Now, the problem comes on this following request and component. This component fetches data from a request that do requires the a Bearer token(which by now should be available)
import { useEffect, useState, useContext } from 'react';
import { withRouter } from 'next/router';
// ACTIONS
import { getTimelineFromServer } from '#/actions/post';
// HELPERS
import NothingFoundAlert from '#/layout/NothingFoundAlert';
import privateRoutes from '#/routing/privateRoutes';
import AuthContext from '#/routing/authContext';
// REACSTRAP
import CardColumns from 'react-bootstrap/CardColumns';
// NESTED COMPONENTS
import Single from './single';
export const getServerSideProps = async (context) => {
const params = `?page=${context.query.page}&limit=${context.query.limit}&sort=${context.query.sort}&status=published`;
const data = (await getTimelineFromServer(params)()) || []; // Get videos
if (!data) {
return { notFound: true };
}
return {
props: {
params: params,
serverPosts: data.data,
}
};
};
const Timeline = ({
params,
serverPosts,
router
}) => {
const [posts, setPosts] = useState([]);
useEffect(() => {
setPosts(serverPosts);
}, [params]);
const { auth } = useContext(AuthContext);
return posts?.length > 0 ? (
<CardColumns>
{posts.map((post, index) => (
<Single
key={post._id}
post={post}
objects={posts}
auth={auth}
/>
))}
</CardColumns>
) : (
<NothingFoundAlert />
)
};
export default withRouter(privateRoutes(Timeline));
The function that requires it, is the getTimelineFromserver(params)() || []; this is what it looks like:
export const getTimelineFromServer = (params) => async (dispatch) => {
console.log(api.defaults.headers.common);
try {
const res = await api.get(`/posts/timeline${params}`);
console.log('Get timeline', api.defaults.headers.common);
return res.data;
} catch (err) {
return { msg: err?.response?.statusText, status: err?.response?.status };
}
};
Now, I'm assuming the error is in the api.defaults.headers.common because when I console.log-it, it only returns { Accept: 'application/json, text/plain, */*' }.
In summary: the token gets deleted for some reason?
Thank you and I know this post is long but I would not be asking if I had not tried anything before; now, I'm out of ideas.
NOTE: I have a privateRoutes HOC that I use with the component above but I do not think that has anything to do with the fetching since the only thing it does is verify if X token is found, otherwise, redirect user to /aut/login

Categories