Joelsome
Joelsome

Reputation: 21

Deferred Data Error Handling with React Router Dom v6

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

Answers (1)

Jayson Florez
Jayson Florez

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

Related Questions