-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Add router abstraction and TanStack Router adapter #11102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
djhi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work 💪
react-adminstill has a dependency onreact-routerso users that want TanStack Router will have both routers installed. This is probably a breaking change for some people though.- TanStack router stories conflict with each other. If you navigate from some deep link in one to another story, you may end up with an empty screen.
- We should probably split tan stack related code in multiple files, only re-export the provider from
ra-coreand make the rest available under a sub-path export (https://github.com/colinhacks/zshy/?tab=readme-ov-file#2-specify-your-entrypoints-in-packagejsonzshy)
docs/CustomRoutes.md
Outdated
| import { tanStackRouterProvider } from 'ra-core'; | ||
| const { Route } = tanStackRouterProvider; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could have a sub-export path to make this more natural: import { Route } from 'ra-core/tanstack';
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice to have, but I'm afraid of the bundler config it requires.
packages/ra-core/src/routing/adapters/tanStackRouterProvider.stories.tsx
Show resolved
Hide resolved
|
Thanks for the review, I have fixed the problems you raised.
You're right, but that's something we can't change in a minor release.
I know, but I don't know how to fix them.
Not sure this is necessary or desireable. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a routing abstraction layer to react-admin, decoupling it from react-router and enabling support for alternative routing libraries like TanStack Router. The abstraction is implemented via a RouterProvider interface similar to dataProvider and authProvider, with react-router as the default and TanStack Router as an alternative implementation.
Changes:
- Added
RouterProviderinterface with hooks (useNavigate, useLocation, useParams, etc.) and components (Link, Navigate, Routes, etc.) - Implemented
reactRouterProvider(default) andtanStackRouterProvideradapters - Updated react-admin core to use router abstraction hooks instead of direct react-router imports
- Added comprehensive tests and documentation for TanStack Router integration
Reviewed changes
Copilot reviewed 140 out of 141 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/ra-core/src/routing/RouterProvider.ts | Core interface defining the router abstraction contract |
| packages/ra-core/src/routing/adapters/reactRouterProvider.tsx | Default react-router implementation of RouterProvider |
| packages/ra-core/src/routing/adapters/tanStackRouterProvider.tsx | TanStack Router implementation with custom path matching |
| packages/ra-core/src/routing/RouterProviderContext.tsx | Context for providing router throughout the app |
| packages/ra-core/src/routing/useNavigate.ts | Router-agnostic navigation hook |
| packages/ra-core/src/routing/useLocation.ts | Router-agnostic location hook |
| packages/ra-core/src/routing/useParams.ts | Router-agnostic params hook |
| packages/ra-core/src/routing/LinkBase.tsx | Router-agnostic Link component |
| packages/ra-core/src/core/CoreAdminContext.tsx | Added routerProvider prop and context setup |
| packages/react-admin/src/TanStackRouterAdmin.stories.tsx | Demo app showcasing TanStack Router integration |
| yarn.lock | Added TanStack Router dependencies |
| docs/TanStackRouter.md | Documentation for TanStack Router usage |
Comments suppressed due to low confidence (3)
packages/ra-core/src/routing/adapters/tanStackRouterProvider.spec.tsx:1
- Test expectation is incorrect. Based on the implementation in tanStackRouterProvider.tsx lines 110-124, when matching
'/*'against'/', theparamsshould be{ '*': '' }(empty string), not{ '*': '/' }. The test at line 65 correctly expects{ '*': '' }for the same pattern.
packages/ra-core/src/routing/adapters/tanStackRouterProvider.spec.tsx:1 - Test expectation appears incorrect based on the implementation. The implementation strips the leading
/from pathname when the pattern is'*'(not'/*'), soparams['*']should be'anything'not'/anything'. See tanStackRouterProvider.tsx lines 112-118.
packages/ra-core/src/routing/adapters/tanStackRouterProvider.spec.tsx:1 - Test expectation is incorrect. When matching pattern
'*'against pathname'/', based on the implementation (lines 112-118), the result should haveparams: { '*': '' }(empty string after stripping leading/), not{ '*': '/' }.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
slax57
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reviewed all files. Now I need to do some testing...
But really not much to say so far, that is incredible work!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here are some issues I found:
TabbedShowLayoutdoesn't work (can be reproduced in the simple demo by using the TanStack router provider)
Warning: A notFoundError was encountered on the route with ID "__root__", but a notFoundComponent option was not configured, nor was a router level defaultNotFoundComponent configured. Consider configuring at least one of these to avoid TanStack Router's overly generic defaultNotFoundComponent (<p>Not Found</p>) Error Component Stack
at OutletImpl (Match.tsx:317:18)
at Outlet (<anonymous>)
at div (<anonymous>)
at Routes (tanStackRouterProvider.tsx:473:19)
at div (<anonymous>)
at emotion-element-489459f2.browser.development.esm.js:33:17
at OptionalRecordContextProvider (OptionalRecordContextProvider.tsx:23:5)
at TabbedShowLayout (TabbedShowLayout.tsx:84:19)
- Navigating to the 2nd page of a list (or changing any list param actually) fails (attempts to redirect to
http://localhost:8080/#/[object%20Object]) - Story
/story/react-admin-resource--nesteddoesn't work with TanStack router
(e.g. browsing to/authors/1/booksresolves to/authors/1instead of/authors/1/books) - Ids that require URL encoding (such as '衣類/衣類') are not supported
(can be reproduced in the simple demo by changing the id of a Post in the dataProvider)
One more thing: from what I understand, TanStack router provider's <Routes> component resorts to scanning its children to register the routes, which means it won't work with wrapped routes like for example
<Routes>
<Route />
<CanAccess>
<Route />
</CanAccess>
</Routes>We may want to document this limitation somewhere.
UPDATE: this is not supported by react-router either
1. Fix nested routes with Outlet (TabbedShowLayout)
- Add RouteChildrenContext to pass children from Routes to Outlet
- Outlet now renders nested Routes when children exist
2. Fix empty path route matching
- Change `if (route.path)` to `if (route.path !== undefined)`
- Empty string paths were incorrectly treated as falsy
3. Fix Route children inside Resource
- Move children routes before catch-all `/*` in Resource.tsx
- Ensures custom routes match before the list catch-all
4. Fix query parameter navigation (sorting, pagination, filters)
- Handle navigate({ search }) without pathname
- Use history API directly to avoid double-encoding
5. Fix UTF-8/special characters in URL params
- Add decodeURIComponent() for dynamic params (:id)
- Add decodeURIComponent() for splat params (*)
WiXSL
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First of all, great work 🥇
I've worked on some tests that failed.
IMPORTANT:
I've made equivalent tests for react-router/-dom that in master and this branch, pass.
The failing cases are only the following using tanStackRouterProvider on this branch. (If you like, I can provide those react-router/-dom tests as well)
The following tests should go inside packages/ra-router-tanstack/src/tanStackRouterProvider.spec.tsx.
1 - Navigate with object to fails
Use case: Redirect with a pathname + query string using the Navigate component.
Expected: /posts?from=redirect
Actual: Navigates to /#/[object Object] (invalid path).
Test:
Expand to View Code
const { matchPath } = tanStackRouterProvider;
const {
Routes,
Route,
Outlet,
Link,
Navigate,
RouterWrapper,
useLocation,
} = tanStackRouterProvider;
const LocationDebug = () => {
const location = useLocation();
return (
<div>
<div data-testid="location-pathname">{location.pathname}</div>
<div data-testid="location-search">{location.search}</div>
</div>
);
};
it('should support object to with pathname and search', async () => {
window.location.hash = '#/redirect';
render(
<RouterWrapper>
<Routes>
<Route
path="/redirect"
element={
<Navigate
to={{
pathname: '/posts',
search: '?from=redirect',
}}
/>
}
/>
<Route
path="/posts"
element={
<div>
<h2>Posts Page</h2>
<LocationDebug />
</div>
}
/>
</Routes>
</RouterWrapper>
);
await waitFor(() => {
expect(screen.getByText('Posts Page')).toBeInTheDocument();
expect(
screen.getByTestId('location-search').textContent
).toContain('?from=redirect');
});
});2 - Link with object to (search-only) fails
Use case: Update only the query string while staying on the same route.
Expected: Same route, updated query string.
Actual: Invalid navigation because object to is treated as a string unless it has pathname.
Test:
Expand to View Code
const { matchPath } = tanStackRouterProvider;
const {
Routes,
Route,
Outlet,
Link,
Navigate,
RouterWrapper,
useLocation,
} = tanStackRouterProvider;
const LocationDebug = () => {
const location = useLocation();
return (
<div>
<div data-testid="location-pathname">{location.pathname}</div>
<div data-testid="location-search">{location.search}</div>
</div>
);
};
it('should support object to with search only', async () => {
window.location.hash = '#/posts';
render(
<RouterWrapper>
<Routes>
<Route
path="/posts"
element={
<div>
<h2>Posts Page</h2>
<Link to={{ search: '?filter=active' }}>
Apply Filter
</Link>
<LocationDebug />
</div>
}
/>
</Routes>
</RouterWrapper>
);
await waitFor(() => {
expect(screen.getByText('Posts Page')).toBeInTheDocument();
});
const user = userEvent.setup();
await user.click(screen.getByText('Apply Filter'));
await waitFor(() => {
expect(screen.getByText('Posts Page')).toBeInTheDocument();
expect(
screen.getByTestId('location-search').textContent
).toContain('?filter=active');
});
});3 - Pathless layout routes don’t match
Use case: Layout wrapper without path containing child routes.
Expected: Layout + child route renders.
Actual: Nothing renders; the parent route never matches.
Test:
Expand to View Code
const { matchPath } = tanStackRouterProvider;
const {
Routes,
Route,
Outlet,
Link,
Navigate,
RouterWrapper,
useLocation,
} = tanStackRouterProvider;
it('should match pathless layout routes', async () => {
window.location.hash = '#/posts';
render(
<RouterWrapper>
<Routes>
<Route
element={
<div>
<h2>Layout</h2>
<Outlet />
</div>
}
>
<Route
path="/posts"
element={<div>Posts Page</div>}
/>
</Route>
</Routes>
</RouterWrapper>
);
await waitFor(() => {
expect(screen.getByText('Layout')).toBeInTheDocument();
expect(screen.getByText('Posts Page')).toBeInTheDocument();
});
});This approach is simpler but less robust than a route scoring algorithm - we may have to revisit it later.
|
Thanks for your reviews! I fixed all the remaining issues and rebased the PR. |
packages/ra-router-tanstack/src/tanStackRouterProvider.stories.tsx
Outdated
Show resolved
Hide resolved
|
Thanks for your review, I took all your remarks into account. |
slax57
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incredible work!!! Can't wait to see that one released!! 💪 💪 💪
|
|
||
| // Pathless layout route: path is undefined (not empty string) and has children | ||
| // Match if any child route would match the pathname | ||
| if (route.path === undefined && route.children) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pathless layout routes short‑circuit the match loop on the first layout with matching children. This bypasses the later “more specific” logic and can make matching order‑dependent, potentially selecting a layout route even if a more specific sibling should win.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tracked in internal issue tracker so we can check this out later.

Problem
React-admin is tied to react-router. But the two main frameworks used to build new apps today are Next.js and TanStack Start, both NOT using react-router. Although it's possible to integrate RA with Next.js (by opting out of SSR and using a HashRouter), it's cumbersome. It's impossible to do it with TanStack Start.
Solution
Add a routing abstraction layer to allow react-admin to use another router.
Add a TanStack Router implementation to check that the abstraction works for at least 2 routers.
Closes #10743
How To Test
React-admin should use react-router by default, and nothing should be broken. The demo apps should work as before.
A demo app powered by TanStack Router was added:
http://localhost:9010/?path=/story/react-admin-frameworks-tanstack--full-app
It contains CRUD routes and custom routes, as well as sub-routes (TabbedForm) and blocker (useWarnWhenUnsavedChanges). All routing features should work, and are covered by integration tests.
The same demo app can be tested in a second story, where it's embedded into an existing TanStack router app:
http://localhost:9010/?path=/story/react-admin-frameworks-tanstack--embedded
In this case, react-admin doesn't create a router but adds its own routes.
Note: these stories don't reset their route properly, so navigating from one to the other will fail. Refresh the browser when opening one of TanStack router's stories.
Reviewing this PR
This is a large PR, so I rebased the history to allow reviewers to read it commit-by-commit.
The tests should pass at the end of each commit.
Additional Checks
masterfor a bugfix or a documentation fix, ornextfor a feature