Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Router v6: Loader functions using Context API

I'm using React-Router V6 and I'm having a problem implementing loaders. I need to make a request for a page through a function that I have in my context file. However, the router is defined in main.js and I cannot access the context there.

main.js

import ReactDOM from 'react-dom/client';
import { Navigate, RouterProvider, createBrowserRouter } from 'react-router-dom';

import App from './App.jsx';
import PetShopProvider from './context/PetShopContext.jsx';
import './index.css';
import { ErrorPage, PetInfoPage, PetsPage } from './pages/index.jsx';
import PetInfoErrorPage from './pages/ErrorPage/PetInfoErrorPage.js';

const router = createBrowserRouter([
  {
    path: '/',
    element: <App />,
    errorElement: <ErrorPage />,
    children: [
      {
        path: '/',
        element: <Navigate to="/pets" />,
      },
      {
        path: '/pets',
        element: <PetsPage />,
      },
      {
        path: 'pets/:id',
        element: <PetInfoPage />,
        errorElement: <PetInfoErrorPage />,
        // loader: 
      },
    ],
  },
]);

ReactDOM.createRoot(document.getElementById('root') as Element).render(
  // <React.StrictMode>
  <PetShopProvider>
    <RouterProvider router={router} />
  </PetShopProvider>,
  // </React.StrictMode>
);

context file

import ...

export const PetShopContext = React.createContext<DataContextType | null>(null);

const PetShopProvider = ({ children }: ContextProps) => {
  const [data, setData] = React.useState<DataType[]>([]);
  const firestoreRef = collection(firestore, 'pets_data');

  const getData = async (sortBy?: string) => {
    // REVIEW:
    let sortData: any;
    switch (sortBy) {
      case 'Z-A':
        sortData = await getDocs(query(firestoreRef, orderBy('name', 'desc')));
        break;
      case 'Tipo':
        sortData = await getDocs(query(firestoreRef, orderBy('type', 'asc'), orderBy('name', 'asc')));
        break;
      case 'Porte':
        sortData = await getDocs(
          query(firestoreRef, orderBy('type', 'asc'), orderBy('size', 'asc'), orderBy('name', 'asc')),
        );
        break;

      default:
        sortData = await getDocs(query(firestoreRef, orderBy('name', 'asc')));
        break;
    }
    const finalData = sortData.docs.map((doc: { data: () => DataType; id: string }) => ({
      ...doc.data(),
      id: doc.id,
    }));
    setData(finalData);
  };

  //TODO: Try Catch !
  const getPet = async (petID: string) => {
    const docRef = doc(firestore, 'pets_data', petID);
    const docSnap = await getDoc(docRef);
    
    if (docSnap.exists()) {
      return { ...docSnap.data(), id: docSnap.id };
    } else {
      throw Error;
    }
  };

  return (
    <PetShopContext.Provider
      value={{
        data,
        setData,
        getData,
      }}
    >
      {children}
    </PetShopContext.Provider>
  );
};

export default PetShopProvider;

PetInfo Page

import ...

const PetInfoPage = () => {
  const navigate = useNavigate();
  const { id } = useParams<PetInfoParams>();
  const { getPet, deleteService } = React.useContext(PetShopContext) as DataContextType;
  const [currentPet, setCurrentPet] = React.useState<DataType | undefined>(undefined);
  const [currentPetService, setCurrentPetService] = React.useState<ServiceDataType | undefined>(undefined);
  const [formModal, setFormModal] = React.useState<boolean>(false);
  const [alertModal, setAlertModal] = React.useState<boolean>(false);

  useEffect(() => {
    setTimeout(() => {
      const fetchPetData = async () => {
        // REVIEW:
        const petData: any = await getPet(id!);
        console.log(petData);
        setCurrentPet(petData);
      };
      fetchPetData();
    }, 500);
  }, []);

...
};

export default PetInfoPage;

How can I use the getPet (PetInfo page) function as a loader? I need to implement errorElement according to the loader result.

like image 897
Yuri da Paz Simonin Avatar asked May 01 '26 15:05

Yuri da Paz Simonin


1 Answers

Create a root component that can access the provided PetShopContext context and pass the context to any route loaders.

Example:

const AppRoot = () => {
  const petShopContext = React.useContext(PetShopContext) as DataContextType;

  // Memoize to provide stable router reference
  const router = React.useMemo(() => createBrowserRouter([
    {
      path: '/',
      element: <App />,
      errorElement: <ErrorPage />,
      children: [
        {
          path: '/',
          element: <Navigate to="/pets" />,
        },
        {
          path: '/pets',
          element: <PetsPage />,
        },
        {
          path: 'pets/:id',
          element: <PetInfoPage />,
          errorElement: <PetInfoErrorPage />,
          loader: petInfoLoader(petShopContext), // <-- pass context
        },
      ],
    },
  ]), [petShopContext]);

  return <RouterProvider router={router} />;
};

ReactDOM.createRoot(document.getElementById('root') as Element).render(
  <React.StrictMode>
    <PetShopProvider>
      <AppRoot />
    </PetShopProvider>,
  </React.StrictMode>
);

petInfoLoader should be a curried function that receives the passed PetShopContext context value and returns the loader function.

Example:

const petInfoLoader = (petShopContext: DataContextType) => {
  return ({ params, request }) => {
    const { id } = params as PetInfoParams;
    const { getPet } = petShopContext;

    return getPet(id!);
  };
};

A variation of this might be to use a React ref to hold a reference to the context value so the ref can also be provided as a stable reference.

const AppRoot = () => {
  const petShopContext = React.useContext(PetShopContext) as DataContextType;
  const petShopContextRef = React.useRef(petShopContext);

  React.useEffect(() => {
    petShopContextRef.current = petShopContext;
  }, [petShopContext]);

  // Memoize to provide stable router reference
  const router = React.useMemo(() => createBrowserRouter([
    {
      path: '/',
      element: <App />,
      errorElement: <ErrorPage />,
      children: [
        {
          path: '/',
          element: <Navigate to="/pets" />,
        },
        {
          path: '/pets',
          element: <PetsPage />,
        },
        {
          path: 'pets/:id',
          element: <PetInfoPage />,
          errorElement: <PetInfoErrorPage />,
          loader: petInfoLoader(petShopContextRef), // <-- pass ref
        },
      ],
    },
  ]), []);

  return <RouterProvider router={router} />;
};
const petInfoLoader = (petShopContextRef: React.RefObject<DataContextType>) => {
  return ({ params, request }) => {
    const { id } = params as PetInfoParams;
    const { getPet } = petShopContextRef.current;

    return getPet(id!);
  };
};
like image 195
Drew Reese Avatar answered May 03 '26 04:05

Drew Reese