From 980046f45661153f8f42237f4cb5aafc5d857ec9 Mon Sep 17 00:00:00 2001 From: Eric Hu Date: Wed, 15 Apr 2026 01:05:14 +0800 Subject: [PATCH 1/5] feat: add TanStack Router integration --- .../docs/core/refine-component/index.md | 1 + .../docs/guides-concepts/routing/index.md | 122 +++++- .../resource-and-routes-usage.tsx | 216 ++++++++++ .../tanstack-router/use-form-usage.tsx | 205 +++++++++ .../tanstack-router/use-modal-form-usage.tsx | 231 +++++++++++ .../tanstack-router/use-table-usage.tsx | 245 +++++++++++ .../docs/packages/list-of-packages/index.md | 1 + .../integrations/tanstack-router/index.md | 331 +++++++++++++++ .../docs/routing/router-provider/index.md | 51 +++ documentation/sidebars.js | 3 +- .../tutorial/essentials/intro/index.md | 2 +- .../authentication/tanstack-router/index.md | 38 ++ .../tanstack-router/sandpack.tsx | 133 ++++++ .../tanstack-router/index.md | 134 ++++++ .../tanstack-router/sandpack.tsx | 389 ++++++++++++++++++ .../routing/intro/tanstack-router/index.md | 62 +++ .../intro/tanstack-router/sandpack.tsx | 118 ++++++ .../navigation/tanstack-router/index.md | 44 ++ .../navigation/tanstack-router/sandpack.tsx | 237 +++++++++++ .../redirects/tanstack-router/index.md | 127 ++++++ .../redirects/tanstack-router/sandpack.tsx | 301 ++++++++++++++ .../tanstack-router/index.md | 53 +++ .../tanstack-router/sandpack.tsx | 293 +++++++++++++ .../syncing-state/tanstack-router/index.md | 59 +++ .../tanstack-router/sandpack.tsx | 197 +++++++++ packages/tanstack-router/.npmignore | 10 + packages/tanstack-router/CHANGELOG.md | 3 + packages/tanstack-router/LICENSE | 21 + packages/tanstack-router/README.md | 29 ++ packages/tanstack-router/package.json | 78 ++++ packages/tanstack-router/src/bindings.tsx | 139 +++++++ .../src/catch-all-navigate.tsx | 12 + .../src/convert-to-number-if-possible.ts | 16 + .../src/document-title-handler.test.tsx | 192 +++++++++ .../src/document-title-handler.tsx | 67 +++ packages/tanstack-router/src/index.ts | 6 + .../src/navigate-to-resource.test.tsx | 205 +++++++++ .../src/navigate-to-resource.tsx | 46 +++ .../tanstack-router/src/test/dataMocks.ts | 110 +++++ packages/tanstack-router/src/test/index.tsx | 125 ++++++ .../tanstack-router/src/test/vitest.setup.ts | 30 ++ .../src/unsaved-changes-notifier.tsx | 65 +++ .../tanstack-router/src/use-document-title.ts | 26 ++ .../tsconfig.declarations.json | 21 + packages/tanstack-router/tsconfig.json | 8 + packages/tanstack-router/tsup.config.ts | 30 ++ packages/tanstack-router/vitest.config.mts | 24 ++ pnpm-lock.yaml | 132 ++++++ 48 files changed, 4984 insertions(+), 4 deletions(-) create mode 100644 documentation/docs/guides-concepts/routing/tanstack-router/resource-and-routes-usage.tsx create mode 100644 documentation/docs/guides-concepts/routing/tanstack-router/use-form-usage.tsx create mode 100644 documentation/docs/guides-concepts/routing/tanstack-router/use-modal-form-usage.tsx create mode 100644 documentation/docs/guides-concepts/routing/tanstack-router/use-table-usage.tsx create mode 100644 documentation/docs/routing/integrations/tanstack-router/index.md create mode 100644 documentation/tutorial/routing/authentication/tanstack-router/index.md create mode 100644 documentation/tutorial/routing/authentication/tanstack-router/sandpack.tsx create mode 100644 documentation/tutorial/routing/inferring-parameters/tanstack-router/index.md create mode 100644 documentation/tutorial/routing/inferring-parameters/tanstack-router/sandpack.tsx create mode 100644 documentation/tutorial/routing/intro/tanstack-router/index.md create mode 100644 documentation/tutorial/routing/intro/tanstack-router/sandpack.tsx create mode 100644 documentation/tutorial/routing/navigation/tanstack-router/index.md create mode 100644 documentation/tutorial/routing/navigation/tanstack-router/sandpack.tsx create mode 100644 documentation/tutorial/routing/redirects/tanstack-router/index.md create mode 100644 documentation/tutorial/routing/redirects/tanstack-router/sandpack.tsx create mode 100644 documentation/tutorial/routing/resource-definition/tanstack-router/index.md create mode 100644 documentation/tutorial/routing/resource-definition/tanstack-router/sandpack.tsx create mode 100644 documentation/tutorial/routing/syncing-state/tanstack-router/index.md create mode 100644 documentation/tutorial/routing/syncing-state/tanstack-router/sandpack.tsx create mode 100644 packages/tanstack-router/.npmignore create mode 100644 packages/tanstack-router/CHANGELOG.md create mode 100644 packages/tanstack-router/LICENSE create mode 100644 packages/tanstack-router/README.md create mode 100644 packages/tanstack-router/package.json create mode 100644 packages/tanstack-router/src/bindings.tsx create mode 100644 packages/tanstack-router/src/catch-all-navigate.tsx create mode 100644 packages/tanstack-router/src/convert-to-number-if-possible.ts create mode 100644 packages/tanstack-router/src/document-title-handler.test.tsx create mode 100644 packages/tanstack-router/src/document-title-handler.tsx create mode 100644 packages/tanstack-router/src/index.ts create mode 100644 packages/tanstack-router/src/navigate-to-resource.test.tsx create mode 100644 packages/tanstack-router/src/navigate-to-resource.tsx create mode 100644 packages/tanstack-router/src/test/dataMocks.ts create mode 100644 packages/tanstack-router/src/test/index.tsx create mode 100644 packages/tanstack-router/src/test/vitest.setup.ts create mode 100644 packages/tanstack-router/src/unsaved-changes-notifier.tsx create mode 100644 packages/tanstack-router/src/use-document-title.ts create mode 100644 packages/tanstack-router/tsconfig.declarations.json create mode 100644 packages/tanstack-router/tsconfig.json create mode 100644 packages/tanstack-router/tsup.config.ts create mode 100644 packages/tanstack-router/vitest.config.mts diff --git a/documentation/docs/core/refine-component/index.md b/documentation/docs/core/refine-component/index.md index cdad6bcbd2f2c..e6f471ada3a82 100644 --- a/documentation/docs/core/refine-component/index.md +++ b/documentation/docs/core/refine-component/index.md @@ -54,6 +54,7 @@ const App = () => ( Refine provides a simple interface from the `routerProvider` prop to infer the resource from route, pass, parse and sync the query parameters and handle navigation operations. This provider and its properties are optional but it is recommended to use it to get the most out of Refine. Bindings to pass to the `routerProvider` prop are provided for the following libraries: - React Router via `@refinedev/react-router` +- TanStack Router via `@refinedev/tanstack-router` - Next.js via `@refinedev/nextjs-router` - Remix via `@refinedev/remix-router` diff --git a/documentation/docs/guides-concepts/routing/index.md b/documentation/docs/guides-concepts/routing/index.md index ccdbcc202f0bd..5ede810fb7117 100644 --- a/documentation/docs/guides-concepts/routing/index.md +++ b/documentation/docs/guides-concepts/routing/index.md @@ -7,7 +7,7 @@ description: "Implement Routing in Refine v5. Learn the key steps. Learn best pr Routing is essential for any CRUD application. Refine's headless architecture allows you to use any router solution, without being locked into a specific router/framework. -Refine also offers built-in router integrations for the most popular frameworks such as **React Router**, **Next.js** and **Remix**. +Refine also offers built-in router integrations for the most popular frameworks such as **TanStack Router**, **React Router**, **Next.js** and **Remix**. These integrations makes it easier to use Refine with these frameworks and offers a lot of benefits such as: @@ -18,6 +18,7 @@ These integrations makes it easier to use Refine with these frameworks and offer Since Refine is router agnostic, you are responsible for creating your own routes. If you are using **React Router**, you'll be defining your routes under the `Routes` component.
+If you are using **TanStack Router**, you'll be defining your routes in a route tree created with `createRootRoute`, `createRoute` and `createRouter`.
If you are using **Next.js**, you'll be defining your routes in the `pages` or `app` directory.
If you are using **Remix**, you'll be defining your routes in the `app/routes` directory. @@ -26,7 +27,45 @@ If you are using **Remix**, you'll be defining your routes in the `app/routes` d To integrate a router provider with Refine, all you need to do is to import the router integration of your choice and pass it to the ``'s `routerProvider` prop. - + + +```tsx title="App.tsx" +import { Outlet, RouterProvider, createRootRoute, createRoute, createRouter } from "@tanstack/react-router"; +import { Refine } from "@refinedev/core"; +// highlight-next-line +import routerProvider from "@refinedev/tanstack-router"; + +const rootRoute = createRootRoute({ + component: () => ( + + + + ), +}); + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/", + component: () =>
Home
, +}); + +const router = createRouter({ + routeTree: rootRoute.addChildren([indexRoute]), +}); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +const App = () => ; +``` + +[Check out TanStack Router documentation for detailed information](/core/docs/routing/integrations/tanstack-router/) + +
+ ```tsx title="App.tsx" import { BrowserRouter, Routes } from "react-router"; @@ -183,6 +222,12 @@ import { ReactRouterResourceAndRoutesUsage } from "./react-router/resource-and-r +#### TanStack Router + +import { TanStackRouterResourceAndRoutesUsage } from "./tanstack-router/resource-and-routes-usage"; + + + #### Next.js import { NextJSResourceAndRoutesUsage } from "./nextjs/resource-and-routes-usage"; @@ -208,6 +253,14 @@ import { RemixResourceAndRoutesUsage } from "./remix/resource-and-routes-usage"; Router integration of Refine allows you to use `useForm` without passing **resource**, **id** and **action** parameters. It will also redirect you to resource's action route defined in `redirect` prop. `redirect` prop is `list` by default. +#### TanStack Router + +import { TanStackRouterUseFormUsage } from "./tanstack-router/use-form-usage"; + + + +#### React Router + import { ReactRouterUseFormUsage } from "./react-router/use-form-usage"; @@ -215,6 +268,55 @@ import { ReactRouterUseFormUsage } from "./react-router/use-form-usage"; Additionally, router integrations exposes an `` component which can be used to notify the user about unsaved changes before navigating away from the current page. This component provides this feature which can be enabled by setting `warnWhenUnsavedChanges` to `true` in `useForm` hooks. + + +```tsx title="app.tsx" +import { Refine } from "@refinedev/core"; +import routerProvider, { + UnsavedChangesNotifier, +} from "@refinedev/tanstack-router"; +import { Outlet, RouterProvider, createRootRoute, createRoute, createRouter } from "@tanstack/react-router"; + +const rootRoute = createRootRoute({ + component: () => ( + + + + + ), +}); + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/", + component: () =>
Home
, +}); + +const router = createRouter({ + routeTree: rootRoute.addChildren([indexRoute]), +}); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +export default function App() { + return ; +} +``` + +Check out the [`UnsavedChangesNotifier` section of the TanStack Router integration documentation](/core/docs/routing/integrations/tanstack-router#unsavedchangesnotifier) for more information. + +
```tsx title="app.tsx" @@ -383,6 +485,8 @@ const { ... } = useTable( /my-products?currentPage=1&pageSize=2&sorters[0][field]=id&sorters[0][order]=asc&filters[0][field]=category.id&filters[0][operator]=eq&filters[0][value]=1 ``` +TanStack Router uses its own search-param serialization, so the exact URL string can differ there even though Refine will infer the same pagination, sorting, and filtering state. + And you will see a list of products, already **filtered**, **sorted** and **paginated** automatically based on the query parameters of the **current route**. ```ts @@ -406,6 +510,12 @@ import { ReactRouterUseTableUsage } from "./react-router/use-table-usage"; +#### TanStack Router + +import { TanStackRouterUseTableUsage } from "./tanstack-router/use-table-usage"; + + + #### Next.js You can use SSR feature with Next.js to fetch initial data on the server side. @@ -440,6 +550,14 @@ Once the modal is visible, current route will look like this: You can see the example below for usage. +#### TanStack Router + +import { TanStackRouterUseModalFormUsage } from "./tanstack-router/use-modal-form-usage"; + + + +#### React Router + import { ReactRouterUseModalFormUsage } from "./react-router/use-modal-form-usage"; diff --git a/documentation/docs/guides-concepts/routing/tanstack-router/resource-and-routes-usage.tsx b/documentation/docs/guides-concepts/routing/tanstack-router/resource-and-routes-usage.tsx new file mode 100644 index 0000000000000..c951a9ed911a2 --- /dev/null +++ b/documentation/docs/guides-concepts/routing/tanstack-router/resource-and-routes-usage.tsx @@ -0,0 +1,216 @@ +import { Sandpack } from "@site/src/components/sandpack"; +import React from "react"; + +export function TanStackRouterResourceAndRoutesUsage() { + return ( + + ); +} + +const AppTsxCode = /* tsx */ ` +import React from "react"; + +import { Refine } from "@refinedev/core"; +import routerProvider from "@refinedev/tanstack-router"; +import dataProvider from "@refinedev/simple-rest"; +import { + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from "@tanstack/react-router"; + +import "./style.css"; + +import { ProductList } from "./pages/products/list.tsx"; +import { ProductShow } from "./pages/products/show.tsx"; + +const rootRoute = createRootRoute({ + component: () => ( + + + + ), +}); + +const listRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/my-products", + component: ProductList, +}); + +const showRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/my-products/$id", + component: ProductShow, +}); + +const routeTree = rootRoute.addChildren([listRoute, showRoute]); + +const router = createRouter({ routeTree }); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +export default function App() { + return ; +} +`.trim(); + +const StyleCssCode = ` +html { + margin: 0; + padding: 0; +} +body { + margin: 0; + padding: 12px; +} +* { + box-sizing: border-box; +} +body { + font-family: sans-serif; +} +form label, form input, form button { + display: block; + width: 100%; + margin-bottom: 6px; +} +span + button { + margin-left: 6px; +} +ul > li { + margin-bottom: 6px; +} +`.trim(); + +const ListTsxCode = ` +import React from "react"; + +import { useGo, useList } from "@refinedev/core"; + +export const ProductList: React.FC = () => { + // We're inferring the resource from the route + // So we call \`useList\` hook without any arguments. + // const { ... } = useList({ resource: "products" }) + const { result, query } = useList(); + const products = result?.data; + + const go = useGo(); + + if (query.isLoading) return
Loading...
; + + return ( +
    + {products?.map((product) => ( +
  • + {product.name} + +
  • + ))} +
+ ); +}; +`.trim(); + +const ShowTsxCode = ` +import React from "react"; + +import { useGo, useShow } from "@refinedev/core"; + +export const ProductShow: React.FC = () => { + // We're inferring the resource and the id from the route params + // So we can call useShow hook without any arguments. + // const result = useShow({ resource: "products", id: "xxx" }) + const result = useShow(); + + const { + query: { data, isLoading }, + } = result; + + const go = useGo(); + + if (isLoading) return
Loading...
; + + return ( + <> +
+

{data?.data?.name}

+

Material: {data?.data?.material}

+ ID: {data?.data?.id} +
+ + + ); +}; +`.trim(); + diff --git a/documentation/docs/guides-concepts/routing/tanstack-router/use-form-usage.tsx b/documentation/docs/guides-concepts/routing/tanstack-router/use-form-usage.tsx new file mode 100644 index 0000000000000..136100154e3b5 --- /dev/null +++ b/documentation/docs/guides-concepts/routing/tanstack-router/use-form-usage.tsx @@ -0,0 +1,205 @@ +import { Sandpack } from "@site/src/components/sandpack"; +import React from "react"; + +export function TanStackRouterUseFormUsage() { + return ( + + ); +} + +const AppTsxCode = /* tsx */ ` +import React from "react"; + +import { Refine } from "@refinedev/core"; +import routerProvider from "@refinedev/tanstack-router"; +import dataProvider from "@refinedev/simple-rest"; +import { + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from "@tanstack/react-router"; + +import "./style.css"; + +import { ProductEdit } from "./edit.tsx"; +import { ProductList } from "./list.tsx"; + +const rootRoute = createRootRoute({ + component: () => ( + + + + ), +}); + +const listRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/my-products", + component: ProductList, +}); + +const editRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/my-products/$id/edit", + component: ProductEdit, +}); + +const routeTree = rootRoute.addChildren([listRoute, editRoute]); + +const router = createRouter({ routeTree }); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +export default function App() { + return ; +} +`.trim(); + +const StyleCssCode = ` +html { + margin: 0; + padding: 0; +} +body { + margin: 0; + padding: 12px; +} +* { + box-sizing: border-box; +} +body { + font-family: sans-serif; +} +form label, form input, form button { + display: block; + width: 100%; + margin-bottom: 6px; +} +span + button { + margin-left: 6px; +} +ul > li { + margin-bottom: 6px; +} +`.trim(); + +const ListTsxCode = ` +import React from "react"; + +import { useGo, useList } from "@refinedev/core"; + +export const ProductList: React.FC = () => { + const { result, query } = useList(); + const products = result?.data; + + const go = useGo(); + + if (query.isLoading) return
Loading...
; + + return ( +
    + {products?.map((product) => ( +
  • + {product.name} + +
  • + ))} +
+ ); +}; +`.trim(); + +const EditTsxCode = ` +import React from "react"; + +import { useForm } from "@refinedev/core"; + +export const ProductEdit: React.FC = () => { + const { formLoading, onFinish, query } = useForm(); + const defaultValues = query?.data?.data; + + const onSubmit = (e) => { + e.preventDefault(); + const data = Object.fromEntries(new FormData(e.target).entries()); + + onFinish(data); + }; + + return ( +
+
+
+
+ + +
+ +
+
+ ); +}; +`.trim(); + diff --git a/documentation/docs/guides-concepts/routing/tanstack-router/use-modal-form-usage.tsx b/documentation/docs/guides-concepts/routing/tanstack-router/use-modal-form-usage.tsx new file mode 100644 index 0000000000000..5125d97c4183b --- /dev/null +++ b/documentation/docs/guides-concepts/routing/tanstack-router/use-modal-form-usage.tsx @@ -0,0 +1,231 @@ +import { Sandpack } from "@site/src/components/sandpack"; +import React from "react"; + +export function TanStackRouterUseModalFormUsage() { + return ( + + ); +} + +const ModalComponentTsxCode = /* tsx */ ` +import React from "react"; + +export const Modal: React.FC = ({ isOpen, onClose, children }) => { + if (!isOpen) return null; + + return ( + <> +
+
+
+ +
+
{children}
+
+ + ); +}; +`.trim(); + +const AppTsxCode = /* tsx */ ` +import React from "react"; + +import { Refine } from "@refinedev/core"; +import routerProvider from "@refinedev/tanstack-router"; +import dataProvider from "@refinedev/simple-rest"; +import { + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from "@tanstack/react-router"; + +import "./style.css"; + +import { ProductList } from "./pages/products/list.tsx"; + +const rootRoute = createRootRoute({ + component: () => ( + + + + ), +}); + +const listRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/my-products", + component: ProductList, +}); + +const routeTree = rootRoute.addChildren([listRoute]); + +const router = createRouter({ routeTree }); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +export default function App() { + return ; +} +`.trim(); + +const StyleCssCode = ` +html { + margin: 0; + padding: 0; +} +body { + margin: 0; + padding: 12px; +} +* { + box-sizing: border-box; +} +body { + font-family: sans-serif; +} + +.overlay { + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + background-color: rgba(0, 0, 0, 0.7); + z-index: 1000; +} + +.modal { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: #fff; + z-index: 1000; + width: 75%; + overflow-y: auto; +} + +.modal .modal-title { + display: flex; + justify-content: flex-end; + padding: 4px; +} + +.modal .modal-content { + padding: 0px 16px 16px 16px; +} +form label, form input, form button { + display: block; + width: 100%; + margin-top: 3px; + margin-bottom: 3px; +} +span + button { + margin-left: 6px; +} +ul > li { + margin-bottom: 6px; +} +`.trim(); + +const ListTsxCode = ` +import React from "react"; + +import { useList } from "@refinedev/core"; +import { useModalForm } from "@refinedev/react-hook-form"; + +import { Modal } from "../../components/modal.tsx"; + +export const ProductList: React.FC = () => { + const { result, query } = useList(); + const products = result?.data; + + const { + modal: { visible, close, show }, + refineCore: { onFinish, formLoading }, + handleSubmit, + register, + saveButtonProps, + } = useModalForm({ + refineCoreProps: { action: "edit" }, + syncWithLocation: true, + }); + + if (query.isLoading) return
Loading...
; + + return ( + <> + +
+
+ + +
+ +
+
+
    + {products?.map((product) => ( +
  • + {product.name} + +
  • + ))} +
+ + ); +}; +`.trim(); + diff --git a/documentation/docs/guides-concepts/routing/tanstack-router/use-table-usage.tsx b/documentation/docs/guides-concepts/routing/tanstack-router/use-table-usage.tsx new file mode 100644 index 0000000000000..62a1e267dbf41 --- /dev/null +++ b/documentation/docs/guides-concepts/routing/tanstack-router/use-table-usage.tsx @@ -0,0 +1,245 @@ +import { Sandpack } from "@site/src/components/sandpack"; +import React from "react"; + +export function TanStackRouterUseTableUsage() { + return ( + + ); +} + +const AppTsxCode = /* tsx */ ` +import React from "react"; + +import { Refine } from "@refinedev/core"; +import routerProvider from "@refinedev/tanstack-router"; +import dataProvider from "@refinedev/simple-rest"; +import { + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from "@tanstack/react-router"; + +import "./style.css"; + +import { ListPage } from "./pages/products/list.tsx"; + +const rootRoute = createRootRoute({ + component: () => ( + + + + ), +}); + +const listRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/my-products", + component: ListPage, +}); + +const routeTree = rootRoute.addChildren([listRoute]); + +const router = createRouter({ routeTree }); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +export default function App() { + return ; +} +`.trim(); + +const StyleCssCode = ` +html { + margin: 0; + padding: 0; + font-size: 14px; +} +body { + margin: 0; + padding: 12px; +} +* { + box-sizing: border-box; +} +body { + font-family: sans-serif; +} +form label, form input, form button { + display: block; + width: 100%; + margin-bottom: 6px; +} +span, button { + margin: 6px 0; +} +ul > li { + margin-bottom: 6px; +} +`.trim(); + +const ListPageTsxCode = ` +import React from "react"; + +import { useTable } from "@refinedev/core"; + +import { ProductList } from "../../components/products/list"; + +export const ListPage: React.FC = () => { + const tableProps = useTable({ + pagination: { currentPage: 1, pageSize: 2 }, + filters: { + initial: [{ field: "category.id", operator: "eq", value: "1" }], + }, + sorters: { initial: [{ field: "id", order: "asc" }] }, + }); + + return ; +}; +`.trim(); + +const ListTsxCode = ` +import React from "react"; + +export const ProductList: React.FC = ({ tableProps }) => { + const { + result, + tableQuery, + isLoading, + currentPage, + setCurrentPage, + pageSize, + pageCount, + filters, + setFilters, + sorters, + setSorters, + } = tableProps; + + if (isLoading) return
Loading...
; + + return ( +
+

Products

+ + + + + + + + + + {result?.data?.map((record) => ( + + + + + + ))} + +
idnamecategoryId
{record.id}{record.name}{record.category.id}
+
+ Sorting by field: + + {sorters[0].field}, order {sorters[0].order} + +
+ +
+ Filtering by field: + + {filters[0].field}, operator {filters[0].operator}, value + {filters[0].value} + +
+ +
+

Current Page: {currentPage}

+

Page Size: {pageSize}

+ + +
+ ); +}; +`.trim(); + diff --git a/documentation/docs/packages/list-of-packages/index.md b/documentation/docs/packages/list-of-packages/index.md index 6479e614ae1f8..d6bd00a125654 100644 --- a/documentation/docs/packages/list-of-packages/index.md +++ b/documentation/docs/packages/list-of-packages/index.md @@ -40,6 +40,7 @@ To learn more about Data Providers, check out [Data Provider](/core/docs/data/da To learn more about Router Providers, check out [Routing](/core/docs/guides-concepts/routing/) guide. - [`@refinedev/react-router`](/core/docs/routing/integrations/react-router/) - Router Provider for [React Router](https://reactrouter.com) +- [`@refinedev/tanstack-router`](/core/docs/routing/integrations/tanstack-router/) - Router Provider for [TanStack Router](https://tanstack.com/router/latest) - [`@refinedev/nextjs-router`](/core/docs/routing/integrations/next-js/) - Router Provider for [Next.js](https://nextjs.org/docs/api-reference/next/router#userouter) - [`@refinedev/remix-router`](/core/docs/routing/integrations/remix/) - Router Provider for [Remix](https://remix.run/) - [`@refinenative/expo-router`](https://www.npmjs.com/package/@refinenative/expo-router) - Router Provider for [Expo](https://docs.expo.dev/) diff --git a/documentation/docs/routing/integrations/tanstack-router/index.md b/documentation/docs/routing/integrations/tanstack-router/index.md new file mode 100644 index 0000000000000..d690209924a61 --- /dev/null +++ b/documentation/docs/routing/integrations/tanstack-router/index.md @@ -0,0 +1,331 @@ +--- +title: "TanStack Router Guide | SSR in Refine v5" +display_title: "TanStack Router" +sidebar_label: "TanStack Router" +description: "Implement TanStack Router in Refine v5. Learn the key steps. Explore typed navigation, layouts, auth flows, and URL state syncing." +--- + +Refine provides router bindings and utilities for [TanStack Router](https://tanstack.com/router/latest). It is built on top of the `@tanstack/react-router` package and gives Refine a typed way to infer resources from the current route, navigate between action pages, keep table state in the URL, and wire route-aware helpers such as `NavigateToResource`, `CatchAllNavigate`, `DocumentTitleHandler`, and `UnsavedChangesNotifier`. + + + +[Refer to the Router Provider documentation for detailed information. →](/core/docs/routing/router-provider/) + +## Usage + +TanStack Router works best when Refine lives inside your route tree rather than around the whole application shell. The usual pattern is: + +1. Create a root route. +2. Render `` from that root route component. +3. Define resource routes with `createRoute`. +4. Pass `@refinedev/tanstack-router` to the `routerProvider` prop. + +### Basic Usage + +```tsx title=App.tsx +import { Refine } from "@refinedev/core"; +import dataProvider from "@refinedev/simple-rest"; +import routerProvider from "@refinedev/tanstack-router"; +import { + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from "@tanstack/react-router"; + +import { PostList, PostCreate } from "pages/posts"; +import { CategoryList, CategoryShow } from "pages/categories"; + +const rootRoute = createRootRoute({ + component: () => ( + + + + ), +}); + +const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/posts", + component: PostList, +}); + +const createPostRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/posts/create", + component: PostCreate, +}); + +const categoriesRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/categories", + component: CategoryList, +}); + +const categoryShowRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/categories/$id", + component: CategoryShow, +}); + +const router = createRouter({ + routeTree: rootRoute.addChildren([ + postsRoute, + createPostRoute, + categoriesRoute, + categoryShowRoute, + ]), +}); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +export default function App() { + return ; +} +``` + +### Usage with Authentication + +TanStack Router encourages route-tree composition. With Refine, a good default is to keep authentication decisions in route components and still use Refine’s [``](/core/docs/authentication/components/authenticated/) component so the same `authProvider` contract powers redirects and UI guards. + +```tsx title=App.tsx +import { Authenticated, Refine } from "@refinedev/core"; +import routerProvider, { NavigateToResource } from "@refinedev/tanstack-router"; +import { + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from "@tanstack/react-router"; + +import { authProvider } from "./authProvider"; +import { AuthPage } from "@refinedev/antd"; +import { PostList } from "./pages/posts/list"; + +const rootRoute = createRootRoute({ + component: () => ( + + + + ), +}); + +const loginRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/login", + component: () => ( + }> + + + ), +}); + +const protectedRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/posts", + component: () => ( + + + + ), +}); + +const router = createRouter({ + routeTree: rootRoute.addChildren([loginRoute, protectedRoute]), +}); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +export default function App() { + return ; +} +``` + +If you prefer TanStack Router’s `beforeLoad` redirects for auth, you can layer that on top of Refine as well. The important part is that the redirect target and the rendered UI still line up with the same Refine `authProvider`. + +### Usage with Layouts + +Layouts fit naturally into TanStack Router by using pathless or parent routes. + +```tsx title=App.tsx +import { Authenticated, Refine } from "@refinedev/core"; +import routerProvider, { NavigateToResource } from "@refinedev/tanstack-router"; +import { ThemedLayout } from "@refinedev/antd"; +import { + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from "@tanstack/react-router"; + +import { authProvider } from "./authProvider"; +import { PostList } from "./pages/posts/list"; + +const rootRoute = createRootRoute({ + component: () => ( + + + + ), +}); + +const appLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + id: "app-layout", + component: () => ( + + + + + + ), +}); + +const indexRoute = createRoute({ + getParentRoute: () => appLayoutRoute, + path: "/", + component: () => , +}); + +const postsRoute = createRoute({ + getParentRoute: () => appLayoutRoute, + path: "/posts", + component: PostList, +}); + +const router = createRouter({ + routeTree: rootRoute.addChildren([ + appLayoutRoute.addChildren([indexRoute, postsRoute]), + ]), +}); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +export default function App() { + return ; +} +``` + +### Usage with an Error Page + +TanStack Router already has a strong route-tree story for 404s. When you want the current location preserved for a login redirect, `CatchAllNavigate` mirrors the React Router helper. + +```tsx title=App.tsx +import { Authenticated } from "@refinedev/core"; +import { CatchAllNavigate } from "@refinedev/tanstack-router"; +import { ErrorComponent } from "@refinedev/antd"; + +const notFoundRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/_404", + component: ErrorComponent, +}); + +const protectedNotFoundRoute = createRoute({ + getParentRoute: () => rootRoute, + id: "protected-404", + component: () => ( + }> + + + ), +}); +``` + +### `NavigateToResource` + +`NavigateToResource` redirects to the first resource with a `list` action, or to the specific `resource` prop when provided. + +```tsx +import { NavigateToResource } from "@refinedev/tanstack-router"; + +; +``` + +### `CatchAllNavigate` + +`CatchAllNavigate` redirects to a path and includes the current location in a `to` search param. This is useful for login pages and auth flows. + +```tsx +import { CatchAllNavigate } from "@refinedev/tanstack-router"; + +; +``` + +### `DocumentTitleHandler` + +`DocumentTitleHandler` derives the current resource and action from Refine’s parsed route info and updates `document.title`. + +```tsx +import { DocumentTitleHandler } from "@refinedev/tanstack-router"; + + { + if (resource?.name === "posts" && action === "list") { + return "Posts | Admin"; + } + + return autoGeneratedTitle; + }} +/>; +``` + +### `UnsavedChangesNotifier` + +`UnsavedChangesNotifier` plugs into TanStack Router’s navigation blocking APIs and Refine’s `warnWhenUnsavedChanges` option. + +```tsx +import { Refine } from "@refinedev/core"; +import routerProvider, { + UnsavedChangesNotifier, +} from "@refinedev/tanstack-router"; + + + +; +``` + +Mount it under `` so it can watch both Refine’s form state and TanStack Router transitions. diff --git a/documentation/docs/routing/router-provider/index.md b/documentation/docs/routing/router-provider/index.md index 1fae63b5cf97e..1f90394bcc54a 100644 --- a/documentation/docs/routing/router-provider/index.md +++ b/documentation/docs/routing/router-provider/index.md @@ -15,6 +15,7 @@ Rather than restricting and limiting our users to specific routing libraries or :::simple Out of the Box Router Providers - [React Router][react-router] +- [TanStack Router][tanstack-router] - [Next.js Router][nextjs-router] - [Remix Router][remix-router] - [Expo Router (Community)][expo-router] @@ -63,6 +64,7 @@ To activate router provider in Refine, we have to pass the `routerProvider` to t defaultValue="react-router-v6" values={[ {label: 'React Router', value: 'react-router-v6'}, +{label: 'TanStack Router', value: 'tanstack-router'}, {label: 'Next.js Router', value: 'nextjs'}, {label: 'Remix Router', value: 'remix'}, {label: 'React Router V5 (Legacy)', value: 'react-router'}, @@ -91,6 +93,53 @@ const App: React.FC = () => { }; ``` +
+ + +```tsx title="App.tsx" +import { Refine } from "@refinedev/core"; +import routerProvider from "@refinedev/tanstack-router"; +import { + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from "@tanstack/react-router"; + +const rootRoute = createRootRoute({ + component: () => ( + + + + ), +}); + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/", + component: () =>
Home
, +}); + +const router = createRouter({ + routeTree: rootRoute.addChildren([indexRoute]), +}); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +const App: React.FC = () => { + return ; +}; +``` +
@@ -204,6 +253,7 @@ The `Link` component is used to create links to other pages. It accepts a `to` p ### Source Code for the Existing Router Providers - [React Router](https://github.com/refinedev/refine/blob/main/packages/react-router/src/bindings.tsx) +- [TanStack Router](https://github.com/refinedev/refine/blob/main/packages/tanstack-router/src/bindings.tsx) - [Next.js Router](https://github.com/refinedev/refine/blob/main/packages/nextjs-router/src/pages/bindings.tsx) - [Remix Router](https://github.com/refinedev/refine/blob/main/packages/remix-router/src/bindings.tsx) @@ -215,5 +265,6 @@ If you want to use a legacy router provider, you can pass them to the ` + +Before defining resource routes, we should split public and protected parts of the route tree. + +With TanStack Router, a clean Refine setup is: + +1. Keep a root route that renders ``. +2. Add a protected layout route that wraps its children with ``. +3. Add a dedicated `/login` route that renders the login page when the user is logged out and redirects back when they are already authenticated. + +```tsx title="src/App.tsx" + +
+ + +``` + +For the login page: + +```tsx title="src/App.tsx" +}> + + +``` + +TanStack Router handles the route tree, while Refine keeps the authentication logic centered around the same `authProvider` contract you already configured in earlier units. + + + +The next step is defining the actual CRUD routes and mapping them to Refine resources. + + diff --git a/documentation/tutorial/routing/authentication/tanstack-router/sandpack.tsx b/documentation/tutorial/routing/authentication/tanstack-router/sandpack.tsx new file mode 100644 index 0000000000000..c4f76a1ce59d3 --- /dev/null +++ b/documentation/tutorial/routing/authentication/tanstack-router/sandpack.tsx @@ -0,0 +1,133 @@ +import React from "react"; +import { TutorialSandpack } from "@site/src/refine-theme/tutorial-sandpack"; +import { useSandpack } from "@codesandbox/sandpack-react"; +import { TutorialUpdateFileButton } from "@site/src/refine-theme/tutorial-update-file-button"; + +import { + dependencies, + finalFiles as initialFiles, +} from "../../intro/tanstack-router/sandpack"; +import { removeActiveFromFiles } from "@site/src/utils/remove-active-from-files"; + +export const Sandpack = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); +}; + +// updates + +const AppTsxWithAuthentication = /* tsx */ ` +import { Refine, Authenticated } from "@refinedev/core"; +import routerProvider from "@refinedev/tanstack-router"; + +import { + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, + Navigate, +} from "@tanstack/react-router"; + +import { dataProvider } from "./providers/data-provider"; +import { authProvider } from "./providers/auth-provider"; + +import { ShowProduct } from "./pages/products/show"; +import { EditProduct } from "./pages/products/edit"; +import { ListProducts } from "./pages/products/list"; +import { CreateProduct } from "./pages/products/create"; + +import { Login } from "./pages/login"; +import { Header } from "./components/header"; + +const rootRoute = createRootRoute({ + component: () => ( + + + + ), +}); + +const authenticatedLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + id: "authenticated-layout", + component: () => ( + +
+ + + ), +}); + +const indexRoute = createRoute({ + getParentRoute: () => authenticatedLayoutRoute, + path: "/", + component: ListProducts, +}); + +const loginRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/login", + component: () => ( + }> + {/* We're redirecting the user to "/" if they are authenticated and trying to access the "/login" route */} + + + ), +}); + +const router = createRouter({ + routeTree: rootRoute.addChildren([ + authenticatedLayoutRoute.addChildren([indexRoute]), + loginRoute, + ]), +}); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +export default function App(): JSX.Element { + return ; +} +`.trim(); + +// actions + +export const AddAuthenticationToApp = () => { + const { sandpack } = useSandpack(); + + return ( + { + sandpack.updateFile("/src/App.tsx", AppTsxWithAuthentication); + sandpack.setActiveFile("/src/App.tsx"); + }} + /> + ); +}; + +// files + +export const finalFiles = { + ...removeActiveFromFiles(initialFiles), + "src/App.tsx": { + code: AppTsxWithAuthentication, + active: true, + }, +}; + diff --git a/documentation/tutorial/routing/inferring-parameters/tanstack-router/index.md b/documentation/tutorial/routing/inferring-parameters/tanstack-router/index.md new file mode 100644 index 0000000000000..bd1cee28c752a --- /dev/null +++ b/documentation/tutorial/routing/inferring-parameters/tanstack-router/index.md @@ -0,0 +1,134 @@ +--- +title: Inferring Parameters +--- + +import { Sandpack, AddInferenceToEditProduct, AddInferenceToCreateProduct, AddInferenceToShowProduct, AddInferenceToListProducts } from "./sandpack.tsx"; + + + +Now we've learned about the `useNavigation` hook and how to handle navigation with Refine. In this step, we'll be updating components to benefit from the parameter inference of Refine. + +When integrated with a router provider, Refine infers the parameters from route definitions and incorporates them into its hooks and components, eliminating the need for manual passing of `resource`, `id` and `action` parameters. + +:::tip + +You can always pass the parameters manually if you want to override the inferred parameters. + +::: + +## Updating the `ListProducts` Component + +Let's update our `` component and omit the `resource` parameter from the `useTable` hook. + +Update your `src/pages/products/list.tsx` file by adding the following lines: + +```tsx title="src/pages/products/list.tsx" +import { useTable, useMany } from "@refinedev/core"; + +export const ListProducts = () => { + const { + tableQuery: { isLoading }, + currentPage, + setCurrentPage, + pageCount, + sorters, + setSorters, + } = useTable({ + // removed-line + resource: "products", + pagination: { currentPage: 1, pageSize: 10 }, + sorters: { initial: [{ field: "id", order: "asc" }] }, + }); + + /* ... */ +}; +``` + + + +## Updating the `ShowProduct` Component + +Let's update our `` component and omit the `resource` and `id` parameters. Remember that previously we've hard-coded the `id` parameter. Now we'll be letting Refine to infer the `id` parameter from the route definition and dynamically fetch the product. + +We'll also start using [`useShow`](/core/docs/data/hooks/use-show) hook which is wrapper around `useOne`. Unlike the useOne hook, it offers inference capabilities, eliminating the need to explicitly pass `resource` and `id` parameters + +Update your `src/pages/products/show.tsx` file by adding the following lines: + +```tsx title="src/pages/products/show.tsx" +// highlight-next-line +import { useShow } from "@refinedev/core"; + +export const ShowProduct = () => { + // removed-line + const { isLoading } = useOne({ resource: "products", id: 123 }); + // added-line + const { query } = useShow(); + + /* ... */ +}; +``` + + + +## Updating the `EditProduct` Component + +Let's update our `` component and omit the `resource`, `action` and `id` parameters from the `useForm` hook. Just like the `` component, we'll be letting Refine to infer the `id` parameter from the route definition. Since we've defined the `edit` action in our resource definition, Refine will also infer the `action` parameter as `edit`. + +Update your `src/pages/products/edit.tsx` file by adding the following lines: + +```tsx title="src/pages/products/edit.tsx" +import { useForm, useSelect } from "@refinedev/core"; + +export const EditProduct = () => { + // removed-line + const { onFinish, mutation, query } = useForm({ + // removed-line + action: "edit", + // removed-line + resource: "products", + // removed-line + id: "123", + // removed-line + }); + // added-line + const { onFinish, mutation, query } = useForm(); + + /* ... */ +}; +``` + + + +## Updating the `CreateProduct` Component + +Let's update our `` component and omit the `resource` and `action` parameters from the `useForm` hook. Since we've defined the `create` action in our resource definition, Refine will also infer the `action` parameter as `create`. + +Update your `src/pages/products/create.tsx` file by adding the following lines: + +```tsx title="src/pages/products/create.tsx" +import { useForm, useSelect } from "@refinedev/core"; + +export const CreateProduct = () => { + // removed-line + const { onFinish, mutation } = useForm({ + // removed-line + action: "create", + // removed-line + resource: "products", + // removed-line + }); + // added-line + const { onFinish, mutation } = useForm(); + + /* ... */ +}; +``` + + + +Now you should be able to see that our components are working as expected. We've successfully updated our components to benefit from the parameter inference of Refine. + +In the next step, we'll be learning about how to handle redirects in our app. + + + diff --git a/documentation/tutorial/routing/inferring-parameters/tanstack-router/sandpack.tsx b/documentation/tutorial/routing/inferring-parameters/tanstack-router/sandpack.tsx new file mode 100644 index 0000000000000..a82f998938085 --- /dev/null +++ b/documentation/tutorial/routing/inferring-parameters/tanstack-router/sandpack.tsx @@ -0,0 +1,389 @@ +import React from "react"; +import { TutorialSandpack } from "@site/src/refine-theme/tutorial-sandpack"; +import { useSandpack } from "@codesandbox/sandpack-react"; +import { TutorialUpdateFileButton } from "@site/src/refine-theme/tutorial-update-file-button"; + +import { dependencies } from "../../intro/tanstack-router/sandpack"; +import { finalFiles as initialFiles } from "../../navigation/tanstack-router/sandpack"; +import { removeActiveFromFiles } from "@site/src/utils/remove-active-from-files"; + +export const Sandpack = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); +}; + +// updates + +const ListProductsWithInference = /* tsx */ ` +import { useTable, useMany, useNavigation } from "@refinedev/core"; + +import { Link } from "@tanstack/react-router"; + +export const ListProducts = () => { + const { + result, + tableQuery: { isLoading }, + currentPage, + setCurrentPage, + pageCount, + sorters, + setSorters, + } = useTable({ + pagination: { currentPage: 1, pageSize: 10 }, + sorters: { initial: [{ field: "id", order: "asc" }] }, + }); + + const { showUrl, editUrl } = useNavigation(); + + const { result: categories } = useMany({ + resource: "categories", + ids: result?.data?.map((product) => product.category?.id) ?? [], + }); + + if (isLoading) { + return
Loading...
; + } + + const onPrevious = () => { + if (currentPage > 1) { + setCurrentPage(currentPage - 1); + } + }; + + const onNext = () => { + if (currentPage < pageCount) { + setCurrentPage(currentPage + 1); + } + }; + + const onPage = (page: number) => { + setCurrentPage(page); + }; + + const getSorter = (field: string) => { + const sorter = sorters?.find((sorter) => sorter.field === field); + + if (sorter) { + return sorter.order; + } + }; + + const onSort = (field: string) => { + const sorter = getSorter(field); + setSorters( + sorter === "desc" + ? [] + : [ + { + field, + order: sorter === "asc" ? "desc" : "asc", + }, + ], + ); + }; + + const indicator = { asc: "⬆️", desc: "⬇️" }; + + return ( +
+

Products

+ + + + + + + + + + + + + {result?.data?.map((product) => ( + + + + + + + + + ))} + +
onSort("id")}> + ID {indicator[getSorter("id")]} + onSort("name")}> + Name {indicator[getSorter("name")]} + Category onSort("material")}> + Material {indicator[getSorter("material")]} + onSort("price")}> + Price {indicator[getSorter("price")]} + Actions
{product.id}{product.name} + { + categories?.data?.find( + (category) => category.id == product.category?.id, + )?.title + } + {product.material}{product.price} + Show + Edit +
+
+ +
+ {currentPage - 1 > 0 && ( + onPage(currentPage - 1)}>{currentPage - 1} + )} + {currentPage} + {currentPage + 1 <= pageCount && ( + onPage(currentPage + 1)}>{currentPage + 1} + )} +
+ +
+
+ ); +}; +`.trim(); + +const ShowProductWithInference = /* tsx */ ` +import { useShow } from "@refinedev/core"; + +export const ShowProduct = () => { + const { + result, + query: { isLoading }, + } = useShow(); + + if (isLoading) { + return
Loading...
; + } + + return
Product name: { result?.name }
; +}; +`.trim(); + +const CreateProductWithInference = /* tsx */ ` +import { useForm, useSelect } from "@refinedev/core"; + +export const CreateProduct = () => { + const { onFinish, mutation } = useForm(); + + const { options } = useSelect({ + resource: "categories", + // optionLabel: "title", // Default value is "title" so we don't need to provide it. + // optionValue: "id", // Default value is "id" so we don't need to provide it. + }); + + const onSubmit = (event: React.FormEvent) => { + event.preventDefault(); + // Using FormData to get the form values and convert it to an object. + const data = Object.fromEntries(new FormData(event.target).entries()); + // Calling onFinish to submit with the data we've collected from the form. + onFinish({ + ...data, + price: Number(data.price).toFixed(2), + category: { id: Number(data.category) }, + }); + }; + + return ( +
+ + + + +