useEffect not triggering in ContextProvider - reactjs

I am using useEffect hook inside an Context Provider called DataProvider. In the useEffect hook I am sending a request to a endpoint using axios. But the thing is, the useEffect hook not triggering. What Should I do in this situation. Can anyone help me on this issue.
Context Provider code:
export const DataProvider = ({ children }) => {
const [data, setData] = useState({});
useEffect(() => {
const options = {
method: "GET",
url: "https://corona-virus-world-and-india-data.p.rapidapi.com/api_india",
headers: {
"x-rapidapi-key": API_KEY,
"x-rapidapi-host": API_HOST
}
};
axios
.request(options)
.then(function(response) {
console.log(response.data);
setData(response.data);
})
.catch(function(error) {
console.error(error);
});
}, []);
if(data) {
return (
<DataContext.Provider value={[data, setData]}>
{children}
</DataContext.Provider>
);
} else {
console.log("error");
}
};
And below is the error image
Please help me to fix this error 🙏.

React renders first and only after first render it executes the useEffect. You're trying to access a property that is not yet initialized.
Just handle your data and render conditionally with if or do this in your card Container:
<Cardcontainer caseNumber={numberWithCommas(data?.total_values?.deaths)}/>
Make sure numberWithCommas knows how to handle an undefined parameter.

Related

Axios throwing CanceledError with Abort controller in react

I have built an axios private instance with interceptors to manage auth request.
The system has a custom axios instance:
const BASE_URL = 'http://localhost:8000';
export const axiosPrivate = axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
});
A custom useRefreshToken hook returns accessToken using the refresh token:
const useRefreshToken = () => {
const { setAuth } = useAuth();
const refresh = async () => {
const response = await refreshTokens();
// console.log('response', response);
const { user, roles, accessToken } = response.data;
setAuth({ user, roles, accessToken });
// return accessToken for use in axiosClient
return accessToken;
};
return refresh;
};
export default useRefreshToken;
Axios interceptors are attached to this axios instance in useAxiosPrivate.js file to attached accessToken to request and refresh the accessToken using a refresh token if expired.
const useAxiosPrivate = () => {
const { auth } = useAuth();
const refresh = useRefreshToken();
useEffect(() => {
const requestIntercept = axiosPrivate.interceptors.request.use(
(config) => {
// attach the access token to the request if missing
if (!config.headers['Authorization']) {
config.headers['Authorization'] = `Bearer ${auth?.accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
const responseIntercept = axiosPrivate.interceptors.response.use(
(response) => response,
async (error) => {
const prevRequest = error?.config;
// sent = custom property, after 1st request - sent = true, so no looping requests
if (error?.response?.status === 403 && !prevRequest?.sent) {
prevRequest.sent = true;
const newAccessToken = await refresh();
prevRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
return axiosPrivate(prevRequest);
}
return Promise.reject(error);
}
);
// remove the interceptor when the component unmounts
return () => {
axiosPrivate.interceptors.response.eject(responseIntercept);
axiosPrivate.interceptors.request.eject(requestIntercept);
};
}, [auth, refresh]);
return axiosPrivate;
};
export default useAxiosPrivate;
Now, this private axios instance is called in functional component - PanelLayout which is used to wrap around the pages and provide layout.
Here, I've tried to use AbortControllers in axios to terminate the request after the component is mounted.
function PanelLayout({ children, title }) {
const [user, setUser] = useState(null);
const axiosPrivate = useAxiosPrivate();
const router = useRouter();
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const signal = controller.signal;
const getUserProfile = async () => {
try {
const response = await axiosPrivate.get('/api/identity/profile', {
signal,
});
console.log(response.data);
isMounted && setUser(response.data.user);
} catch (error) {
console.log(error);
router.push({
pathname: '/seller/auth/login',
query: { from: router.pathname },
});
}
};
getUserProfile();
return () => {
isMounted = false;
controller.abort();
};
}, []);
console.log('page rendered');
return (
<div className='flex items-start'>
<Sidebar className='h-screen w-[10rem]' />
<section className='min-h-screen flex flex-col'>
<PanelHeader title={title} classname='left-[10rem] h-[3.5rem]' />
<main className='mt-[3.5rem] flex-1'>{children}</main>
</section>
</div>
);
}
export default PanelLayout;
However, the above code is throwing the following error:
CanceledError {message: 'canceled', name: 'CanceledError', code: 'ERR_CANCELED'}
code: "ERR_CANCELED"
message: "canceled"
name: "CanceledError"
[[Prototype]]: AxiosError
constructor: ƒ CanceledError(message)
__CANCEL__: true
[[Prototype]]: Error
Please suggest how to avoid the above error and get axios to work properly.
I also encountered the same issue and I thought that there was some flaw in my logic which caused the component to be mounted twice. After doing some digging I found that react apparently added this feature with with the new version 18 in StrictMode where useEffect was being run twice. Here's a link to the article clearly explaining this new behaviour.
One way you could solve this problem is by removing StrictMode from your application (Temporary Solution)
Another way is by using useRef hook to store some piece of state which is updated when your application is mounted the second time.
// CODE BEFORE USE EFFECT
const effectRun = useRef(false);
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const signal = controller.signal;
const getUserProfile = async () => {
try {
const response = await axiosPrivate.get('/api/identity/profile', {
signal,
});
console.log(response.data);
isMounted && setUser(response.data.user);
} catch (error) {
console.log(error);
router.push({
pathname: '/seller/auth/login',
query: { from: router.pathname },
});
}
};
// Check if useEffect has run the first time
if (effectRun.current) {
getUserProfile();
}
return () => {
isMounted = false;
controller.abort();
effectRun.current = true; // update the value of effectRun to true
};
}, []);
// CODE AFTER USE EFFECT
Found the solution from this YouTube video.
I, too, encountered this issue. What made it worse is that axios doesn't provide an HTTP status code when the request has been canceled, although you do get error.code === "ERR_CANCELED". I solved it by handling the abort within the axios interceptor:
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (error.code === "ERR_CANCELED") {
// aborted in useEffect cleanup
return Promise.resolve({status: 499})
}
return Promise.reject((error.response && error.response.data) || 'Error')
}
);
As you can see, I ensure that the error response in the case of an abort supplies a status code of 499.
I faced the same problem in similar project, lets start by understanding first the root cause of that problem.
in react 18 the try to make us convenient to the idea of mounting and unmounting components twice for future features that the are preparing, the the useEffect hook now is mounted first time then unmounted the mounted finally.
so they need from us adapt our projects to the idea of mount and unmount of components twice
so you have two ways, adapting these changes and try to adapt your code to accept mounting twice, or making some turn around code to overcome mounting twice, and I would prefer the first one.
here in your code after first mount you aborted your API request in clean up function, so when the component dismount and remount again it face an error when try to run previously aborted request, so it throw exception, that's what happens
1st solution (adapting to react changing):
return () => {
isMounted = false
isMounted && controller.abort()
}
so in above code we will abort controller once only when isMounted is true, and thats will solve your problem
2nd solution (turn around to react changing):
by using useRef hook and asign it to a variable and update its boolean value after excuting the whole code only one time.
const runOnce = useRef(true)
useEffect(()=>{
if(runOnce.current){
//requesting from API
return()=>{
runOnce.current = false
}
}
},[])
3rd solution (turn around to react changing):
remove React.StrictMode from index.js file

Access React context in an API service

In my React application I use the context API to store the user information through the useContext hook:
const AuthContext = createContext<AuthContextType>(null!);
const useAuth = () => useContext(AuthContext);
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User>();
// Implementations of values
const value = useMemo(() => ({ user, login, logout }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export { AuthProvider, useAuth };
Accessing the auth information works all fine and dandy in the components:
export default function CoolComponent() {
const auth = useAuth();
if (auth.user) {
// Do something
}
return <div>Hello {auth.user}</div>;
}
The thing is that my jwt-token is stored in the user object and I need it for my API calls in my service, but hooks are not allowed outside functional components. Can I circumvent this in a clever way? Some things that I can think of is to pass the token on every call to the service (not very DRY) or save the token in localStorage and then retrieve it from there in the service, but it seems unnecessary to store the same information in two different places?
Update:
Now with the service code:
const baseUrl = environment.apiUrl;
function getToken() {
// This is what I would like to get some help with
}
const headers = {
...(getToken() && { Authorization: `Bearer ${getToken()}` }),
"Content-Type": "application/json",
};
function getAllProjects(): Promise<IProject[]> {
return fetch(`${baseUrl}projects`, {
headers,
}).then((response) => response.json());
}
function createProject(project: CreateProjectDTO): Promise<IProject> {
return fetch(`${baseUrl}projects`, {
method: "POST",
headers,
body: JSON.stringify(project),
}).then((response) => response.json());
}
// + many more
export { getAllProjects, createProject };
Calling the service in a component:
useEffect(() => {
const fetchProjects = async () => {
setIsLoading(true);
try {
const allProjects = await getAllProjects();
setProjects(allProjects);
} catch (error) {
// Handle error
} finally {
setIsLoading(false);
}
};
fetchProjects();
}, []);
The React documentation says that you cannot call hooks inside JavaScript functions.
What can you do?
Use custom hooks. rename functions as useCreateProject and return your function. Then you will be able to call useAuth inside your custom hook:
const useCreateProject =() =>{
const {user} = useAuth();
function createProject(project: CreateProjectDTO): Promise<IProject> {
return fetch(`${baseUrl}projects`, {
method: "POST",
headers,
body: JSON.stringify(project),
}).then((response) => response.json());
}
return createProject
}
Then call it like this:
const createProject = useCreateProject()
useEffect(() => {
const create = async () => {
setIsLoading(true);
try {
await createProject()
} catch (error) {
// Handle error
} finally {
setIsLoading(false);
}
};
create();
}, []);
But my advice is to store the token on localStorage or in cookies. Context data will be lost when user refreshes page. However, if that is not case for you, you can continue using context.

React : is it bad to use a hook in a normal function?

So i have this react functional component :
import { createContext, useContext, useState } from 'react';
const GeneralContext = createContext();
export function GeneralContextWrapper({ children }) {
const [userSessionExpired, setUserSessionExpired] = useState(false);
return (
<GeneralContext.Provider
value={{
userSessionExpired,
setUserSessionExpired,
}}
>
{children}
</GeneralContext.Provider>
);
}
export function useGeneralContext() {
return useContext(GeneralContext);
}
I have to change the state of userSessionExpired in a normal function so i do :
const checkToken = (data) => {
const { userSessionExpired, setUserSessionExpired } = useGeneralContext();
if (data.status === 'fail' && data.message === 'Sessione scaduta') {
window.location.href = '/auth/login?session=expired';
localStorage.removeItem('jwt');
}
return data;
};
And i got eslint warning :
React Hook "useGeneralContext" is called in function "checkToken" that
is neither a React function component nor a custom React Hook
function. React component names must start with an uppercase letter.
where i call the checktoken function example in a fetch call :
export const call = async (url, token, data) => {
const dataReturned = await fetch(url, {
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(data),
})
.then((response) => {
return response.json();
})
.catch((err) => {
return { status: 'fail', message: 'API CALL ERROR', error: err.message };
});
return checkToken(dataReturned);
};
I would like to know if i can ignore the warning or not?
It's unclear how your checkToken is ever called, but you surely shouldn't be using any hooks from within an async function. In order for hooks to work properly, they have to be called in the same order every time the component renders, and if a call is originating from within an async function, it's almost guaranteed that's not going to happen.
The warning you're seeing is good advice, and you should heed it. Call your hooks within your component (or within other hooks that you create), and then if you have a function that needs the result of those hooks (in your case, a context), then pass that into the function.

Reloading the page loses API data using useContext

I am using a RapidAPI Api to load crypto currency data in my project. The data is loading and even rendering in my React components but as soon as I refresh, I have to load the data from the beginning to get to specific coin data. On reload, I get TypeError: Cannot read properties of undefined (reading 'name')
Here is my code:
import React, { useState, useEffect } from "react";
import "./Homepage.css";
import CryptoCard from "../Card/Card";
import axios from "axios";
const Homepage = () => {
const [coinData, setCoinData] = useState([]);
useEffect(() => {
const i = 5;
const options = {
method: "GET",
url: "https://coinranking1.p.rapidapi.com/exchanges",
headers: {
"x-rapidapi-host": "coinranking1.p.rapidapi.com",
"x-rapidapi-key": "REDACTED",
},
};
axios
.request(options)
.then((response) => {
setCoinData(response.data.data.exchanges);
// console.log(coinData);
// console.log("Working!!");
})
.catch((error) => {
console.error(error);
});
}, []);
return (
<div className="homepage">
<div className="heading">
<h1>Discover {coinData[0].name}</h1>
<hr className="line" />
</div>
<div className="cards-container">
<CryptoCard />
</div>
</div>
);
};
export default Homepage;
Why am I getting
Reason for your error message
coinData[0] does not exist when rendering the component initially. You've defined it as useState([]), so every time the component gets created, you start with a fresh empty array. Therefore, you should add a check, if you got some data in it.
<h1>Discover {coinData.length > 0 && coinData[0].name}</h1>
Reason for refetch
Your useEffect will be executed once when the component gets rendered. You make the request and put the data in the coinData state. But the state is not persistent. You could use the local storage to cache your request across page refresh. To do this, you need to persist the data when your request finishes and load the data when you create your state.
const [coinData, setCoinData] = useState([], () => {
const localData = localStorage.getItem('coinData');
return localData ? JSON.parse(localData) : [];
});
useEffect(() => {
const i = 5;
const options = {
method: "GET",
url: "https://coinranking1.p.rapidapi.com/exchanges",
headers: {
"x-rapidapi-host": "coinranking1.p.rapidapi.com",
"x-rapidapi-key": "REDACTED",
},
};
axios
.request(options)
.then((response) => {
setCoinData(response.data.data.exchanges);
// console.log(coinData);
// console.log("Working!!");
// persist in localStorage
localStorage.setItem("coinData", JSON.stringify(response.data.data.exchanges))
})
.catch((error) => {
console.error(error);
});
}, []);
EDIT: This will still make a request every time you hit refresh, but I guess this code will make it clear how it works. So I guess you'll be able to add an if-condition, if you got some data already and skip the new request ;-)

How to use fetch function and show api response on component ? Custom fetch response is not passing to component

After Fetch /Vehicles from api I wanted to show them in component but inside useFetch function I can console.log res.data but inside the Trucks component I could not map trucks.
My Custom useFetch function:
import { useState, useEffect } from 'react';
import axios from 'axios';
const useFetch = (url) => {
const api = axios.create({
baseURL: 'api url',
headers: { Authorization: `Bearer ${localStorage.token}` },
});
const [data, setData] = useState(null);
const [isPending, setIsPending] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortCont = new AbortController();
setTimeout(() => {
api
.get(url)
.then((res) => {
console.log(res.data);
if (!res.ok) {
throw Error('Could not with fetch data');
}
})
.then((data) => {
data.json();
console.log(data);
setIsPending(false);
setError(null);
})
.catch((err) => {
if (err.name === 'AbortError') {
console.log('fetch aborted');
} else {
setIsPending(false);
setError(err.message);
}
});
}, 1000);
return () => abortCont.abort();
}, [url]);
return { data, isPending, error };
};
export default useFetch;
I used this custom useFetch function and could get data (inside the function) its not usable to render
Some possible reasons I thought ;
useFetch function may not return data properly
I may need to use stringify or json function for API respond
Trucks Component:
const Trucks = () => {
const { data: trucks, error, isPending } = useFetch('/vehicles');//
console.log('they are ' + { trucks }); //console output ->[object,Object]
return (Some Jsx)};
export default Trucks;
Thanks
There are two issues with your Hook itself that I can see.
The first thing is that you never call setData. This means that although you are fetching the data inside your hook and consoling it out, you never set the data object to hold the retrieved data.
The second thing is that the second then isn't actually doing anything with the data you are retrieving. You can handle this all inside the first statement as such
.then((res) => {
setData(res.data);
setIsPending(false);
if (!res.ok) {
setIsPending(false);
throw Error("Could not with fetch data");
}
})
Once you do this you should be able to call your hook inside the related component, and render it using some JSX. Just make sure to add a check for isPending and display some sort of loading screen before actually rendering your data.
Another note, when you use axios, you do not need to call data.json(). That is something that is required for the fetch API but axios just requires you to call res.data when resolving the promise

Resources