How to navigate based on state using react router - reactjs

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

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

unmount state update in useEffect() cleanup function

To get data from API I call a http request. Sometimes I warned with a error that tell me I am trying to update state which unmounted. To solve that, I use clean up function in useEffect() hook like this:
const [products, setProducts] = useState([]);
useEffect(() => {
const source = axios.CancelToken.source();
const token = source.token;
const fetchProducts = async () => {
try {
const response = await ProductService.getProducts(token);
setProducts(response.data);
} catch (error) {
console.log(error.message, error.response.status);
}
};
fetchProducts();
return () => {
source.cancel();
};
}, []);
and my service file like this:
const ProductService = {
getProducts: async function (token) {
try {
const response = await axios.get(myURL, {
cancelToken: token
});
return response.data
} catch (error) {
throw error
}
}
};
Have I done anything wrong or unnecessary thing in this case or can I update this code block ??
Please help me.
I see, so you're in the twilight zone between the asynchronous request succeeding, so the cancel token won't work, and the enqueued state update.
From here you've a couple options.
Ignore since this is only a warning. You've already tried cancelling in-flight network requests, or unsubscribed from subscriptions, etc... so at this point it's only a warning.
Use the old isMounted hack.
Using the isMounted hack uses a mutable reference in the useEffect hook that will always be synchronously updated when unmounting, and can be a final check before enqueueing the state update.
useEffect(() => {
const source = axios.CancelToken.source();
const token = source.token;
let isMounted = true;
const fetchProducts = async () => {
try {
const response = await ProductService.getProducts(token);
isMounted && setProducts(response.data);
} catch (error) {
console.log(error.message, error.response.status);
}
};
fetchProducts();
return () => {
isMounted = false;
source.cancel();
};
}, []);
I call this a hack as it's really only a way to skirt/stifle the warning. React state updates to unmounted components are ignored anyway.

Why whenever I type something that does not exist, i got this error?

Whenever I type something that does not exist in the json I got this error:
TypeError: countries.map is not a function
The search functionality works fine until I type in a result that doesn't exist.
const mainUrl = `https://restcountries.eu/rest/v2/`
const all = `${'all'}`
const serachUrl = `${'name/'}`
const Home = () => {
// usesstate to conutries
const [countries, setCountries] = useState([])
// usesstate to query
const [query, setQuery] = useState('')
{
/* // fetch countries */
}
const fetchCountries = async () => {
let url
if (query) {
url = `${mainUrl}${serachUrl}${query}`
} else {
url = `${mainUrl}${all}`
}
try {
const response = await fetch(url)
const data = await response.json()
setCountries(data)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
fetchCountries()
}, [query])
Issue
When you search for something that doesn't exist the API is returning an error object, a 404.
{
"status": 404,
"message": "Not Found"
}
This is stored in countries state and you then attempt to map it, OFC throwing the error.
Solution
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 fetch API returns a resolved Promise even for 400 responses. You should check that the request was successful.
const fetchCountries = async () => {
let url;
if (query) {
url = `${mainUrl}${serachUrl}${query}`;
} else {
url = `${mainUrl}${all}`;
}
try {
const response = await fetch(url);
if (!response.ok) { // <-- check OK response
throw new Error("Network response was not ok");
}
const data = await response.json();
setCountries(data);
} catch (error) {
console.log(error);
}
};

useFetch Can't perform a React state update on an unmounted component warning

I have a custom hook that I'm using to make API requests on my react front-end application but the hook seems to be having a bug.
It makes API requests as intended but whenever I unmount the current container/page in which the request is being made, my hook doesn't know that the page has been unmounted so it doesn't cancel the request and therefore react throws the 'Can't perform a React state update on an unmounted component' warning.
export function useFetch(initialValue, url, options, key) {
const [response, setResponse] = useLocalStorage(key, initialValue);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
const isMounted = { state: true };
async function fetchData() {
setLoading(true);
try {
const res = await axios({
url: url,
baseURL: BASE_URL,
cancelToken: source.token,
...options
});
if (res.data.results) {
setResponse(res.data.results);
} else {
setResponse(res.data);
}
setLoading(false);
} catch (error) {
setError(error);
setLoading(false);
}
}
if (isMounted.state) {
fetchData();
}
return () => {
isMounted.state = false;
source.cancel('Operation canceled by the user.');
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]);
return [response, { error, loading }];
}
By now you are checking for if(isMounter.state) in wrong place. It's currently very next step after you've initialized it.
I believe it should be
const isMounted = { state: true };
async function fetchData() {
setLoading(true);
try {
const res = await axios({
url: url,
baseURL: BASE_URL,
cancelToken: source.token,
...options
});
if(!isMounted.state) return;
.....
}
}
fetchData();
BTW you don't have to use object there: isMounted = true/isMounted = false will work just fine through closure.
Actually your have 2 different approaches mixed: using flag(isMounted) and cancelling request. You may use just one. Cancelling request should work(as far as I see) but it leads your catch block is executed:
} catch (error) {
setError(error);
setLoading(false);
}
See, unmounting cancels request, but your code still tries to set up some state. Probably you better check if request has been failed or canceled with axious.isCancel:
} catch (error) {
if (!axios.isCancel(error)) {
setError(error);
setLoading(false);
}
}
And you may get rid of isMounted in this case.
I use the following hook to get an ifMounted function
const useIfMounted = () => {
const isMounted = useRef(true)
useEffect(
() => () => {
isMounted.current = false
},[]
)
const ifMounted = useCallback(
func => {
if (isMounted.current && func) {
func()
}
},[]
)
return ifMounted
}
Then in your code add const ifMounted = useIfMounted() to useFetch and before your set functions do ifMounted(() => setLoading(true), ifMounted(() => setError(error)), etc....
Here's a blog post I wrote on the subject: https://aceluby.github.io/blog/react-hooks-cant-set-state-on-an-unmounted-component

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