Data Fetching Strategies in React
Learning Outcome Guide
Section titled “Learning Outcome Guide”At the end of this class, you should be able to…
- Implement asynchronous data fetching using
fetch()anduseEffect. - Manage loading and error states during data retrieval.
- Compare client-side data fetching with route-based data loading in React Router 7.
Additional Notes:
- Reinforce that
useEffect’s dependency array must be used carefully to prevent infinite requests. - React Router 7’s Data APIs (
loader,action,useLoaderData) automate data fetching per route - this will be explored in a future lesson. - Optional extension: mention how TanStack Query can further simplify asynchronous state management (to be covered later in the course).
Start the backend API
Section titled “Start the backend API”Lesson 19 continues using the local backend server.
In a second terminal:
- Move into the backend directory:
cd backend- Install dependencies (first time only):
npm install- Start the server:
npm startThe server listens on http://localhost:3000.
We will continue using:
GET /resourcesGET /resources/:idPOST /resourcesPUT /resources/:id
Keep this server running while you work on the frontend.
Lesson focus
Section titled “Lesson focus”This lesson introduces React Router 7 data mode.
We will:
- Replace component-based route configuration with a router object
- Load route data with
loaderfunctions - Read loaded data with
useLoaderData() - Submit mutations with route
actionfunctions - Use router-aware
<Form>components instead of manual submit handlers
This lesson is about moving data loading and mutations into the router.
Connecting to prior lessons
Section titled “Connecting to prior lessons”So far, the app uses:
BrowserRouterRoutesRouteuseEffectfor data fetching- manual
fetch()calls in components for create and update operations
While the current solution works, it spreads data logic across components.
React Router data mode gives us a different pattern:
- routes declare their own data requirements
- routes can handle form submissions
- components focus more on rendering
Phase 1: What is data mode?
Section titled “Phase 1: What is data mode?”In component routing, routes define which component should render.
In data mode, routes define:
- which component should render
- how data is loaded
- how mutations are handled
This means the router becomes responsible for both navigation and route-driven data work.
Phase 2: Create route loaders and actions
Section titled “Phase 2: Create route loaders and actions”Create a new file:
src/router.jsxThis file will hold:
- the router configuration
- route loaders
- route actions
We are moving toward a router object instead of JSX route declarations inside main.jsx.
Phase 3: Add shared helpers for API calls
Section titled “Phase 3: Add shared helpers for API calls”Inside src/router.jsx, start by adding a small API helper section.
const API_BASE_URL = 'http://localhost:3000';
async function fetchResources() { const res = await fetch(`${API_BASE_URL}/resources`);
if (!res.ok) { throw new Error(`Could not load resources: ${res.status}`); }
return res.json();}
async function fetchResourceById(resourceId) { const res = await fetch(`${API_BASE_URL}/resources/${resourceId}`);
if (!res.ok) { throw new Error(`Could not load resource: ${res.status}`); }
return res.json();}These helpers will be reused by loaders and actions.
Phase 4: Create loaders for the directory and admin routes
Section titled “Phase 4: Create loaders for the directory and admin routes”Still in src/router.jsx, add route loader functions.
export async function resourceDirectoryLoader() { const resources = await fetchResources(); return { resources };}
export async function adminLoader({ params }) { const resources = await fetchResources();
if (!params.resourceId) { return { resources, resourceId: null, selectedResource: null, }; }
const selectedResource = await fetchResourceById(params.resourceId);
return { resources, resourceId: params.resourceId, selectedResource, };}Important ideas:
- the directory route only needs the full list
- the admin route needs the list and, sometimes, one selected resource
paramsis available inside the loader just like route parameters are available inside components
Phase 5: Create an action for the admin form
Section titled “Phase 5: Create an action for the admin form”Next, add an action that handles both create and update.
import { redirect } from 'react-router';
export async function adminAction({ request, params }) { const formData = await request.formData();
const payload = { title: formData.get('title'), category: formData.get('category'), summary: formData.get('summary'), location: formData.get('location'), hours: formData.get('hours'), contact: formData.get('contact'), virtual: formData.get('virtual') === 'on', openNow: formData.get('openNow') === 'on', };
const isEditing = Boolean(params.resourceId); const url = isEditing ? `${API_BASE_URL}/resources/${params.resourceId}` : `${API_BASE_URL}/resources`; const method = isEditing ? 'PUT' : 'POST';
const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload), });
if (!res.ok) { throw new Error(`Could not ${isEditing ? 'update' : 'create'} resource`); }
const savedResource = await res.json();
return redirect(`/admin/${savedResource.id}`);}This action does the same work your manual submit handler used to do, but now it belongs to the route.
Phase 6: Define the router object
Section titled “Phase 6: Define the router object”Now build the router object in src/router.jsx.
import App from './App';import ResourceDirectoryPage from './pages/ResourceDirectoryPage';import AdminPage from './pages/AdminPage';import { createBrowserRouter,} from 'react-router';
export const router = createBrowserRouter([ { path: '/', element: <App />, children: [ { index: true, element: <ResourceDirectoryPage />, loader: resourceDirectoryLoader, }, { path: 'admin', element: <AdminPage />, loader: adminLoader, action: adminAction, }, { path: 'admin/:resourceId', element: <AdminPage />, loader: adminLoader, action: adminAction, }, ], },]);We now have route definitions that include:
elementloaderaction
Phase 7: Update main.jsx to use RouterProvider
Section titled “Phase 7: Update main.jsx to use RouterProvider”Open main.jsx.
Replace the current BrowserRouter + Routes setup with:
import { StrictMode } from 'react';import { createRoot } from 'react-dom/client';import { RouterProvider } from 'react-router';
import './index.css';import { router } from './router';
createRoot(document.getElementById('root')).render( <StrictMode> <RouterProvider router={router} /> </StrictMode>);This is the key shift into data mode.
Instead of rendering route JSX directly, the app now renders a configured router object.
NOTE: your application will effectively be ‘broken’ at this stage. We now must work within the pages to make use of the new loaders and action functions.
Phase 8: Refactor ResourceDirectoryPage to use useLoaderData()
Section titled “Phase 8: Refactor ResourceDirectoryPage to use useLoaderData()”Open src/pages/ResourceDirectoryPage.jsx.
Remove the useResources() hook import and replace it with:
import { useLoaderData } from 'react-router';Then read route data from the loader:
const { resources } = useLoaderData();This means the page no longer fetches resources for itself.
The route already loaded the data before rendering the page.
You can now remove component-level loading and error handling related to useResources() from this page.
Phase 9: Refactor AdminPage to use useLoaderData()
Section titled “Phase 9: Refactor AdminPage to use useLoaderData()”Open src/pages/AdminPage.jsx.
Replace useResources() usage with:
import { Form, useLoaderData, useNavigation } from 'react-router';Inside the component:
const { resources, selectedResource, resourceId } = useLoaderData();const navigation = useNavigation();const isSubmitting = navigation.state === 'submitting';Now the page reads its data from the route instead of fetching it directly.
The selected resource is already prepared by the loader.
Phase 10: Replace the manual form with router <Form>
Section titled “Phase 10: Replace the manual form with router <Form>”Because the route now has an action, the admin page can use React Router’s <Form> component.
Example:
<Form method="post" className="space-y-4"> <div className="space-y-1"> <label htmlFor="title" className="block text-sm font-medium text-gray-700"> Title </label> <input id="title" name="title" type="text" defaultValue={selectedResource?.title ?? ''} className="w-full rounded border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200" /> </div>
<button type="submit" className="rounded bg-sky-600 px-4 py-2 text-sm font-semibold text-white hover:bg-sky-700" disabled={isSubmitting} > {isSubmitting ? 'Saving...' : resourceId ? 'Update Resource' : 'Add Resource'} </button></Form>Important differences from the old pattern:
- no manual
onSubmit - no manual
fetch()call inside the component - the form submits to the route action automatically
Phase 11: Make the resource list route-driven
Section titled “Phase 11: Make the resource list route-driven”The admin page should still allow selecting a resource for editing. Add a nav link to each rendered item in the list. Add the NavLink to the imports:
import { Form, NavLink, useLoaderData, useNavigation } from 'react-router';Example list item:
<li key={resource.id}> <NavLink to={`/admin/${resource.id}`} className={({ isActive }) => `block rounded border p-3 ${isActive ? 'border-sky-500 bg-sky-50' : 'border-gray-200'}` } > <p className="font-semibold">{resource.title}</p> <p className="text-sm text-base-content/70">{resource.category}</p> </NavLink></li>Clicking a resource updates the route, the loader runs again, and the selected resource is loaded into the form.
Phase 12: Add a create-mode navigation path
Section titled “Phase 12: Add a create-mode navigation path”To return to create mode, update the reset button to be a link that goes to /admin.
Example:
<NavLink to="/admin" className="rounded border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-50"> New Resource</NavLink>This switches back to create mode by changing the route.
Phase 13: Why the admin inputs do not reset properly yet
Section titled “Phase 13: Why the admin inputs do not reset properly yet”At this point, the application may appear to work, but the admin form still has an important problem.
Because the inputs use defaultValue, they are uncontrolled inputs.
For example:
<input id="title" name="title" type="text" defaultValue={selectedResource?.title ?? ''}/>This only applies the value when the input first mounts.
That means:
- navigating from
/adminto/admin/:resourceIdmay not fully reset the visible form fields - clicking a different resource may update the route, but not fully reset typed input values
- returning to
/adminmay leave previously typed values visible
The route and loader data are updating correctly, but the input elements are keeping their own internal browser-managed state.
A cleaner solution is to move the form into a separate ResourceForm component and let that component manage controlled input state.
When the route changes, we will render ResourceForm with a key tied to resourceId so React remounts the form and resets the fields correctly.
Step 1: Create a form component
Section titled “Step 1: Create a form component”Create a new file:
src/components/ResourceForm.jsxAdd the following:
import { Form, navigate } from 'react-router';import { useState } from 'react';
export default function ResourceForm({ initialData, isEditing, isSubmitting,}) { const [formData, setFormData] = useState(initialData);
function handleChange(e) { const { name, value, type, checked } = e.target;
setFormData((prev) => ({ ...prev, [name]: type === 'checkbox' ? checked : value, })); }
function handleReset() { if (isEditing) { navigate('/admin'); } else { setFormData(initialData); } }
return ( <Form method="post" className="space-y-4"> <div className="space-y-1"> <label htmlFor="title" className="block text-sm font-medium text-gray-700"> Title </label> <input id="title" name="title" type="text" value={formData.title} onChange={handleChange} className="w-full rounded border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200" /> </div>
<div className="space-y-1"> <label htmlFor="category" className="block text-sm font-medium text-gray-700"> Category </label> <input id="category" name="category" type="text" value={formData.category} onChange={handleChange} className="w-full rounded border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200" /> </div>
<div className="space-y-1"> <label htmlFor="summary" className="block text-sm font-medium text-gray-700"> Summary </label> <textarea id="summary" name="summary" value={formData.summary} onChange={handleChange} className="w-full rounded border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200" /> </div>
<div className="space-y-1"> <label htmlFor="location" className="block text-sm font-medium text-gray-700"> Location </label> <input id="location" name="location" type="text" value={formData.location} onChange={handleChange} className="w-full rounded border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200" /> </div>
<div className="space-y-1"> <label htmlFor="hours" className="block text-sm font-medium text-gray-700"> Hours </label> <input id="hours" name="hours" type="text" value={formData.hours} onChange={handleChange} className="w-full rounded border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200" /> </div>
<div className="space-y-1"> <label htmlFor="contact" className="block text-sm font-medium text-gray-700"> Contact </label> <input id="contact" name="contact" type="text" value={formData.contact} onChange={handleChange} className="w-full rounded border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200" /> </div>
<label className="flex items-center gap-2 text-sm text-gray-700"> <input name="virtual" type="checkbox" checked={formData.virtual} onChange={handleChange} /> Virtual </label>
<label className="flex items-center gap-2 text-sm text-gray-700"> <input name="openNow" type="checkbox" checked={formData.openNow} onChange={handleChange} /> Open now </label>
<div className="flex gap-2"> <NavLink to="/admin" className="rounded border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-50" > {isEditing ? 'Clear' : 'Reset'} </NavLink>
<button type="submit" className="rounded bg-sky-600 px-4 py-2 text-sm font-semibold text-white hover:bg-sky-700" disabled={isSubmitting} > {isSubmitting ? 'Saving...' : isEditing ? 'Update Resource' : 'Add Resource'} </button> </div> </Form> );}NOTE: You will also notice that the ‘Reset’ button has been reverted to a simple button component that now imperitively navigates to the
admin/route only if a selected resource has been loaded. Otherwise, the button resets the create resource form.
Step 2: Use ResourceForm inside AdminPage
Section titled “Step 2: Use ResourceForm inside AdminPage”Open src/pages/AdminPage.jsx.
Import the component:
import ResourceForm from '../components/ResourceForm';Derive initial form data from the loader result:
const initialFormData = selectedResource ? { title: selectedResource.title, category: selectedResource.category, summary: selectedResource.summary, location: selectedResource.location, hours: selectedResource.hours, contact: selectedResource.contact, virtual: selectedResource.virtual, openNow: selectedResource.openNow, } : { title: '', category: '', summary: '', location: '', hours: '', contact: '', virtual: false, openNow: false, };Then replace the inline <Form> with:
<ResourceForm key={resourceId ?? 'new'} initialData={initialFormData} isEditing={Boolean(resourceId)} isSubmitting={isSubmitting}/>The key is important.
When the route changes between:
/admin/admin/:resourceId
React remounts ResourceForm, so its local state resets using the new initialData.
This gives us:
- route-driven form state
- correctly resetting inputs
- no need to synchronize input values manually after render
Phase 14: What changed architecturally?
Section titled “Phase 14: What changed architecturally?”Before data mode:
- routes selected components
- components fetched their own data
- components handled their own submit requests
After data mode:
- routes select components
- routes load data with loaders
- routes handle mutations with actions
- components mainly render data and forms
This is a major architecture shift.
Phase 15: Validate the finished behavior
Section titled “Phase 15: Validate the finished behavior”Test the following:
- Visiting
/loads directory data through a route loader - Visiting
/adminloads the admin page through a route loader - Visiting
/admin/:resourceIdloads the selected resource through the loader - Submitting the admin form triggers the route action
- Creating a new resource redirects to
/admin/:newId - Updating a resource keeps the app on that resource route
Key Concepts Reinforced
Section titled “Key Concepts Reinforced”- Data mode uses a router object and
RouterProvider loaderfunctions fetch route datauseLoaderData()reads the loaded route dataactionfunctions handle route-based mutations<Form>submits directly to the route action- Route data and route state can work together cleanly
- Controlled form components may still be useful in data mode when route changes need to reset UI state cleanly
Assessment
Section titled “Assessment”- The app uses
createBrowserRouter()andRouterProvider - Route data is loaded with loaders
- The admin form submits through a route action
useLoaderData()replaces component-level data fetching- The route still controls create vs edit mode
- The admin form resets correctly when switching between create mode and edit mode
Student Exercise
Section titled “Student Exercise”- Implement an route-level ErrorBoundary to handle errors fetching data.
- Add a pending message or disabled state for all admin form buttons while submitting.
- Add simple field validation in the action and return an error response for invalid submissions.
- Add a success message after saving by reading navigation or location state.
Push to your GitHub workbook repo
Section titled “Push to your GitHub workbook repo”- Stage all changes:
git add -A- Commit:
git commit -m 'Lesson 19 - React Router data mode'- Push:
git push origin main