React Hooks : Conditionally call another custom hook - reactjs

I have a an async hook, which gets the user useGetUser
function useGetUser() {
const [user, setUser] = useState(false);
useEffect(() => {
async function getUser() {
const session = await Auth.currentSession();
setUser(session);
}
if (!user) {
getUser();
}
}, [user]);
return user;
}
From another hook, I'm calling this hook, and only when I get the user, I want to execute the query:
function useGraphQLQuery() {
const user = useGetUser();
useEffect(() => {
if (user) {
useQuery(`blablabla`, async () =>
request(endpoint, query, undefined, {
authorization: user.getAccessToken().getJwtToken() || '',
})
);
}
}, [user]);
}
This code doesn't work because useQuery needs to be outside of useEffect and also because of the condition, but I need to wait for the user to be fetched...
Thank you.

I think you should have a slightly different approach.
For example , encapsulate all your routes into a component , let's call it App , in App.js. In App.js you want to use the useEffect hook to check if the user is authentificated or not, something like this:
React.useEffect(() => {
//We refresh the access token every time the page is changed
fetch('http://localhost:1000/refresh_token', {
method: 'POST',
credentials: 'include',
}).then(async x => {
const { accessToken } = await x.json()
// we set the access token
setAccessToken(accessToken)
setLoading(false)
})
}, [])
Then , from the App component, render all your components , just like in index.js.
Now you can easily use the useQuery hook in each of your components corresponding to a page .

The useGetUser hook is a bit strange since it uses a state variable which also is being set by the effect. Since it is really easy to get unwanted effects or even infinite render loops, I would prevent that.
Since it only needs to run once on startup, you can remove the !user part and also the user dependency. I can imagine you would like this hook to be updated when the Auth service receives a session.
function useGetUser() {
const [user, setUser] = useState(false);
useEffect(() => {
async function getUser() {
const session = await Auth.currentSession();
setUser(session);
}
getUser();
}, []);
return user;
}
For the useQuery hook, you can use the enabled option to prevent the useQuery from executing when set to false. You can also remove the useEffect since it's already a hook which responds to option changes.
function useGraphQLQuery() {
const user = useGetUser();
useQuery(`blablabla`, async () => request(endpoint, query, undefined, {
authorization: user.getAccessToken().getJwtToken() || '',
}), { enabled: !!user });
}

Related

Asynchronous function makes request without data when page reloads

So the problem here is I have this asynchronous function that makes request with string variable, but when page reloads it makes same request without this string despite the fact that variable itself is not empty. As a result I am receiving an error 'Bad Request' because text was not provided. Would someone be so kindly to explain me how thing works here so i could fix it so that those requests after page reloading were sent with data ?!
const { text, setText } = useContext(TextContext);
const [isLoading, setLoading] = useState(true);
const [entitiesData, setEntitiesData] = useState([]);
const call_razor = async (text_encoded) => {
try {
console.log(text_encoded) //here it shows data even when described error occurs after
const response = await axios.post('https://api.textrazor.com/',
"extractors=entities&text="+text_encoded,
{
headers: {
'x-textrazor-key': API_KEY,
'Content-Type': 'application/x-www-form-urlencoded',
}
});
setEntitiesData(response.data.response.entities)
setLoading(false)
} catch (err) {
console.log(err)
console.log(err.response.data.error)
console.log(err.response)
setLoading(false)
}
}
const dataFetch = async () => {
let textEncoded = encodeURIComponent(text)
await call_razor(textEncoded).then(() => splitIntoSentences())
}
useEffect(() => {
if (text) {
localStorage.setItem('text', text)
} else {
setText(localStorage.getItem('text'))
}
dataFetch();
}, [isLoading]);
The problem you're encountering is likely due to the fact that the useEffect hook is running before the text value is set from the TextContext context object.
One way to fix this issue is to move the useEffect hook to the parent component that is providing the TextContext context object, and pass the text value as a prop to the child component that is making the API request. This way, the text value will be available to the child component before the useEffect hook is run.
Another way would be to add a check for the text variable being empty or not in dataFetch() function, If it's empty, you can set isLoading to false so that it doesn't trigger the useEffect callback function.
const dataFetch = async () => {
if(text){
let textEncoded = encodeURIComponent(text)
await call_razor(textEncoded).then(() => splitIntoSentences())
}else {
setLoading(false)
}
}
You can also move the dataFetch() function call inside the useEffect callback after the text value is set.
useEffect(() => {
if (text) {
localStorage.setItem('text', text)
dataFetch();
} else {
setText(localStorage.getItem('text'))
}
}, [isLoading]);

Custom useAxios hook in react

I am using axios with react, so I thought to write a custom hook for this which I did and it is working fine like below
const useAxios = () => {
const [response, setResponse] = useState([]);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true); //different!
const [controller, setController] = useState();
const axiosFetch = async (configObj) => {
const { axiosInstance, method, url, requestConfig = {} } = configObj;
try {
const ctrl = new AbortController();
setController(ctrl);
const res = await axiosInstance[method.toLowerCase()](url, {
...requestConfig,
});
setResponse(res.data);
} catch (err) {
console.log(err.message);
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
console.log(controller);
// useEffect cleanup function
return () => controller && controller.abort();
}, [controller]);
return [response, error, loading, axiosFetch];
};
I have also created one axiosInstance to pass BASE_URL and headers.
Now calling useAxios to fetch data from api like below
const [data, error, loading, axiosFetch] = useAxios();
const getData = () => {
axiosFetch({
axiosInstance: axios,
method: "GET",
url: "/url",
});
};
useEffect(() => {
getData();
}, []);
My Question is
When I need to call one api I am doing above.
But what if I have to call three or four APIs in a single page.
Shall I replicate the code like this const [data1, error1, loading1, axiosFetch]=useAxios();
Or is there any other way to minimize the code.
Edit / Update
I ran above code to get data from /url, what if I want to hit different route to get one more data from server for other work, the base url remains the same
So if the second route is /users
const [data, error, loading, axiosFetch] = useAxios();
const getUsers = () => {
axiosFetch({
axiosInstance: axios,
method: "GET",
url: "/users",
});
};
useEffect(() => {
getUsers();
}, [on_BTN_Click]);
THe above codeI want to run in same file, one to get data and one to get users, how should I write my axios, as I think this const [data, error, loading, axiosFetch] = useAxios(); should gets called only once, Don't know how to do this or what is the correct way, shall I need to change my useAxios hook?
What you could do is pass the endpoint to the hook or properly call the axiosFetch callback with the different endpoints. But I have another opinion about what you are trying to do and here are my 5 cents on why this "axios hook" might not be a good idea.
A good rule of thumb on React Hooks is to use a custom hook if you need to encapsulate component logic that uses React Hooks.
Another important thing that is described in the React Hooks docs is:
Custom Hooks are a mechanism to reuse stateful logic (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated.
So, eventually, if 2 different components call the fetch for the same endpoint, they both are going to execute the call to the Backend. How to prevent that? Well, you could use a lib such as React Query, that creates some kind of "cache" for you (and a bunch of other nice features!)
And last but not least: API calls are much more related to a Service/Module than a React Hook (isolate component logic), as a concept. I hardly advise you to create a service for making API calls and using that service inside your hook instead of coupling that logic to your hook and having to handle all kinds of issues such as Caching and multiple instances of the same hook or even multiple instances of this hook calling multiple different endpoints that eventually could or could not be dependant of themselves.
How about a generic useAsync hook that accepts any asynchronous call? This decouples axios specifics from the the hook.
function useAsync(func, deps = []) {
const [state, setState] = useState({ loading: true, error: null, data: null })
useEffect(
() => {
let mounted = true
func()
.then(data => mounted && setState({ loading: false, error: null, data }))
.catch(error => mounted && setState({ loading: false, error, data: null }))
return () => { mounted = false }
},
deps,
)
return state
}
Here's a basic example of its usage -
function UserProfile({ userId }) {
const user = useAsync(
() => axios.get(`/users/${userId}`), // async call
[userId], // dependencies
})
if (user.loading)
return <Loading />
if (user.error)
return <Error message={user.error.message} />
return <User user={user.data} />
}
The idea is any asynchronous operation can be performed. A more sophisticated example might look like this -
function UserProfile({ userId }) {
const profile = useAsync(
async () => {
const user = await axios.get(`/users/${userId}`)
const friends = await axios.get(`/users/${userId}/friends`)
const notifications = await axios.get(`/users/${userId}/notifications`)
return {user, friends, notifications}
},
[userId],
)
if (profile.loading) return <Loading />
if (profile.error) return <Error message={profile.error.message} />
return <>
<User user={profile.data.user} />
<Friends friends={profile.data.friends} />
<Notifications notifications={profile.data.notifications} />
</>
}
In the last example, all fetches need to complete before the data can begin rendering. You could use the useAsync hook multiple times to get parallel processing. Don't forget you have to check for loading and error before you can safely access data -
function UserProfile({ userId }) {
const user = useAsync(() => axios.get(`/users/${userId}`), [userId])
const friends = useAsync(() => axios.get(`/users/${userId}/friends`), [userId])
const notifications = useAsync(() => axios.get(`/users/${userId}/notifications`), [userId])
return <>
{ user.loading
? <Loading />
: user.error
? <Error message={user.error.message }
: <User user={user.data} />
}
{ friends.loading
? <Loading />
: friends.error
? <Error message={friends.error.message} />
: <Friends friends={friends.data} />
}
{ notifications.loading
? <Loading />
: notifications.error
? <Error message={notifications.error.message} />
: <Notifications notifications={notifications.data} />
}
</>
}
I would recommend you decouple axios from your components as well. You can do this by writing your own API module and even providing a useAPI hook. See this Q&A if that sounds interesting to you.

Refactoring useEffect to only require one database call

At the moment, I have a component which completes some backend calls to decide when to start displaying the UI.
It's structured like this:
useEffect(() => {
getData()
})
const getData = async () => {
await verifyUser()
await fetchData()
}
The purpose here, is that verifyUser() is supposed to run first, and in the response to verifyUser(), a user id is provided by the backend.
const verifyUser = async () => {
if (!localStorage.getItem('auth')) {
return
}
if (localStorage.getItem('auth')) {
await axios.post("/api/checkAuth", {
token: JSON.parse(localStorage.getItem('auth'))
})
.then((response) => {
return setUserId(response.data.user_id)
})
.catch((err) => {
console.log(err)
localStorage.removeItem('auth')
})
}
}
As a result of this, the fetchData() function is supposed to wait until the verifyUser() function has stopped resolving, so it can use the user id in the database query.
However, at the moment it...
Calls once, without the user id
Then calls again, with the user id (and therefore resolves successfully)
Here's the function for reference:
const fetchData = async () => {
console.log("Fetch data called.")
console.log(userId)
await axios.post("/api/fetch/fetchDetails", {
user_id: userId
})
.then((response) => {
// Sets user details in here...
return response
})
.then(() => {
return setFetching(false)
})
.catch((err) => {
console.log(err)
})
}
What I'm trying to achieve here is to essentially remove any concurrency and just run the functions sequentially. I'm not 100% sure what the best practice here would be, so some feedback would be appreciated!
Your useEffect is missing a dependency array argument:
useEffect(() => {
getData()
})
should be:
useEffect(() => {
getData()
}, [])
Without that argument, useEffect will run once each time your component renders. With that argument, it will only run once, when the component is first mounted (ie. after the first render).
If you needed it to depend on another variable (eg. user.id isn't defined on load, but is later on) you could put that variable in the dependency array, ie.
useEffect(() => {
if (!user.id) return;
getData()
}, [user.id])
This version would run once when the component is mounted, then again if the user.id changes (eg. if it goes from null to an actual number).
In React, the useEffect hook accepts two arguments - the first one is a function (this is the "effect"), and the second one is a dependency array. The simplest useEffect hook looks like this:
useEffect(() => {
}, [])
The above hook has no dependency (because the array is empty), and runs only when the component initially mounts, and then goes silent.
If you don't pass in a dependency array as the second argument, as #machineghost said, the hook will run the "effect" function every time your component re-renders.
Now to your specific problem. You want to run fetchData after verifyUser has resolved its Promise, so you'd add the outcome of verifyUser as a dependency to a separate useEffect hook that calls fetchData. In this case, the outcome is setting userId.
So instead of this:
useEffect(() => {
getData()
})
const getData = async () => {
await verifyUser()
await fetchData()
}
Do this:
useEffect(() => {
verifyUser();
}, []);
useEffect(() => {
if (userId) { // assuming userId has a false-y value before verifyUser resolved
await fetchData();
}
}, [userId])

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

How to stop useEffect from making so many requests? Empty Dependencies don't work

I have a component that updates a piece of state but I'm having issues with it
I have the state declared
const [data, setData] = useState([]);
Then in my useEffect I am
useEffect(() => {
const fetchData = async () => {
await axios
.get(
API_URL,
{
headers: {
'Content-Type': 'application/json',
'X-API-KEY': API_KEY
},
params:{
"titleId": id
}
}
)
.then((response) => {
setData(response.data.Item);
})
.catch((err) => {
console.error("API call error:", err.message);
});
}
fetchData();
}, [data, id])
If I declare "data" in my dependencies, I get an endless loop of requests which is obviously no good. But if I leave 'data' out from the dependencies it shows nothing, though I am successfully retrieving it in my network's tab and even when I {JSON.styringify(data)} in a div tag aI get the json content too. So the info is in the DOM, but it's not updating the components
How can I do this so I can make an initial request to load the data and not thousands of them?
I've tried the following:
a setTimeout on the callback function
the isCancelled way with a return (() => { callbackFunction.cancel(); })
And there is an Abort way of doing this too but I can't figure it out. Every example I've seen is for class components
Sorry for the vague code. I can't replicate this without lots of coding and an API. Thanks in advance
You want to set the state and then check if is different. I use a custom hook for this which uses the useRef hook:
export function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
const prevData = usePrevious(data);
I don't know what your data looks like, but build a conditional from it. Inside of your useEffect you'll need something like:
if (data !== prevData) fetchData()
or
if (data.id !== prevData.id) fetchData()
You'll then add prevData to you dependencies:
[data, prevData, id]
So useEffects works with dependency.
With dependency - on changing dependency value useEffect will trigger
useEffect(() => {
// code
}, [dependency])
With empty brackets - will trigger on initial of component
useEffect(() => {
// code
}, [])
Without dependency and Brackets - will trigger on every state change
useEffect(() => {
// code
})
Do something like this, if that can help. I also used async/await so you can check that.
const App = () => {
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(API_URL, {
headers: {
'Content-Type': 'application/json',
'X-API-KEY': API_KEY,
},
params: {
titleId: id,
},
});
setData(response.data.Item);
} catch (err) {
console.error('API call error:', err.message);
}
};
fetchData();
}, [id]);
if (!data.length) return null;
return <p>Yes, I have data</p>;
};
obviously you will get an infinit loop !
you are updating the data inside your useEffect which means each time the data changes, triggers useEffect again and so on !
what you should do is change your dependencies depending on your case for example :
const [data, setData] = useState([])
const [fetchAgain, setFetchAgain] = useState(false)
useEffect(()=> {
fetchData();
}, [])
useEffect(() => {
if(fetchAgain) {
setFetchAgain(false)
fetchData();
}
}, [fetchAgain])
now each time you want to fetch data again you need to update the fetchAgain to true

Resources