Reputation: 21
I am confused by React Router Dom's deferred data error handling methods.
In my component, I'm using the component imported from the library. On the promise successfully resolving, my data is returned and I'm correctly rendering my UI.
However, when the promise rejects, I'm confused!
React Router Dom's documentation on the await component states "The error element renders instead of the children when the promise rejects. You can access the error with useAsyncError
."
(documentation here - https://reactrouter.com/en/main/components/await)
However, upon rejection my error that I get returned from useAysncError()
remains undefined.
import {
useSubmit,
Link,
json,
useRouteLoaderData,
redirect,
defer,
Await,
useAsyncError,
} from "react-router-dom";
import { lostPetInstance } from "../util/BaseAxiosInstance";
import SimpleAccordion from "../components/Accordion";
import { getToken } from "../util/authTokenGetter";
const ShowPage = () => {
const lostPetData = useRouteLoaderData("lostPetShow");
const [popupInfo, setPopupInfo] = useState(null);
const token = useRouteLoaderData("root");
const submit = useSubmit();
const theme = useTheme();
const error = useAsyncError();
console.log(error);
console.log(lostPetData);
console.log(token);
const startDeleteHandler = () => {
const proceed = window.confirm("Are you sure?");
if (proceed) {
submit(null, { method: "delete" });
}
};
return (
<>
<Suspense
fallback={
<h1 style={{ textAlign: "center", color: "red" }}>Loading...</h1>
}
>
<Await resolve={lostPetData.resolvedLostPetData} errorElement={<p>error.message</p>}> **<---- error is undefined!**
{(resolvedLostPetData) => {
console.log(resolvedLostPetData);
const lostPetImageData = resolvedLostPetData.data.lostPetImages.map(
(image, index) => {
if (index < 3) {
return {
img: image.path,
title: image.filename,
rows:
resolvedLostPetData.data.lostPetImages.length === 1
? 3
: index === 0
? 2
: 1,
cols:
resolvedLostPetData.data.lostPetImages.length === 1
? 3
: index === 0
? 2
: 1,
};
s;
}
}
);
const inputDate = resolvedLostPetData.data.dateLost
? resolvedLostPetData.data.dateLost
: new Date().toISOString();
const dateComponents = inputDate.split("T")[0].split("-");
const year = parseInt(dateComponents[0], 10);
const month = parseInt(dateComponents[1], 10) - 1;
const day = parseInt(dateComponents[2], 10);
const readableLostDate = new Date(
year,
month,
day
).toLocaleDateString("en-us", {
weekday: "long",
year: "numeric",
month: "short",
day: "numeric",
});
return (
<Paper elevation={3} sx={{ padding: 6, marginBottom: "6rem" }}>
<Stack spacing={2}>
<Box sx={{ width: "90%", margin: "0 auto" }}>
{/* <MapboxShowMap coordinates={lostPetData.lastLocation.coordinates}/> */}
<Map
mapboxAccessToken={import.meta.env.VITE_MAPBOX_TOKEN}
initialViewState={{
longitude:
resolvedLostPetData.data.lastLocation.coordinates[0],
latitude:
resolvedLostPetData.data.lastLocation.coordinates[1],
zoom: 6,
}}
style={{
width: "100%",
height: 500,
margin: "2rem 0",
border: `2px solid ${theme.palette.primary.light}`,
}}
mapStyle="mapbox://styles/mapbox/streets-v12"
>
<Marker
longitude={
resolvedLostPetData.data.lastLocation.coordinates[0]
}
latitude={
resolvedLostPetData.data.lastLocation.coordinates[1]
}
anchor="bottom"
key={resolvedLostPetData.data._id}
onClick={(e) => {
e.originalEvent.stopPropagation();
setPopupInfo(resolvedLostPetData.data);
}}
>
<img src="../../map-marker-2-24.png" />
</Marker>
{popupInfo && (
<Popup
anchor="top"
longitude={popupInfo.lastLocation.coordinates[0]}
latitude={popupInfo.lastLocation.coordinates[1]}
onClose={() => setPopupInfo(null)}
>
<Box>
<Typography variant="h4" component="h6">
{popupInfo.name}
</Typography>
{/* <Button variant="contained" sx={{marginY: 1}}>
<Link style={{textDecoration: 'none', color: 'inherit'}} to={`/lostpets/${popupInfo._id}`}>
View Pet's Page
</Link>
</Button> */}
<img
src={popupInfo.lostPetImages[0].path}
width="100%"
height="120px"
/>
</Box>
</Popup>
)}
</Map>
</Box>
<Box>
<QuiltedImageList itemData={lostPetImageData} />
</Box>
<Box>
<Box sx={{ margin: 3 }}>
<Typography
variant="h4"
component="h2"
sx={{ textAlign: "center" }}
>
{resolvedLostPetData.data.name}{" "}
<span style={{ color: "grey" }}>|</span> Age:{" "}
{resolvedLostPetData.data.age}{" "}
<span style={{ color: "grey" }}>|</span> Last Seen:{" "}
{readableLostDate}
</Typography>
</Box>
<SimpleAccordion lostPetData={resolvedLostPetData.data} />
{token &&
token.userId &&
token.userId === resolvedLostPetData.data.owner._id && (
<Box sx={{ marginBottom: 5, marginTop: 10 }}>
<Stack
direction="row"
spacing={2}
sx={{
justifyContent: "center",
alignItems: "center",
}}
>
<Button variant="contained" color="warning">
<Link
to="edit"
style={{
color: "inherit",
textDecoration: "none",
}}
>
Edit
</Link>
</Button>
<Button
variant="contained"
color="error"
onClick={startDeleteHandler}
>
Delete
</Button>
</Stack>
</Box>
)}
</Box>
</Stack>
</Paper>
);
}}
</Await>
</Suspense>
</>
);
};
The documentation further states, "If you do not provide an errorElement, the rejected value will bubble up to the nearest route-level errorElement and be accessible via the useRouteError hook."
Well, usually in my loader when not deferring data, I throw using react-router-dom's json() method to pass the parsed response to my errorPage.
export const loader = async ({ request, params }) => {
const lostPetId = params.lostPetId;
const lostPetDataPromise = lostPetInstance.get(`/lostpets/${lostPetId}`);
console.log(lostPetDataPromise);
return defer({
resolvedLostPetData: lostPetDataPromise,
});
// try {
// const response = await lostPetInstance.get(`/lostpets/${lostPetId}`);
// // return response.data;
// return defer({
// resolvedLostPetData: response,
// });
// } catch (error) {
// console.log(error.response);
// throw json(
// { message: error.response?.data?.error?.message || "Error loading lost pet data" },
// { status: error.response?.status || 500 }
// );
// }
Well, in order to catch the error, I need to wait for the asynchronous code and then I can catch the error and throw it, which is received by my ErrorPage.
I can't wait for the asynchronous code, however, because that defeats the purpose of deferring the data to begin with. This means message, and status, are undefined if I try to chain on a .catch onto the bare promise in my loader, meaning my errorPage throws an uncaught Type error.
Here's my errorPage for good measure:
import { Box, Container, Paper, Typography } from "@mui/material";
import { useRouteError } from "react-router-dom";
import ButtonAppBar from "../components/Navbar";
export default function ErrorPage() {
const error = useRouteError();
console.log(error);
return (
<>
<ButtonAppBar />
<Container sx={{ display: "flex", justifyContent: "center" }}>
<Paper elevation={3} sx={{padding: "3rem"}}>
<Typography variant="h2" component="h2" textAlign="center" marginBottom={3}>
An Error Has Occured!
</Typography>
<Typography variant="h4" component="h4">
{error.status} {error.data.message}
</Typography>
</Paper>
</Container>
</>
);
As you can see, it's depending on error to be defined.
TL;DR: How do I handle errors when deferring data? const error = useAsyncError() is always undefined even upon promise rejection. And I can't bubble up to my nearest route error boundary because I can't throw it the necessary data from the rejected response because I'm not awaiting it. Reading the documentation, the error should be available in my route level error boundary via the useRouteError hook if I omit an errorElement in my Await component, but it also remains undefined upon promise rejection.
I've tried awaiting the response, but that defeats the purpose of deferring the data.
Upvotes: 2
Views: 2390
Reputation: 19
Just a hunch but it seems you are calling useAsyncError
from the wrong place.
When you call it from <ShowPage />
no errors have occured so you get undefined
.
However, if you call useAsyncError
from the component that you defined in the errorElement
property of the Await
component, then by the time the errorElement
is called, the error would have already occurred.
I would suggest changing this line
<Await resolve={lostPetData.resolvedLostPetData} errorElement={<p>error.message</p>}>
to this:
<Await resolve={lostPetData.resolvedLostPetData} errorElement={<MyErrorComponent />}>
and then write the component:
import {useAsyncError} from "react-router-dom";
export function MyErrorComponent(){
const error = useAsyncError();
return (<p>Error: {error.message} </p>)
}
From the docs: https://reactrouter.com/en/main/components/await#errorelement
Hope it helps!
Upvotes: 1