This page documents patterns and strategies for testing data loading (loaders), data mutations (actions), form submissions, and error boundaries in React Router applications. For information about the underlying test infrastructure and fixtures, see Integration Testing Framework. For testing server-side rendering and hydration, see Testing SSR and Hydration.
React Router's integration tests verify the complete data flow from server loaders/actions through client components. Tests cover both document requests (initial page loads) and client-side transitions (SPA navigation). The testing approach validates:
useLoaderData()Sources: integration/form-test.ts1-52 integration/action-test.ts1-42 integration/loader-test.ts1-62
Sources: integration/helpers/create-fixture.ts87-273 integration/form-test.ts54-503 integration/action-test.ts30-122
Loaders execute on the server for initial document requests. Test using fixture.requestDocument():
Pattern: The response HTML contains the rendered output with loader data embedded. Use text matching or HTML parsing to verify loader data appears in the document.
Sources: integration/action-test.ts140-144 integration/loader-test.ts17-52
For client-side navigations, loaders execute via single fetch requests. Test using PlaywrightFixture:
Pattern: Navigate using clickLink() or goto(), then wait for loader data to appear in the DOM using Playwright selectors.
Sources: integration/transition-test.ts223-237 integration/loader-test.ts116-121
Single fetch consolidates multiple loader calls into one request. Test the response structure:
Pattern: The response contains a keyed object where each key is a route ID and the value is { data: <loader return value> }.
Sources: integration/loader-test.ts54-60 integration/helpers/create-fixture.ts230-244
Sources: integration/action-test.ts1-232 integration/form-test.ts1-1435
Test actions with document POST requests:
Pattern: Use fixture.postDocument() with URLSearchParams or FormData to simulate form submissions. The response contains the rendered page with action data.
Sources: integration/action-test.ts146-156 integration/form-test.ts269-274
Test action submissions via browser interactions:
Pattern: Navigate to the page, trigger form submission, wait for action data to appear via useActionData().
Sources: integration/action-test.ts158-166 integration/form-test.ts522-528
Actions that don't exist return 405 errors:
Sources: integration/action-test.ts168-184 integration/resource-routes-test.ts315-335
Forms resolve actions relative to the closest route. Test resolution patterns:
| Route Type | Form Location | No Action | Action="." | Action=".." |
|---|---|---|---|---|
Static (/inbox) | Same route | /inbox | /inbox | / |
Dynamic (/blog/:id) | Same route | /blog/:id | /blog/:id | /blog |
Index (/blog) | Index route | /blog?index | /blog?index | / |
| Layout | Layout route | /blog | /blog | / |
Pattern: Forms without an action prop submit to their own route. The ?index param distinguishes index routes from parent layout routes.
Sources: integration/form-test.ts588-657 integration/form-test.ts659-727 integration/form-test.ts729-851
Verify the rendered action attribute:
Pattern: Use getElement() to extract form elements and verify their action attribute matches the expected resolution.
Sources: integration/form-test.ts590-598 integration/helpers/playwright-fixture.ts164-174
Forms preserve search params in the action URL:
Pattern: Current search params are included in the form action unless the action is . (current route without params).
Sources: integration/form-test.ts600-608 integration/form-test.ts630-636
Buttons with name and value attributes include their data in submissions:
Pattern: Clicking a submit button includes its name/value in the form data. Test both mouse clicks and keyboard submissions.
Sources: integration/form-test.ts539-558 integration/form-test.ts358-379
Buttons outside forms can submit via the form attribute:
Pattern: The form attribute connects external buttons to forms. Test that these submissions work correctly.
Sources: integration/form-test.ts568-573 integration/form-test.ts358-379
Sources: integration/error-boundary-test.ts1-595 integration/catch-boundary-test.ts1-381
Test errors thrown from loaders:
Pattern: Errors thrown in loaders render the nearest ErrorBoundary. Test both document requests and client transitions.
Sources: integration/error-boundary-test.ts159-169 integration/error-boundary-test.ts367-379
Test errors thrown from actions:
Sources: integration/error-boundary-test.ts124-141 integration/error-boundary-test.ts313-328
Errors bubble to parent boundaries when routes lack their own:
Sources: integration/error-boundary-test.ts171-178 integration/error-boundary-test.ts381-385
Thrown responses (like 401, 404) also trigger error boundaries:
Pattern: Use useRouteError() in error boundaries to access thrown responses. Test status codes and error data.
Sources: integration/catch-boundary-test.ts97-149 integration/catch-boundary-test.ts319-323
Sources: integration/fetcher-test.ts1-518 integration/fetcher-layout-test.ts1-201
Test background data loading with fetcher.load():
Pattern: fetcher.load() calls a route's loader without navigation. Verify data appears in fetcher.data.
Sources: integration/fetcher-test.ts271-276 integration/fetcher-test.ts53-89
Test background mutations with fetcher.submit():
Sources: integration/fetcher-test.ts278-283 integration/fetcher-test.ts25-89
Fetchers preserve data across reloads:
Pattern: Track state transitions by capturing fetcher.state and fetcher.data changes. Old data persists during reloads.
Sources: integration/fetcher-test.ts355-380 integration/fetcher-test.ts174-221
Fetchers handle ?index param for index route disambiguation:
Pattern: Use action: "/parent?index" to target index routes specifically vs parent layout routes.
Sources: integration/fetcher-test.ts328-353 integration/fetcher-test.ts91-148
Test redirects returned from loaders:
Sources: integration/transition-test.ts266-283 integration/loader-test.ts88-121
Test redirects from form actions:
Sources: integration/redirects-test.ts200-229 integration/action-test.ts200-229
redirectDocument() forces a full page reload instead of client-side navigation:
Sources: integration/redirects-test.ts236-250 integration/redirects-test.ts91-124
Test selective revalidation with shouldRevalidate:
Sources: integration/revalidate-test.ts1-270 integration/revalidate-test.ts48-84
Test manual revalidation:
Pattern: revalidator.revalidate() calls all loaders for current matches, respecting shouldRevalidate.
Sources: integration/revalidate-test.ts162-170 integration/revalidate-test.ts87-151
Actions trigger automatic revalidation of loaders:
Pattern: After actions complete, React Router revalidates loaders based on their shouldRevalidate functions.
Sources: integration/revalidate-test.ts233-258 integration/revalidate-test.ts87-151
| Test Type | Server Method | Client Method | Key Verification |
|---|---|---|---|
| Loader document | fixture.requestDocument() | app.goto() | HTML contains loader data |
| Loader transition | N/A | app.clickLink() | Wait for selector with data |
| Action document | fixture.postDocument() | N/A | HTML contains action data |
| Action transition | N/A | app.clickSubmitButton() | Wait for action data in DOM |
| Form resolution | N/A | app.getHtml() + getElement() | Verify action attribute |
| Error boundary | requestDocument() with error | Navigate to error route | Wait for boundary element |
| Fetcher load | N/A | app.clickElement() | Wait for fetcher.data |
| Fetcher submit | N/A | app.clickElement() | Wait for fetcher.data |
| Redirect | Check status + Location header | page.waitForURL() | Verify final URL |
| Revalidation | N/A | Navigate + check data updates | Compare data before/after |
Sources: integration/form-test.ts509-1435 integration/action-test.ts1-232 integration/fetcher-test.ts1-518 integration/revalidate-test.ts1-270
Refresh this wiki