Routing
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.
These integrations makes it easier to use Refine with these frameworks and offers a lot of benefits such as:
- Automatic parameter detection in hooks/components.
- Automatic redirections after mutation or authentication.
- Set of utility components & hooks which can be used to navigate between pages/routes.
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 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.
Router Integrations
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 <Refine />
's routerProvider
prop.
- React Router
- Next.js App
- Next.js Pages
- Remix
- Expo Router (React Native)
React Router
import { BrowserRouter, Routes } from "react-router";
import routerProvider from "@refinedev/react-router";
const App = () => (
<BrowserRouter>
<Refine routerProvider={routerProvider}>
<Routes>{/* Your route definitions */}</Routes>
</Refine>
</BrowserRouter>
);
Check out React Router documentation for detailed information
Next.js App
"use client";
import { Refine } from "@refinedev/core";
import routerProvider from "@refinedev/nextjs-router";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Refine
routerProvider={routerProvider}
>
{children}
</Refine>
</body>
</html>
);
}
Next.js Pages
import { Refine } from "@refinedev/core";
import routerProvider from "@refinedev/nextjs-router/pages";
export function MyApp({ Component, pageProps }) {
return (
<Refine
routerProvider={routerProvider}
>
<Component {...pageProps} />
</Refine>
);
}
While using this integration, you won't be missing out Next.js features such as SSR and ISR.
Check out Next.js Router documentation for detailed information
Remix
import routerProvider from "@refinedev/remix-router";
export default function App() {
return (
<html>
<body>
<Refine
routerProvider={routerProvider}
>
<Outlet />
</Refine>
</body>
</html>
);
}
While using this integration, you won't be missing out Remix features such as SSR and ISR.
Check out Remix Router documentation for detailed information
Expo Router (React Native)
import { Refine } from "@refinedev/core";
import routerProvider from "@refinenative/expo-router";
export const App = () => (
<Refine
routerProvider={routerProvider}
>
{/* ... */}
</Refine>
);
Refine is able to work on React Native apps and with the help of the community package @refinenative/expo-router
, you can use Refine's routing features on React Native as well.
Check out Expo Router (Community Package) documentation for detailed information
Once you passed router provider to <Refine />
component, you can use all the features of Refine in a same way, regardless of your application's framework/router.
Relationship Between Resources and Routes
Refine can infer current resource
, action
and it's id
from the current route based on your resource definitions.
This eliminates the need of passing these parameters to the components/hooks manually.
All you have to do is to define your resource and their routes.
<Refine
resources={[
{
name: "products",
list: "/my-products", // http://localhost:3000/my-products
show: "my-products/:id", // http://localhost:3000/my-products/1
create: "/my-products/new", // http://localhost:3000/my-products/new
edit: "/my-products/:id/edit", // http://localhost:3000/my-products/1/edit
clone: "/my-products/:id/clone", // http://localhost:3000/my-products/1/clone
},
]}
/>
You can see how we omit resource and id parameters for useList
, and useShow
hooks in the examples below.
React Router
Code Example
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/react-router@latest,react-router@^7.0.2
Content: import React from "react"; import { Refine } from "@refinedev/core"; import routerProvider from "@refinedev/react-router"; import dataProvider from "@refinedev/simple-rest"; import { BrowserRouter, Route, Routes } from "react-router"; import "./style.css"; import { ProductList } from "./pages/products/list.tsx"; import { ProductShow } from "./pages/products/show.tsx"; export default function App() { return ( <BrowserRouter> <Refine routerProvider={routerProvider} dataProvider={dataProvider("https://api.fake-rest.refine.dev")} resources={[ { name: "products", // We're defining the routes and assigning them to an action of a resource list: "/my-products", show: "/my-products/:id", // For sake of simplicity, we are not defining other routes here but the implementation is the same // create: "/my-products/new", // edit: "/my-products/:id/edit", // clone: "/my-products/:id/clone", }, ]} > <Routes> <Route path="/my-products" element={<ProductList />} /> <Route path="/my-products/:id" element={<ProductShow />} /> </Routes> </Refine> </BrowserRouter> ); }
Content: 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; }
Content: 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 { data, isLoading } = useList(); const go = useGo(); if (isLoading) return <div>Loading...</div>; return ( <ul> {data?.data?.map((product) => ( <li key={product.id}> <span>{product.name}</span> <button onClick={() => { go({ to: { resource: "products", action: "show", id: product.id, }, }); }} > show </button> </li> ))} </ul> ); };
Content: 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 { queryResult: { data, isLoading }, } = result; const go = useGo(); if (isLoading) return <div>Loading...</div>; return ( <> <div> <h1>{data?.data?.name}</h1> <p>Material: {data?.data?.material}</p> <small>ID: {data?.data?.id}</small> </div> <button onClick={() => { go({ to: { resource: "products", action: "list", }, }); }} > Go to Products list </button> </> ); };
Next.js
Code Example
Dependencies:
Content: import React from "react"; import { Refine } from "@refinedev/core"; import routerProvider from "@refinedev/nextjs-router/pages"; import dataProvider from "@refinedev/simple-rest"; import type { AppProps } from "next/app"; import "../style.css"; function App({ Component, pageProps }: AppProps) { return ( <Refine routerProvider={routerProvider} dataProvider={dataProvider("https://api.fake-rest.refine.dev")} resources={[ { name: "products", // We're defining the routes and assigning them to an action of a resource list: "/my-products", show: "/my-products/:id", // For sake of simplicity, we are not defining other routes here but the implementation is the same // create: "/my-products/new", // edit: "/my-products/:id/edit", // clone: "/my-products/:id/clone", }, ]} > <Component {...pageProps} /> </Refine> ); } export default App;
Content: 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; }
Content: import React from "react"; import { useGo, useList } from "@refinedev/core"; const ProductList = () => { // We're inferring the resource from the route // So we call `useList` hook without any arguments. // const { ... } = useList({ resource: "products" }) const { data, isLoading } = useList(); const go = useGo(); if (isLoading) return <div>Loading...</div>; return ( <ul> {data?.data?.map((product) => ( <li key={product.id}> <span>{product.name}</span> <button onClick={() => { go({ to: { resource: "products", action: "show", id: product.id, }, }); }} > show </button> </li> ))} </ul> ); }; export default ProductList;
Content: import React from "react"; import { useGo, useShow } from "@refinedev/core"; const ProductShow = () => { // 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 { queryResult: { data, isLoading }, } = result; const go = useGo(); if (isLoading) return <div>Loading...</div>; return ( <> <div> <h1>{data?.data?.name}</h1> <p>Material: {data?.data?.material}</p> <small>ID: {data?.data?.id}</small> </div> <button onClick={() => { go({ to: { resource: "products", action: "list" } }); }} > Go to Products list </button> </> ); }; export default ProductShow;
Usage with App Router
You can see the example here: https://github.com/refinedev/refine/tree/main/examples/with-nextjs
Remix
Code Example
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/remix-router@latest,react-router@^7.0.2
Content: import React from "react"; import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; import { Refine } from "@refinedev/core"; import routerProvider from "@refinedev/remix-router"; import dataProvider from "@refinedev/simple-rest"; export default function App() { return ( <html lang="en"> <head> <Meta /> <Links /> </head> <body> <Refine routerProvider={routerProvider} dataProvider={dataProvider("https://api.fake-rest.refine.dev")} resources={[ { name: "products", // We're defining the routes and assigning them to an action of a resource list: "/my-products", show: "/my-products/:id", // For sake of simplicity, we are not defining other routes here but the implementation is the same // create: "/my-products/new", // edit: "/my-products/:id/edit", // clone: "/my-products/:id/clone", }, ]} > <Outlet /> </Refine> <ScrollRestoration /> <Scripts /> <LiveReload /> </body> </html> ); }
Content: import { useGo, useList } from "@refinedev/core"; import React from "react"; const ProductList = () => { // We're inferring the resource from the route // So we call `useList` hook without any arguments. // const { ... } = useList({ resource: "products" }) const { data, isLoading } = useList(); const go = useGo(); if (isLoading) return <div>Loading...</div>; return ( <ul> {data?.data?.map((product) => ( <li key={product.id}> <span>{product.name}</span> <button onClick={() => { go({ to: { resource: "products", action: "show", id: product.id, }, }); }} > show </button> </li> ))} </ul> ); }; export default ProductList;
Content: import React from "react"; import { useGo, useShow } from "@refinedev/core"; const ProductShow = () => { // 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 { queryResult: { data, isLoading }, } = result; const go = useGo(); if (isLoading) return <div>Loading...</div>; return ( <> <div> <h1>{data?.data?.name}</h1> <p>Material: {data?.data?.material}</p> <small>ID: {data?.data?.id}</small> </div> <button onClick={() => { go({ to: { resource: "products", action: "list" } }); }} > Go to Products list </button> </> ); }; export default ProductShow;
Hook Integrations
useForm
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.
Code Example
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/react-router@latest,react-router@^7.0.2
Content: import React from "react"; import { Refine } from "@refinedev/core"; import routerProvider from "@refinedev/react-router"; import dataProvider from "@refinedev/simple-rest"; import { BrowserRouter, Route, Routes } from "react-router"; import "./style.css"; import { ProductEdit } from "./edit.tsx"; import { ProductList } from "./list.tsx"; export default function App() { return ( <BrowserRouter> <Refine routerProvider={routerProvider} dataProvider={dataProvider("https://api.fake-rest.refine.dev")} resources={[ { name: "products", list: "/my-products", edit: "/my-products/:id/edit", }, ]} > <Routes> <Route path="/my-products" element={<ProductList />} /> <Route path="/my-products/:id/edit" element={<ProductEdit />} /> </Routes> </Refine> </BrowserRouter> ); }
Content: 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; }
Content: import React from "react"; import { useGo, useList } from "@refinedev/core"; export const ProductList: React.FC = () => { const { data, isLoading } = useList(); const go = useGo(); if (isLoading) return <div>Loading...</div>; return ( <ul> {data?.data?.map((product) => ( <li key={product.id}> <span>{product.name}</span> <button onClick={() => { go({ to: { resource: "products", action: "edit", id: product.id, }, }); }} > edit </button> </li> ))} </ul> ); };
Content: 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 ( <div> <br /> <form onSubmit={onSubmit}> <div> <label htmlFor="name">name</label> <input type="text" id="name" name="name" placeholder="name" defaultValue={defaultValues?.name} /> </div> <button type="submit" disabled={formLoading}> <span>Save</span> </button> </form> </div> ); };
Additionally, router integrations exposes an <UnsavedChangesNotifier />
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.
- React Router
- Next.js
- Remix
React Router
import { Refine } from "@refinedev/core";
import {
routerProvider,
UnsavedChangesNotifier,
} from "@refinedev/react-router";
import { BrowserRouter, Routes } from "react-router";
const App = () => (
<BrowserRouter>
<Refine
// ...
routerProvider={routerProvider}
options={{
warnWhenUnsavedChanges: true,
}}
>
<Routes>{/* ... */}</Routes>
{/* The `UnsavedChangesNotifier` component should be placed under <Refine /> component. */}
<UnsavedChangesNotifier />
</Refine>
</BrowserRouter>
);
Check out the UnsavedChangesNotifier
section of the React Router integration documentation for more information.
Next.js
import type { AppProps } from "next/app";
import { Refine } from "@refinedev/core";
import {
routerProvider,
UnsavedChangesNotifier,
} from "@refinedev/nextjs-router/pages";
export default function App({ Component, pageProps }) {
return (
<Refine
// ...
routerProvider={routerProvider}
options={{
warnWhenUnsavedChanges: true,
}}
>
<Component {...pageProps} />
{/* The `UnsavedChangesNotifier` component should be placed under <Refine /> component. */}
<UnsavedChangesNotifier />
</Refine>
);
}
Check out the UnsavedChangesNotifier
section of the React Router integration documentation for more information.
Remix
import type { MetaFunction } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import { Refine } from "@refinedev/core";
import routerProvider, {
UnsavedChangesNotifier,
} from "@refinedev/remix-router";
export default function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Refine
// ...
routerProvider={routerProvider}
options={{
warnWhenUnsavedChanges: true,
}}
>
<Outlet />
<UnsavedChangesNotifier />
</Refine>
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
Check out the UnsavedChangesNotifier
section of the React Router integration documentation for more information.
useTable
useTable
can synchronize it's parameters (filters, pagination, sorting) with the current route.
To enable synchronization, you need to pass syncWithLocation: true
to <Refine />
component's options
prop.
<Refine {...} options={{ syncWithLocation: true }}>
Once you pass syncWithLocation: true
to <Refine />
component's options
prop, useTable
will:
- Read the current route and update it's parameters (filters, pagination, sorting) accordingly.
- Update the current route when it's parameters (filters, pagination, sorting) change.
Let's say we have a products
list page with the following route:
/my-products
And we want to filter products by category.id
and sort them by id
in asc
order.
We can pass these parameters to useTable
hook as follows:
const { ... } = useTable(
{
current: 1,
pageSize: 2,
filters: { initial: [{ field: "category.id", operator: "eq", value: 1 }]},
sorters: { initial: [{ field: "id", direction: "asc" }] }
}
);
useTable
will automatically update the route to:
/my-products
/my-products?current=1&pageSize=2&sorters[0][field]=id&sorters[0][order]=asc&filters[0][field]=category.id&filters[0][operator]=eq&filters[0][value]=1
And you will see a list of products, already filtered, sorted and paginated automatically based on the query parameters of the current route.
const { tableQuery, current, pageSize, filters, sorters } = useTable();
console.log(tableQuery.data.data); // [{...}, {...}]
console.log(tableQuery.data.total); // 32 - total number of unpaginated records
console.log(current); // 1 - current page
console.log(pageSize); // 2 - page size
console.log(filters); // [{ field: "category.id", operator: "eq", value: "1" }]
console.log(sorters); // [{ field: "id", order: "asc" }]
Check the examples below to see how you can use useTable
with router integration.
Notice how components/products/list.tsx
is the same, regardless of the router integration.
React Router
Code Example
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/react-router@latest,react-router@^7.0.2
Content: import React from "react"; import { Refine } from "@refinedev/core"; import routerProvider from "@refinedev/react-router"; import dataProvider from "@refinedev/simple-rest"; import { BrowserRouter, Route, Routes } from "react-router"; import "./style.css"; import { ListPage } from "./pages/products/list.tsx"; export default function App() { return ( <BrowserRouter> <Refine routerProvider={routerProvider} dataProvider={dataProvider("https://api.fake-rest.refine.dev")} resources={[ { name: "products", list: "/my-products", }, ]} options={{ syncWithLocation: true }} > <Routes> <Route path="/my-products" element={<ListPage />} /> </Routes> </Refine> </BrowserRouter> ); }
Content: 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; }
Content: import React from "react"; import { useTable } from "@refinedev/core"; import { ProductList } from "../../components/products/list"; export const ListPage: React.FC = () => { const tableProps = useTable({ pagination: { current: 1, pageSize: 2 }, filters: { initial: [{ field: "category.id", operator: "eq", value: "1" }], }, sorters: { initial: [{ field: "id", order: "asc" }] }, }); return <ProductList tableProps={tableProps} />; };
Content: import React from "react"; export const ProductList: React.FC = ({ tableProps }) => { const { tableQuery, isLoading, current, setCurrent, pageSize, pageCount, filters, setFilters, sorters, setSorters, } = tableProps; if (isLoading) return <div>Loading...</div>; return ( <div> <h3>Products</h3> <table style={{ border: "1px solid black" }}> <thead> <tr key="header"> <td>id</td> <td>name</td> <td>categoryId</td> </tr> </thead> <tbody> {tableQuery.data?.data?.map((record) => ( <tr key={record.id}> <td>{record.id}</td> <td>{record.name}</td> <td>{record.category.id}</td> </tr> ))} </tbody> </table> <hr /> Sorting by field: <b> {sorters[0].field}, order {sorters[0].order} </b> <br /> <button onClick={() => { setSorters([ { field: "id", order: sorters[0].order === "asc" ? "desc" : "asc", }, ]); }} > Toggle Sort </button> <hr /> Filtering by field: <b> {filters[0].field}, operator {filters[0].operator}, value {filters[0].value} </b> <br /> <button onClick={() => { setFilters([ { field: "category.id", operator: "eq", value: filters[0].value === "1" ? "2" : "1", }, ]); }} > Toggle Filter </button> <hr /> <p>Current Page: {current}</p> <p>Page Size: {pageSize}</p> <button onClick={() => { setCurrent(current - 1); }} disabled={current < 2} > Previous Page </button> <button onClick={() => { setCurrent(current + 1); }} disabled={current === pageCount} > Next Page </button> </div> ); };
Next.js
You can use SSR feature with Next.js to fetch initial data on the server side.
Code Example
Dependencies:
Content: import React from "react"; import { Refine } from "@refinedev/core"; import routerProvider from "@refinedev/nextjs-router/pages"; import dataProvider from "@refinedev/simple-rest"; import type { AppProps } from "next/app"; function App({ Component, pageProps }: AppProps) { return ( <Refine routerProvider={routerProvider} dataProvider={dataProvider("https://api.fake-rest.refine.dev")} resources={[ { name: "products", list: "/my-products", }, ]} options={{ syncWithLocation: true }} > <Component {...pageProps} /> </Refine> ); } export default App;
Content: import React from "react"; import { useTable } from "@refinedev/core"; import { parseTableParams } from "@refinedev/nextjs-router/pages"; import dataProvider from "@refinedev/simple-rest"; import { ProductList } from "../../components/products/list"; export const getServerSideProps = async (context) => { const { pagination: queryPagination, filters: queryFilters, sorters: querySorters, } = parseTableParams(context.resolvedUrl?.split("?")[1] ?? ""); const pagination = { current: queryPagination.current ?? 1, pageSize: queryPagination.pageSize ?? 2, }; const filters = queryFilters ?? [ { field: "category.id", operator: "eq", value: "1", }, ]; const sorters = querySorters ?? [{ field: "id", order: "asc" }]; const data = await dataProvider("https://api.fake-rest.refine.dev").getList({ resource: "products", filters, pagination, sorters, }); return { props: { initialData: data, initialProps: { pagination, filters, sorters }, }, }; }; const ProductListPage = (props) => { const { initialData, initialProps: { filters, sorters, pagination }, } = props; const tableProps = useTable({ queryOptions: { initialData }, filters: { initial: filters }, sorters: { initial: sorters }, pagination, }); return <ProductList tableProps={tableProps} />; }; export default ProductListPage;
Content: import React from "react"; export const ProductList: React.FC = ({ tableProps }) => { const { tableQuery, isLoading, current, setCurrent, pageSize, pageCount, filters, setFilters, sorters, setSorters, } = tableProps; if (isLoading) return <div>Loading...</div>; return ( <div> <h3>Products</h3> <table style={{ border: "1px solid black" }}> <thead> <tr key="header"> <td>id</td> <td>name</td> <td>categoryId</td> </tr> </thead> <tbody> {tableQuery.data?.data?.map((record) => ( <tr key={record.id}> <td>{record.id}</td> <td>{record.name}</td> <td>{record.category.id}</td> </tr> ))} </tbody> </table> <hr /> Sorting by field: <b> {sorters[0]?.field}, order {sorters[0]?.order} </b> <br /> <button onClick={() => { setSorters([ { field: "id", order: sorters[0]?.order === "asc" ? "desc" : "asc", }, ]); }} > Toggle Sort </button> <hr /> Filtering by field: <b> {filters[0]?.field}, operator {filters[0]?.operator}, value:{" "} {filters[0]?.value} </b> <br /> <button onClick={() => { setFilters([ { field: "category.id", operator: "eq", value: filters[0]?.value === "1" ? "2" : "1", }, ]); }} > Toggle Filter </button> <hr /> <p>Current Page: {current}</p> <p>Page Size: {pageSize}</p> <button onClick={() => { setCurrent(+current - 1); }} disabled={+current < 2} > Previous Page </button> <button onClick={() => { setCurrent(+current + 1); }} disabled={current === pageCount} > Next Page </button> </div> ); };
Remix
You can use SSR feature with Remix to fetch initial data on the server side.
Code Example
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/remix-router@latest
Content: import React from "react"; import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; import { Refine } from "@refinedev/core"; import routerProvider from "@refinedev/remix-router"; import dataProvider from "@refinedev/simple-rest"; export default function App() { return ( <html lang="en"> <head> <Meta /> <Links /> </head> <body> <Refine routerProvider={routerProvider} dataProvider={dataProvider("https://api.fake-rest.refine.dev")} resources={[ { name: "products", list: "/my-products", }, ]} > <Outlet /> </Refine> <ScrollRestoration /> <Scripts /> <LiveReload /> </body> </html> ); }
Content: import React from "react"; export const ProductList: React.FC = ({ tableProps }) => { const { tableQuery, isLoading, current, setCurrent, pageSize, pageCount, filters, setFilters, sorters, setSorters, } = tableProps; if (isLoading) return <div>Loading...</div>; return ( <div> <h3>Products</h3> <table style={{ border: "1px solid black" }}> <thead> <tr key="header"> <td>id</td> <td>name</td> <td>categoryId</td> </tr> </thead> <tbody> {tableQuery.data?.data?.map((record) => ( <tr key={record.id}> <td>{record.id}</td> <td>{record.name}</td> <td>{record.category.id}</td> </tr> ))} </tbody> </table> <hr /> Sorting by field: <b> {sorters[0]?.field}, order {sorters[0]?.order} </b> <br /> <button onClick={() => { setSorters([ { field: "id", order: sorters[0]?.order === "asc" ? "desc" : "asc", }, ]); }} > Toggle Sort </button> <hr /> Filtering by field: <b> {filters[0]?.field}, operator {filters[0]?.operator}, value:{" "} {filters[0]?.value} </b> <br /> <button onClick={() => { setFilters([ { field: "category.id", operator: "eq", value: filters[0]?.value === "1" ? "2" : "1", }, ]); }} > Toggle Filter </button> <hr /> <p>Current Page: {current}</p> <p>Page Size: {pageSize}</p> <button onClick={() => { setCurrent(+current - 1); }} disabled={+current < 2} > Previous Page </button> <button onClick={() => { setCurrent(+current + 1); }} disabled={current === pageCount} > Next Page </button> </div> ); };
Content: import React from "react"; import { json, LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { useTable } from "@refinedev/core"; import { parseTableParams } from "@refinedev/remix-router"; import dataProvider from "@refinedev/simple-rest"; import { ProductList } from "../components/products/list"; export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); const { pagination: queryPagination, filters: queryFilters, sorters: querySorters, } = parseTableParams(url.search); const pagination = { current: queryPagination.current ?? 1, pageSize: queryPagination.pageSize ?? 2, }; const filters = queryFilters ?? [ { field: "category.id", operator: "eq", value: "1", }, ]; const sorters = querySorters ?? [{ field: "id", order: "asc" }]; const data = await dataProvider("https://api.fake-rest.refine.dev").getList({ resource: "products", filters, pagination, sorters, }); return json({ initialData: data, initialProps: { pagination, filters, sorters }, }); } const ProductList = () => { const { initialData, initialProps: { filters, sorters, pagination }, } = useLoaderData<typeof loader>(); const tableProps = useTable({ queryOptions: { initialData }, filters: { initial: filters }, sorters: { initial: sorters }, pagination, }); return <ProductList tableProps={tableProps} />; }; export default ProductList;
useModalForm
useModalForm
can automatically detect resource
parameter from the current route.
It can also sync it's parameters with the current route.
const { ... } = useModalForm({ syncWithLocation: true })
Once the modal is visible, current route will look like this:
/my-products?modal-products-edit[open]=true&modal-products-edit[id]=1
You can see the example below for usage.
Code Example
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/react-router@latest,@refinedev/react-hook-form@latest,react-router@^7.0.2
Content: import React from "react"; import { Refine } from "@refinedev/core"; import routerProvider from "@refinedev/react-router"; import dataProvider from "@refinedev/simple-rest"; import { BrowserRouter, Route, Routes } from "react-router"; import "./style.css"; import { ProductList } from "./pages/products/list.tsx"; export default function App() { return ( <BrowserRouter> <Refine routerProvider={routerProvider} dataProvider={dataProvider("https://api.fake-rest.refine.dev")} resources={[ { name: "products", list: "/my-products", }, ]} > <Routes> <Route path="/my-products" element={<ProductList />} /> </Routes> </Refine> </BrowserRouter> ); }
Content: 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; }
Content: import React from "react"; export const Modal: React.FC = ({ isOpen, onClose, children }) => { if (!isOpen) return null; return ( <> <div className="overlay" onClick={onClose}></div> <div className="modal"> <div className="modal-title"> <button className="close-button" onClick={onClose}> × </button> </div> <div className="modal-content">{children}</div> </div> </> ); };
Content: 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 { data, isLoading } = useList(); const { modal: { visible, close, show }, refineCore: { onFinish, formLoading }, handleSubmit, register, saveButtonProps, } = useModalForm({ refineCoreProps: { action: "edit" }, syncWithLocation: true, }); if (isLoading) return <div>Loading...</div>; return ( <> <Modal isOpen={visible} onClose={close}> <form onSubmit={handleSubmit(onFinish)}> <div> <label htmlFor="name">name</label> <input {...register("name")} /> </div> <button type="submit" {...saveButtonProps}> <span>Save</span> </button> </form> </Modal> <ul> {data?.data?.map((product) => ( <li key={product.id}> <span>{product.name}</span> <button onClick={() => { show(product.id); }} > edit </button> </li> ))} </ul> </> ); };
useOne
useOne
can automatically detect resource
and id
parameters from the current route.
import { useOne } from "@refinedev/core";
const { data: productResponse } = useOne({ resource: "products", id: "1" });
console.log(productResponse.data); // { id: "1", title: "Product 1", ... }
const { data: productResponse } = useOne();
console.log(productResponse.data); // { id: "1", title: "Product 1", ... }
useShow
useShow
can automatically detect resource
and id
parameters from the current route.
import { useShow } from "@refinedev/core";
const { queryResult: showResponse } = useShow({
resource: "products",
id: "1",
});
console.log(showResponse.data.data); // { id: "1", title: "Product 1", ... }
const { queryResult: showResponse } = useShow();
console.log(showResponse.data.data); // { id: "1", title: "Product 1", ... }
useList
useList
can automatically detect resource
parameter from the current route.
import { useList } from "@refinedev/core";
const { data: listResponse } = useList({ resource: "products" });
console.log(listResponse.data); // [{ id: "1", title: "Product 1", ... }, { id: "2", title: "Product 2", ... }]
console.log(listResponse.total); // 32 - total number of unpaginated records
const { data: listResponse } = useList();
console.log(listResponse.data); // [{ id: "1", title: "Product 1", ... }, { id: "2", title: "Product 2", ... }]
console.log(listResponse.total); // 32 - total number of unpaginated records
CAUTION
config.pagination
, config.filters
, config.sorters
will not be automatically detected from the current route.
The routerProvider
Interface
A router integration of Refine consists of a set of basic implementations for:
- Ability to navigate between pages/routes
- An interface to interact with the parameters and query strings of the current route
- An utility to navigate back in the history
- A simple component to use for anchor tags
These implementations will be provided via routerProvider
which expects an object with the following methods:
go
: A function that accepts an object and returns a function that handles the navigation.back
: A function that returns a function that handles the navigation back in the history.parse
: A function that returns a function that parses the current route and returns an object.Link
: A React component that accepts ato
prop and renders a component that handles the navigation to the givento
prop.
While all these methods are optional, if you're working on creating a custom router integration, you'll be able to incrementally add more features and adopt more of Refine's features by implementing more of these methods.
To learn more about the routerProvider
interface, check out the Router Provider
section of the Core API Reference.