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.
Related
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.
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;
I am following along in a React course on Udemy. In this module, we have a simple task app to demonstrate custom hooks. I've come across a situation where the "task" state is being managed in the App.js file, the "useHttp" custom hook has a function "fetchTasks" which accepts "transformTasks" as a parameter when called inside App.js. The issue I am having is that "tranformTasks" manipulates the "tasks" state inside App.js, but it is actually being called and executed inside the "useHttp" custom hook. Would really love some help understanding the mechanism for how this works. How can the state be manipulated while called from another file without the state being passed in? The code does work as intended. Here's the github link to the full app, and below are the two relevant files: https://github.com/yanichik/react-course/tree/main/full-course/custom-hooks-v2
Here is the App.js file:
import React, { useEffect, useMemo, useState } from "react";
import Tasks from "./components/Tasks/Tasks";
import NewTask from "./components/NewTask/NewTask";
import useHttp from "./custom-hooks/useHttp";
function App() {
// manage tasks state here at top level
const [tasks, setTasks] = useState([]);
const myUrl = useMemo(() => {
return {
url: "https://react-http-104c4-default-rtdb.firebaseio.com/tasks.json",
};
}, []);
const { isLoading, error, sendRequest: fetchTasks } = useHttp();
useEffect(() => {
// func transforms loaded data to add id (firebase-generated), push to loadedTasks, then
// push to tasks state
const transformTasks = (taskObj) => {
let loadedTasks = [];
for (const taskKey in taskObj) {
loadedTasks.push({ id: taskKey, text: taskObj[taskKey].text });
}
setTasks(loadedTasks);
};
fetchTasks(myUrl, transformTasks);
// if you add fetchTasks as a dependency this will trigger a re-render each time states
// are set inside sendRequest (ie fetchTasks) and with each render the custom hook (useHttp)
// will be recalled to continue the cycle. to avoid this, wrap sendRequest with useCallback
}, [fetchTasks, myUrl]);
const addTaskHandler = (task) => {
setTasks((prevTasks) => prevTasks.concat(task));
};
return (
<React.Fragment>
<NewTask onEnterTask={addTaskHandler} />
<Tasks
items={tasks}
loading={isLoading}
error={error}
onFetch={fetchTasks}
/>
</React.Fragment>
);
}
export default App;
And here is the "useHttp" custom hook:
import { useState, useCallback } from "react";
// NOTE that useCallback CANNOT be used on the top level function
function useHttp() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const sendRequest = useCallback(async (httpConfig, applyFunction) => {
setIsLoading(true);
setError(false);
try {
const response = await fetch(httpConfig.url, {
method: httpConfig.method ? httpConfig.method : "GET",
headers: httpConfig.headers ? httpConfig.headers : {},
body: httpConfig.body ? JSON.stringify(httpConfig.body) : null,
});
// console.log("response: " + response.method);
if (!response.ok) {
throw new Error("Request failed!");
}
const data = await response.json();
applyFunction(data);
// console.log("the formatted task is:" + applyFunction(data));
} catch (err) {
setError(err.message || "Something went wrong!");
}
setIsLoading(false);
}, []);
return { sendRequest, isLoading, error };
}
export default useHttp;
Sounds like you're learning from a decent course. The hook is using a technique called "composition". It knows you'll want to do some processing on the data once it has been fetched and let's you pass in (the applyFunction variable) your own snippet of code to do that processing.
Your snippet of code is just a function, but all parties agree on what parameters the function takes. (This is where using typescript helps catch errors.)
So you pass in a function that you write, and your function takes 1 parameter, which you expect will be the data that's downloaded.
The useHttp hook remembers your function and once it has downloaded the data, it calls your function passing in the data.
If you've used some of your own variables within the function you pass to the hook, they get frozen in time ... sort-of. This can of worms is a topic called 'closures' and I'm sure it will come up in the course if it hasn't already.
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).
I am pretty new to React Context API. What I am trying to do is set a loader when I perform an API call and stop the loader after an API call is done. But if I do these dispatch actions from helper function, I am getting an 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
fix this problem.
// ApiCalls.js
export const LoginService = (username, password) => {
//to show loader when api call starts
const [dispatch] = useContext(LoaderContext);
dispatch({
type: "SHOWLOADER",
payload: true
});
}
// Hello.js
export default function Hello(props) {
useEffect(() => {
LoginService();
}, []);
return(
<h2>Hello</h2>
)
}
Reproducible example.
You have two mistakes:
useContext(LoaderContext) returns a tuple (with your implementation), so you need the setter function:
// Not [dispatch]
const [,dispatch] = useContext(LoaderContext);
LoginService is actually a custom hook, and shouldn't be called inside other hook (See Rules of hooks).
import { LoaderContext } from './LoaderContext';
import {useContext, useEffect} from 'react';
// Use username,password
export const useLoginService = (username, password) => {
const [, dispatch] = useContext(LoaderContext);
useEffect(() => {
dispatch({
type: "SHOWLOADER",
payload: true,
});
}, []);
};
// Usage
export default function Hello(props) {
useLoginService();
return(
<h2>Hello</h2>
)
}
See fixed example.
You need to change your component this way:
export default function Hello(props) {
const [state, dispatch] = useContext(LoaderContext);
useEffect(() => {
dispatch({
type: "SHOWLOADER",
payload: true
});
}, []);
return(
<h2>Hello</h2>
)
}