Issue with MERN stack when data is coming back from Node - reactjs

I am using the Fetch API to post data to my Node server. There is behaviour that I am seeing which I am unable to understand and need some help in resolving. When the user clicks a button a modal opens up with a form with a few fields. On hitting submit, the data is passed to a node server. First it creates a new record for Mongo and saves the data in the collection. After saving I want to fetch some data from a few other collections and then send the information back to my react app.
Here is what I am seeing.
If i only save the data and don't do the fetching part and send back a message like this
res.status(201).json({"message":"data received"})
Then after the modal closes in my console I see the message "Data Received".
However if I do the fetch part, which basically means a few more milli seconds of delay, then once the modal closes, this message flashes in the react console.
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.
DOMException: The user aborted a request.
Here is my submit handler in react
const submitHandler = async(event) => {
event.preventDefault()
try {
const responseData = await sendRequest('http://localhost:5000/challenge/createChallenge', 'POST',
JSON.stringify({
location: selectedOption,
format: selectedOption1,
date: selectedDate.toString(),
opponent: props.initialValues,
month: cMonth,
year: cYear,
player: auth.profile.MemberName
}),
{
'Content-Type': 'application/json',
Authorization: 'Bearer ' + auth.token
})
console.log(responseData)
} catch (err){
console.log(err)
}
}
And here is my http-hook, which is a custom hook I am using
import { useState, useCallback, useRef, useEffect } from 'react'
export const useHttpClient = () => {
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(false)
const activeHttpRequests = useRef([])
const sendRequest = useCallback(async (url, 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 (err){
setError(err.message)
setIsLoading(false)
throw err
}
}, [])
const clearError = () => {
setError(null)
}
useEffect(() => {
return () => {
activeHttpRequests.current.forEach(abortCtrl => abortCtrl.abort())
}
}, [])
return { isLoading, error, sendRequest, clearError}
}
Can some one please help. I am not sure how to go about resolving this. I checked the reasons for this error, while some of the content on the internet gave me some clue. But I am not clear enough to resolve this.

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]);

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 navigate based on state using react router

In my react project, I'm using fetch api to get the user profile from backend. If there is any error occurred in the API call I'm showing it on the screen.
const navigate = useNavigate();
const [errorMessage, setErrorMessage] = React.useState("");
...
const handleGetProfile = async () => {
await fetch(`${API_URL}/profile`).then(...).catch(err=>setErrorMessage(err.message))
!errorMessage && navigate("/");
}
I wanted to navigate to root path only if no error occurred in the api call. So I'm checking if the error is empty and navigating to the root path.
The problem with this approach is that the setErrorMessage does not guarantee immediate update because it schedules the state update, so it is always navigating to the root path even if there is an error.
How do I solve this issue, any suggestions?
Correct, because React state updates are asynchronously processed, and treated as const the errorMessage state won't have updated inside the handleGetProfile callback.
const handleGetProfile = async () => {
await fetch(`${API_URL}/profile`)
.then(...)
.catch(err => setErrorMessage(err.message));
!errorMessage && navigate("/");
}
It's also anti-pattern to mix async/await with Promise chains. Generally you use one or the other.
To resolve you should move the navigate call into the "resolved" part of the logic. Since fetch returns a Promise and only rejects on network errors you need to also check the response status.
See Checking that the fetch was successful
A fetch() promise will reject with a TypeError when a
network error is encountered or CORS is misconfigured on the
server-side, although this usually means permission issues or similar
— a 404 does not constitute a network error, for example. An accurate
check for a successful fetch() would include checking that the
promise resolved, then checking that the Response.ok property has
a value of true. The code would look something like this:
fetch('flowers.jpg')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not OK');
}
return response.blob();
})
.then(myBlob => {
myImage.src = URL.createObjectURL(myBlob);
})
.catch(error => {
console.error('There has been a problem with your fetch operation:', error);
});
Using Promise chain
const handleGetProfile = () => {
fetch(`${API_URL}/profile`)
.then((response) => {
if (!response.ok) {
throw new Error('Network response was not OK');
}
// handle any successful response stuff
navigate("/");
})
.catch(err => {
setErrorMessage(err.message || err);
});
}
Using async/await with try/catch
const handleGetProfile = async () => {
try {
const response = await fetch(`${API_URL}/profile`);
if (!response.ok) {
throw new Error('Network response was not OK');
}
// handle any successful response stuff
navigate("/");
} catch(err) {
setErrorMessage(err.message || err);
}
}
Use an useEffect hook to response to state changes
const navigate = useNavigate();
const [isFetched, setIsFetched] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState("");
useEffect(() => {
if (isFetched && !errorMessage) {
navigate("/");
}
}, [errorMessage, isFetched, navigate]);
...
const handleGetProfile = async () => {
setErrorMessage(null);
setIsFetched(false);
try {
const response = await fetch(`${API_URL}/profile`);
if (!response.ok) {
throw new Error('Network response was not OK');
}
// handle any successful response stuff
navigate("/");
} catch(err) {
setErrorMessage(err.message || err);
} finally {
setIsFetched(true);
}
}

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

React - useEffect running even when there was no change in state variable

I have an endpoint in my kotlin app that looks like this:
either.eager<String, Unit> {
val sessionAndCookieUser = commonAuth.decryptCookieGetUser(getCookie(context), ::userTransform).bind()
val user = sessionAndCookieUser.session.user
val ctx = Ctx(ds, SystemSession, conf)
val dbUser = getUserEither(ctx, user.id).bind()
val signatureAlgorithm = SignatureAlgorithm.HS256
val signingKey = SecretKeySpec(conf.get(ZendeskJWTSecret).toByteArray(), signatureAlgorithm.jcaName)
val iat = Date(System.currentTimeMillis())
val exp = Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000)
val token = Jwts.builder()
.claim("name", dbUser.name)
.claim("email", dbUser.email)
.setIssuer(conf.get(StreamAppName))
.setIssuedAt(iat)
.setExpiration(exp)
.signWith(signingKey, signatureAlgorithm)
.compact()
context.setResponseCode(StatusCode.OK)
.setResponseType("application/json")
.send(jsonObject("token" to token).toString())
}.mapLeft {
context.setResponseCode(StatusCode.UNAUTHORIZED)
}
I am setting a response where I should send a jsonObject if a user is authenticated or UNAUTHORIZED if the user is not authenticated.
When I am testing this endpoint in a browser I just get status unknown for that request - when I was debugging the backend, otherwise I get 200 with no response data.
If I test it in postman I get json as a response.
I see that token is being built and everything looks good on the backend side, but then response is not being loaded in the browser.
I am fetching it like this from react:
export const fetchGet = (uriPath: string) =>
fetch(fullUrl(uriPath), {
method: 'GET',
credentials: 'include'
})
useEffect(() => {
console.log('got here')
fetchGet('/auth/token')
.then(res => {
console.log('res ', res)
return res.json()
})
.then(res => {
console.log('res.json ', res)
return res.ok ? setJwtToken(res.token) : Promise.reject(res.statusText)
})
.catch(error => {
console.log('err ', error)
setError(error.toString())
})
}, [])
In the console I can only see 'got here' being logged, nothing else, and frontend crushed with an error:
DevTools failed to load source map: Could not load content for
data:application/json;charset=utf-8;base64, longTokenString...:
Load canceled due to reload of inspected page
What am I doing wrong here?
Updated
I found an issue here, I had 2 more useEffect functions, and they were redirecting before I had a result. I am not sure why was the useEffect function where I am passing the error state variable running when there was no change from initial state?
Here is the full code:
const [jwtToken, setJwtToken] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
fetchGet('/auth/token')
.then(async res => {
const data = await res.json()
if (!res.ok) {
const error = data?.message || res.statusText
return Promise.reject(error)
}
return data
})
.then(({token}) => setJwtToken(token))
.catch(err => {
console.log('err ', err)
setError(err.toString())
})
}, [])
useEffect(() => {
if (jwtToken) {
// window.location.href = `/mypage.com?access/jwt?jwt=${jwtToken}&return_to=`
console.log(jwtToken)
}
}, [jwtToken])
useEffect(() => {
console.log(error)
//window.location.href = '/login'
}, [error])
Update nr. 2:
const [jwtToken, setJwtToken] = useState('')
const { search } = useLocation()
useEffect(() => {
fetchGet('/auth/token')
.then(async res => {
const data = await res.json()
if (!res.ok) {
const error = data?.message || res.statusText
return Promise.reject(error)
}
return data
})
.then(({token}) => setJwtToken(token))
.catch(() => window.location.href = '/login')
}, [])
useEffect(() => {
const params = new URLSearchParams(search)
const returnTo = params.get('return_to') ? `&return_to=${params.get('return_to')}` : ''
jwtToken !== '' ? window.location.href = `${url}/jwt?jwt=${jwtToken}${returnTo}` : null
}, [jwtToken])
return <p>Authenticating ...</p>
I have removed unnecessary error useEffect function, but now I get:
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 get this warning and it is also not redirecting after the token is fetched. What am I doing wrong this time around?
Every useEffect callback will be invoked on first mount. You should include a simple if statement to ensure an error is set before running your error handling logic.
useEffect(() => {
if(error) {
console.log(error)
//window.location.href = '/login'
}
}, [error])
There is likely an issue with the CORS configuration of your API.
Access-Control-Allow-Origin response header must be set to the origin of your react app (it cannot be * for credentialed requests) and Access-Control-Allow-Credentials must be true. Failing to include them will result in an opaque response.
https://fetch.spec.whatwg.org/#cors-protocol-and-credentials
Here is my completed answer. The main problem here is using useEffect incorrectly, especially with objects in the dependency array.
Let's talk about this code first
useEffect(() => {
// TODO something with error
}, [error]);
Because error is an object and React useEffect use shallow comparison as you can see in this question. It will make the code inside that useEffect will run forever.
Next part, you get warnings because your use of redirect is not in the right way. Just remove that useEffect and it should work.
The reason why is, when we have an error, your code in your catch should run. Beside that, jwtToken will be changed at that time too. It will make your app redirected before the rendering process is completed.

Resources