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.
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!);
};
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With