Skip to content

Commit 47bd4e7

Browse files
[AppProvider] Add Tanstack React Router provider (#4971)
1 parent abf6ca3 commit 47bd4e7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1553
-193
lines changed

.eslintignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ test
2424
/packages/toolpad-studio/
2525
/packages/toolpad-studio-runtime/
2626
/packages/toolpad-studio-components/
27+
28+
# generated TanStack Router files
29+
routeTree.gen.ts

docs/data/toolpad/core/components/app-provider/app-provider.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export default function App(props) {
8080
}
8181
```
8282

83-
## Client-side routing
83+
## React Router
8484

8585
The `ReactRouterAppProvider` includes routing out-of-the-box for projects using [react-router](https://www.npmjs.com/package/react-router).
8686

@@ -90,6 +90,14 @@ This specific `ReactRouterAppProvider` is recommended when building single-page
9090
import { ReactRouterAppProvider } from '@toolpad/core/react-router';
9191
```
9292

93+
## Other Integrations
94+
95+
### TanStack Router
96+
97+
```tsx
98+
import { TanStackRouterAppProvider } from '@toolpad/core/tanstack-router';
99+
```
100+
93101
## Theming
94102

95103
An `AppProvider` can set a visual theme for all elements inside it to adopt via the `theme` prop. This prop can be set in a few distinct ways with different advantages and disadvantages:

docs/data/toolpad/core/integrations/react-router.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: React Router - Integration
66

77
<p class="description">To integrate Toolpad Core into a single-page app (with Vite, for example) using React Router, follow these steps.</p>
88

9-
## Wrap all your pages in a `ReactRouterAppProvider`
9+
## Wrap all your pages with a `ReactRouterAppProvider`
1010

1111
In your router configuration (for example `src/main.tsx`), use a shared component or element (for example `src/App.tsx`) as a root **layout route** that wraps the whole application with the `ReactRouterAppProvider` from `@toolpad/core/react-router`.
1212

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
---
2+
title: TanStack Router - Integration
3+
---
4+
5+
# TanStack Router
6+
7+
<p class="description">To integrate Toolpad Core into a project that uses TanStack Router, follow these steps.</p>
8+
9+
## Wrap all routes with a `TanStackRouterAppProvider`
10+
11+
In the root route (for example `src/routes/__root.tsx`), wrap all the route page content with the `TanStackRouterAppProvider` from `@toolpad/core/tanstack-router`.
12+
13+
```tsx title="src/routes/__root.tsx"
14+
import * as React from 'react';
15+
import { Outlet, createRootRoute } from '@tanstack/react-router';
16+
import DashboardIcon from '@mui/icons-material/Dashboard';
17+
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
18+
import { TanStackRouterAppProvider } from '@toolpad/core/tanstack-router';
19+
import type { Navigation } from '@toolpad/core/AppProvider';
20+
21+
const NAVIGATION: Navigation = [
22+
{
23+
kind: 'header',
24+
title: 'Main items',
25+
},
26+
{
27+
title: 'Dashboard',
28+
icon: <DashboardIcon />,
29+
},
30+
{
31+
segment: 'orders',
32+
title: 'Orders',
33+
icon: <ShoppingCartIcon />,
34+
},
35+
];
36+
37+
const BRANDING = {
38+
title: 'My Toolpad Core App',
39+
};
40+
41+
function App() {
42+
return (
43+
<TanStackRouterAppProvider navigation={NAVIGATION} branding={BRANDING}>
44+
<Outlet />
45+
</TanStackRouterAppProvider>
46+
);
47+
}
48+
49+
export const Route = createRootRoute({
50+
component: App,
51+
});
52+
```
53+
54+
## Create a dashboard layout
55+
56+
Place all your other routes under a `_layout` folder, where the `_layout/route.tsx` file defines a shared layout to be used by all those routes. The `<Outlet />` component from `@tanstack/react-router` should also be used:
57+
58+
```tsx title="src/routes/_layout/route.tsx"
59+
import * as React from 'react';
60+
import { Outlet, createFileRoute } from '@tanstack/react-router';
61+
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
62+
import { PageContainer } from '@toolpad/core/PageContainer';
63+
64+
function Layout() {
65+
return (
66+
<DashboardLayout>
67+
<PageContainer>
68+
<Outlet />
69+
</PageContainer>
70+
</DashboardLayout>
71+
);
72+
}
73+
74+
export const Route = createFileRoute('/_layout')({
75+
component: Layout,
76+
});
77+
```
78+
79+
The [`DashboardLayout`](/toolpad/core/react-dashboard-layout/) component provides a consistent layout for your dashboard pages, including a sidebar, navigation, and header. The [`PageContainer`](/toolpad/core/react-page-container/) component is used to wrap the page content, and provides breadcrumbs for navigation.
80+
81+
## Create pages
82+
83+
Create a dashboard page (for example `src/routes/_layout/index.tsx`) and an orders page (`src/routes/_layout/orders.tsx`).
84+
85+
```tsx title="src/routes/_layout/index.tsx"
86+
import * as React from 'react';
87+
import { createFileRoute } from '@tanstack/react-router';
88+
import Typography from '@mui/material/Typography';
89+
90+
function DashboardPage() {
91+
return <Typography>Welcome to Toolpad!</Typography>;
92+
}
93+
94+
export const Route = createFileRoute('/_layout/')({
95+
component: DashboardPage,
96+
});
97+
```
98+
99+
```tsx title="src/routes/_layout/orders.tsx"
100+
import * as React from 'react';
101+
import { createFileRoute } from '@tanstack/react-router';
102+
import Typography from '@mui/material/Typography';
103+
104+
function OrdersPage() {
105+
return <Typography>Welcome to the Toolpad orders!</Typography>;
106+
}
107+
108+
export const Route = createFileRoute('/_layout/orders')({
109+
component: OrdersPage,
110+
});
111+
```
112+
113+
That's it! You now have Toolpad Core integrated into your project with TanStack Router!
114+
115+
<!-- :::info
116+
@TODO: Uncomment when example is live
117+
For a full working example, see the [Toolpad Core Vite app with TanStack Router example](https://github.com/mui/toolpad/tree/master/examples/core/vite-tanstack-router/)
118+
::: -->

docs/data/toolpad/core/pages.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ const pages: MuiPage[] = [
5555
pathname: '/toolpad/core/integrations/react-router',
5656
title: 'Vite with React Router',
5757
},
58+
{
59+
pathname: '/toolpad/core/integrations/tanstack-router',
60+
title: 'TanStack Router',
61+
},
5862
],
5963
},
6064
{

docs/pages/toolpad/core/api/app-provider.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"name": "AppProvider",
4242
"imports": [
4343
"import { AppProvider } from '@toolpad/core/AppProvider';",
44-
"import { AppProvider } from '@toolpad/core';\nimport { NextAppProvider } from '@toolpad/core/nextjs'; // Next.js\nimport { ReactRouterAppProvider } from '@toolpad/core/react-router'; // React Router"
44+
"import { AppProvider } from '@toolpad/core';\nimport { NextAppProvider } from '@toolpad/core/nextjs'; // Next.js\nimport { ReactRouterAppProvider } from '@toolpad/core/react-router'; // React Router\nimport { TanStackRouterAppProvider } from '@toolpad/core/tanstack-router'; // TanStack Router"
4545
],
4646
"classes": [],
4747
"spread": true,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as React from 'react';
2+
import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs';
3+
import * as pageProps from '../../../../data/toolpad/core/integrations/tanstack-router.md?muiMarkdown';
4+
5+
export default function Page() {
6+
return <MarkdownDocs disableAd {...pageProps} />;
7+
}

packages/toolpad-core/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
"@mui/utils": "^7.0.2",
4646
"@mui/x-data-grid": "^8.5.0",
4747
"@mui/x-date-pickers": "^8.5.0",
48-
"@standard-schema/spec": "^1.0.0",
4948
"@toolpad/utils": "workspace:*",
5049
"client-only": "^0.0.1",
5150
"dayjs": "1.11.13",
@@ -58,6 +57,8 @@
5857
"@emotion/react": "11.14.0",
5958
"@mui/icons-material": "^7.0.2",
6059
"@mui/material": "^7.0.2",
60+
"@standard-schema/spec": "^1.0.0",
61+
"@tanstack/react-router": "^1.120.5",
6162
"@mui/x-data-grid-premium": "^8.5.0",
6263
"@mui/x-data-grid-pro": "^8.5.0",
6364
"@types/invariant": "2.2.37",
@@ -79,6 +80,7 @@
7980
"@emotion/react": "^11",
8081
"@mui/icons-material": "^7.0.0-beta || ^7.0.0",
8182
"@mui/material": "^7.0.0-beta || ^7.0.0",
83+
"@tanstack/react-router": "^1",
8284
"next": "^14 || ^15",
8385
"react": "^18 || ^19",
8486
"react-dom": "^18 || ^19",
@@ -90,6 +92,9 @@
9092
},
9193
"react-router": {
9294
"optional": true
95+
},
96+
"@tanstack/react-router": {
97+
"optional": true
9398
}
9499
},
95100
"sideEffects": false,

packages/toolpad-core/src/Crud/List.tsx

Lines changed: 65 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import invariant from 'invariant';
3030
import { useDialogs } from '../useDialogs';
3131
import { useNotifications } from '../useNotifications';
3232
import { NoSsr } from '../shared/NoSsr';
33-
import { CrudContext, RouterContext, WindowContext } from '../shared/context';
33+
import { CrudContext, RouterContext } from '../shared/context';
3434
import { useLocaleText } from '../AppProvider/LocalizationProvider';
3535
import { DataSourceCache } from './cache';
3636
import { useCachedDataSource } from './useCachedDataSource';
@@ -158,9 +158,6 @@ function List<D extends DataModel>(props: ListProps<D>) {
158158
const { getMany, deleteOne } = methods;
159159

160160
const routerContext = React.useContext(RouterContext);
161-
const appWindowContext = React.useContext(WindowContext);
162-
163-
const appWindow = appWindowContext ?? (typeof window !== 'undefined' ? window : null);
164161

165162
const dialogs = useDialogs();
166163
const notifications = useNotifications();
@@ -192,53 +189,77 @@ function List<D extends DataModel>(props: ListProps<D>) {
192189
const [isLoading, setIsLoading] = React.useState(true);
193190
const [error, setError] = React.useState<Error | null>(null);
194191

195-
React.useEffect(() => {
196-
if (appWindow) {
197-
const url = new URL(appWindow.location.href);
192+
const handlePaginationModelChange = React.useCallback(
193+
(model: GridPaginationModel) => {
194+
setPaginationModel(model);
198195

199-
url.searchParams.set('page', String(paginationModel.page));
200-
url.searchParams.set('pageSize', String(paginationModel.pageSize));
196+
if (routerContext) {
197+
const { pathname, searchParams, navigate } = routerContext;
201198

202-
if (!appWindow.frameElement) {
203-
appWindow.history.pushState({}, '', url);
204-
}
205-
}
206-
}, [appWindow, paginationModel.page, paginationModel.pageSize]);
199+
// Needed because searchParams from Next.js are read-only
200+
const writeableSearchParams = new URLSearchParams(searchParams);
207201

208-
React.useEffect(() => {
209-
if (appWindow) {
210-
const url = new URL(appWindow.location.href);
211-
212-
if (
213-
filterModel.items.length > 0 ||
214-
(filterModel.quickFilterValues && filterModel.quickFilterValues.length > 0)
215-
) {
216-
url.searchParams.set('filter', JSON.stringify(filterModel));
217-
} else {
218-
url.searchParams.delete('filter');
219-
}
202+
writeableSearchParams.set('page', String(paginationModel.page));
203+
writeableSearchParams.set('pageSize', String(paginationModel.pageSize));
204+
205+
const newSearchParamsString = writeableSearchParams.toString();
220206

221-
if (!appWindow.frameElement) {
222-
appWindow.history.pushState({}, '', url);
207+
navigate(`${pathname}${newSearchParamsString ? '?' : ''}${newSearchParamsString}`);
223208
}
224-
}
225-
}, [appWindow, filterModel]);
209+
},
210+
[paginationModel.page, paginationModel.pageSize, routerContext],
211+
);
226212

227-
React.useEffect(() => {
228-
if (appWindow) {
229-
const url = new URL(appWindow.location.href);
213+
const handleFilterModelChange = React.useCallback(
214+
(model: GridFilterModel) => {
215+
setFilterModel(model);
230216

231-
if (sortModel.length > 0) {
232-
url.searchParams.set('sort', JSON.stringify(sortModel));
233-
} else {
234-
url.searchParams.delete('sort');
217+
if (routerContext) {
218+
const { pathname, searchParams, navigate } = routerContext;
219+
220+
// Needed because searchParams from Next.js are read-only
221+
const writeableSearchParams = new URLSearchParams(searchParams);
222+
223+
if (
224+
filterModel.items.length > 0 ||
225+
(filterModel.quickFilterValues && filterModel.quickFilterValues.length > 0)
226+
) {
227+
writeableSearchParams.set('filter', JSON.stringify(filterModel));
228+
} else {
229+
writeableSearchParams.delete('filter');
230+
}
231+
232+
const newSearchParamsString = writeableSearchParams.toString();
233+
234+
navigate(`${pathname}${newSearchParamsString ? '?' : ''}${newSearchParamsString}`);
235235
}
236+
},
237+
[filterModel, routerContext],
238+
);
239+
240+
const handleSortModelChange = React.useCallback(
241+
(model: GridSortModel) => {
242+
setSortModel(model);
243+
244+
if (routerContext) {
245+
const { pathname, searchParams, navigate } = routerContext;
246+
247+
// Needed because searchParams from Next.js are read-only
248+
const writeableSearchParams = new URLSearchParams(searchParams);
236249

237-
if (!appWindow.frameElement) {
238-
appWindow.history.pushState({}, '', url);
250+
if (sortModel.length > 0) {
251+
writeableSearchParams.set('sort', JSON.stringify(sortModel));
252+
} else {
253+
writeableSearchParams.delete('sort');
254+
}
255+
256+
const newSearchParamsString = writeableSearchParams.toString();
257+
258+
navigate(`${pathname}${newSearchParamsString ? '?' : ''}${newSearchParamsString}`);
239259
}
240-
}
241-
}, [appWindow, sortModel]);
260+
},
261+
[routerContext, sortModel],
262+
);
242263

243264
const loadData = React.useCallback(async () => {
244265
setError(null);
@@ -417,11 +438,11 @@ function List<D extends DataModel>(props: ListProps<D>) {
417438
filterMode="server"
418439
paginationMode="server"
419440
paginationModel={paginationModel}
420-
onPaginationModelChange={setPaginationModel}
441+
onPaginationModelChange={handlePaginationModelChange}
421442
sortModel={sortModel}
422-
onSortModelChange={setSortModel}
443+
onSortModelChange={handleSortModelChange}
423444
filterModel={filterModel}
424-
onFilterModelChange={setFilterModel}
445+
onFilterModelChange={handleFilterModelChange}
425446
disableRowSelectionOnClick
426447
onRowClick={handleRowClick}
427448
loading={isLoading}

0 commit comments

Comments
 (0)