
Refine Dev
- 188 installs
- 22 repo stars
- Updated August 1, 2026
- itechmeat/llm-code
Spin up Refine admin panels and CRUD apps with auth, data providers, routing, and resource views connected to REST or GraphQL backends in days not weeks.
About
Teaches agents to scaffold Refine-based admin panels and CRUD applications with authentication, data providers, routing, and resource views wired to REST or GraphQL backends for fast SaaS internals.
- Refine admin and CRUD scaffolding
- Auth and role-based access patterns
- REST and GraphQL data providers
- Resource list, create, edit, show views
- Rapid internal tool delivery
Refine Dev by the numbers
- 188 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #874 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/itechmeat/llm-code --skill refine-devAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 188 |
|---|---|
| repo stars | ★ 22 |
| Last updated | August 1, 2026 |
| Repository | itechmeat/llm-code ↗ |
What it does
Spin up Refine admin panels and CRUD apps with auth, data providers, routing, and resource views connected to REST or GraphQL backends in days not weeks.
Files
Refine.dev Framework
Refine is a headless React framework for building enterprise CRUD applications. It provides data fetching, routing, authentication, and access control out of the box while remaining UI-agnostic.
Core Concepts
Refine is built around these key abstractions:
1. Data Provider — adapter for your backend (REST, GraphQL, etc.) 2. Resources — entities in your app (e.g., posts, users, products) 3. Hooks — useList, useOne, useCreate, useUpdate, useDelete, useForm, useTable 4. Auth Provider — handles login, logout, permissions 5. Router Provider — integrates with React Router, etc.
Quick Start (Vite)
Scaffold: npm create refine-app@latest (select Vite, Mantine, REST API). For manual setup, install @refinedev/core @refinedev/mantine @refinedev/react-router and Mantine packages.
Minimal App Structure
// src/App.tsx
import { Refine } from "@refinedev/core";
import { MantineProvider } from "@mantine/core";
import routerProvider from "@refinedev/react-router";
import dataProvider from "@refinedev/simple-rest";
import { BrowserRouter, Routes, Route } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<MantineProvider>
<Refine
dataProvider={dataProvider("https://api.example.com")}
routerProvider={routerProvider}
resources={[
{
name: "posts",
list: "/posts",
create: "/posts/create",
edit: "/posts/edit/:id",
show: "/posts/show/:id",
},
]}
>
<Routes>{/* Your routes here */}</Routes>
</Refine>
</MantineProvider>
</BrowserRouter>
);
}Critical Prohibitions
- Do NOT mix multiple UI libraries (pick Mantine and stick with it)
- Do NOT bypass data provider — always use Refine hooks for data operations
- Do NOT hardcode API URLs — use data provider configuration
- Do NOT skip resource definition — all CRUD entities must be declared in
resources - Do NOT ignore TypeScript types — Refine is fully typed, leverage it
Steps for New Feature
1. Define the resource in <Refine resources={[...]}> 2. Create page components (List, Create, Edit, Show) 3. Set up routes matching resource paths 4. Use appropriate hooks (useTable for lists, useForm for create/edit) 5. Configure auth provider if authentication is needed
Definition of Done
- [ ] Resource defined in Refine configuration
- [ ] All CRUD pages implemented with proper hooks
- [ ] Routes match resource configuration
- [ ] TypeScript types for resource data defined
- [ ] Error handling in place
- [ ] Loading states handled
Release Note (5.0.12)
useListanduseTablenow preserve custom fields returned bygetList, which matters when your data provider returns extra aggregate or pagination metadata alongside the row list.
References (Detailed Guides)
Core
- data-providers.md — Data provider interface, available providers, custom implementation
- resources.md — Resource definition and configuration
- routing.md — React Router integration and route patterns
Hooks
- hooks.md — All hooks: useList, useOne, useCreate, useUpdate, useDelete, useForm, useTable, useSelect
Security & Auth
- auth.md — Auth provider, access control, RBAC/ABAC, Casbin/CASL integration
UI & Components
- mantine-ui.md — Mantine components integration
- inferencer.md — Auto-generate CRUD pages from API schema
Utilities & Features
- notifications.md — Notification provider and useNotification hook
- i18n.md — Internationalization with i18nProvider
- realtime.md — LiveProvider for websocket/realtime subscriptions
Links
````markdown
Authentication & Access Control
Auth Provider Interface
import type { AuthProvider } from "@refinedev/core";
const authProvider: AuthProvider = {
// Required
login: async ({ email, password }) => AuthActionResponse,
logout: async () => AuthActionResponse,
check: async () => CheckResponse,
onError: async (error) => OnErrorResponse,
// Optional
register: async (params) => AuthActionResponse,
forgotPassword: async (params) => AuthActionResponse,
updatePassword: async (params) => AuthActionResponse,
getPermissions: async () => unknown,
getIdentity: async () => unknown,
};JWT Auth Implementation
import { AuthProvider } from "@refinedev/core";
import axios from "axios";
const API_URL = "https://api.example.com";
// Add token to all requests
axios.interceptors.request.use((config) => {
const token = localStorage.getItem("access_token");
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
export const authProvider: AuthProvider = {
login: async ({ email, password }) => {
try {
const { data } = await axios.post(`${API_URL}/auth/login`, { email, password });
localStorage.setItem("access_token", data.accessToken);
localStorage.setItem("refresh_token", data.refreshToken);
return { success: true, redirectTo: "/" };
} catch (error: any) {
return {
success: false,
error: { name: "LoginError", message: error.response?.data?.message || "Login failed" },
};
}
},
logout: async () => {
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
return { success: true, redirectTo: "/login" };
},
check: async () => {
const token = localStorage.getItem("access_token");
if (!token) return { authenticated: false, redirectTo: "/login" };
try {
await axios.get(`${API_URL}/auth/me`);
return { authenticated: true };
} catch {
return { authenticated: false, logout: true, redirectTo: "/login" };
}
},
onError: async (error) => {
if (error.response?.status === 401) {
const refreshToken = localStorage.getItem("refresh_token");
if (refreshToken) {
try {
const { data } = await axios.post(`${API_URL}/auth/refresh`, { refreshToken });
localStorage.setItem("access_token", data.accessToken);
return {};
} catch {
return { logout: true, redirectTo: "/login" };
}
}
return { logout: true, redirectTo: "/login" };
}
return { error };
},
getIdentity: async () => {
try {
const { data } = await axios.get(`${API_URL}/auth/me`);
return data;
} catch { return null; }
},
getPermissions: async () => {
try {
const { data } = await axios.get(`${API_URL}/auth/me`);
return data.roles;
} catch { return null; }
},
};Auth Hooks
import { useLogin, useLogout, useIsAuthenticated, useGetIdentity, usePermissions } from "@refinedev/core";
// Login
const { mutate: login, isLoading } = useLogin();
login({ email, password }, { onSuccess: () => {}, onError: (e) => {} });
// Logout
const { mutate: logout } = useLogout();
// Check auth
const { data: { authenticated } } = useIsAuthenticated();
// Get user info
const { data: user } = useGetIdentity<{ id: number; name: string }>();
// Get permissions
const { data: permissions } = usePermissions();Protected Routes
import { Authenticated } from "@refinedev/core";
import { Navigate, Outlet, Routes, Route } from "react-router-dom";
<Routes>
{/* Public */}
<Route path="/login" element={<AuthPage type="login" />} />
<Route path="/register" element={<AuthPage type="register" />} />
{/* Protected */}
<Route element={
<Authenticated key="protected" fallback={<Navigate to="/login" />}>
<Layout><Outlet /></Layout>
</Authenticated>
}>
<Route path="/" element={<Dashboard />} />
<Route path="/posts" element={<PostList />} />
</Route>
</Routes>Auth Pages (Mantine)
import { AuthPage } from "@refinedev/mantine";
<AuthPage
type="login" // "login" | "register" | "forgotPassword" | "updatePassword"
title="Welcome"
rememberMe={true}
forgotPasswordLink="/forgot-password"
registerLink="/register"
/>---
Access Control Provider
import type { AccessControlProvider } from "@refinedev/core";
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action, params }) => {
// Return { can: true } or { can: false, reason: "..." }
return { can: true };
},
options: {
buttons: {
enableAccessControl: true,
hideIfUnauthorized: false, // Hide vs disable unauthorized buttons
},
},
};Role-Based Access Control (RBAC)
const roles: Record<string, string[]> = {
admin: ["*"],
editor: ["posts.list", "posts.show", "posts.create", "posts.edit"],
viewer: ["posts.list", "posts.show"],
};
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action }) => {
const user = JSON.parse(localStorage.getItem("user") || "{}");
const permissions = roles[user.role || "viewer"] || [];
if (permissions.includes("*")) return { can: true };
const key = `${resource}.${action}`;
return {
can: permissions.includes(key),
reason: permissions.includes(key) ? undefined : `Cannot ${action} ${resource}`,
};
},
};Attribute-Based Access (ABAC)
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action, params }) => {
const user = JSON.parse(localStorage.getItem("user") || "{}");
// Users can only edit their own posts
if (resource === "posts" && action === "edit") {
const postAuthorId = params?.id ? await getPostAuthor(params.id) : null;
if (postAuthorId !== user.id && user.role !== "admin") {
return { can: false, reason: "You can only edit your own posts" };
}
}
// Premium content check
if (params?.resource?.meta?.premium && !user.isPremium) {
return { can: false, reason: "Premium subscription required" };
}
return { can: true };
},
};useCan Hook
import { useCan } from "@refinedev/core";
const { data: canEdit } = useCan({ resource: "posts", action: "edit", params: { id: 1 } });
const { data: canDelete } = useCan({ resource: "posts", action: "delete", params: { id: 1 } });
{canEdit?.can && <EditButton />}
{!canEdit?.can && <span>{canEdit?.reason}</span>}CanAccess Component
import { CanAccess } from "@refinedev/core";
<CanAccess resource="posts" action="create" fallback={<div>No access</div>}>
<CreateButton />
</CanAccess>Button Auto-Checks
Refine buttons automatically check access control:
| Button | Check |
|---|---|
<ListButton /> | { resource, action: "list" } |
<CreateButton /> | { resource, action: "create" } |
<EditButton recordItemId={id} /> | { resource, action: "edit", params: { id } } |
<DeleteButton recordItemId={id} /> | { resource, action: "delete", params: { id } } |
<ShowButton recordItemId={id} /> | { resource, action: "show", params: { id } } |
// Hide instead of disable
<EditButton recordItemId={1} accessControl={{ hideIfUnauthorized: true }} />Casbin Integration
import { newEnforcer } from "casbin";
let enforcer: Awaited<ReturnType<typeof newEnforcer>>;
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action }) => {
if (!enforcer) enforcer = await newEnforcer("model.conf", "policy.csv");
const user = JSON.parse(localStorage.getItem("user") || "{}");
const can = await enforcer.enforce(user.role, resource, action);
return { can, reason: can ? undefined : "Forbidden by policy" };
},
};CASL Integration
import { createMongoAbility, AbilityBuilder } from "@casl/ability";
const defineAbilityFor = (user: { role: string }) => {
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
if (user.role === "admin") can("manage", "all");
else if (user.role === "editor") {
can("read", "all");
can(["create", "update"], "Post");
cannot("delete", "Post");
} else {
can("read", "all");
}
return build();
};
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action }) => {
const user = JSON.parse(localStorage.getItem("user") || "{}");
const ability = defineAbilityFor(user);
const caslAction = { list: "read", show: "read", create: "create", edit: "update", delete: "delete" }[action] || action;
const can = ability.can(caslAction, resource);
return { can, reason: can ? undefined : `Cannot ${action} ${resource}` };
},
};Performance
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action }) => { /* ... */ },
options: {
queryOptions: {
staleTime: 5 * 60 * 1000, // Cache 5 min
cacheTime: 10 * 60 * 1000,
},
},
};````
Data Providers
Data providers are the core abstraction layer in Refine for backend communication. They handle all CRUD operations through a unified interface.
Interface Methods
| Method | Purpose | Key Parameters |
|---|---|---|
getList | Fetch paginated list | resource, pagination, filters, sorters |
getOne | Fetch single record | resource, id |
getMany | Fetch multiple by IDs | resource, ids |
create | Create new record | resource, variables |
update | Update existing | resource, id, variables |
deleteOne | Delete single | resource, id |
deleteMany | Delete multiple | resource, ids |
createMany | Bulk create | resource, variables[] |
updateMany | Bulk update | resource, ids, variables |
Available Providers
REST API
@refinedev/simple-rest— Generic REST API (recommended for custom APIs)@refinedev/nestjsx-crud— NestJS with @nestjsx/crud
GraphQL
@refinedev/graphql— Generic GraphQL with URQL@refinedev/hasura— Hasura GraphQL Engine
Backend-as-a-Service
@refinedev/supabase— Supabase (PostgreSQL + Auth + Storage)@refinedev/appwrite— Appwrite backend@refinedev/firebase— Firebase/Firestore
CMS
@refinedev/strapi-v4— Strapi v4@refinedev/airtable— Airtable
Basic Usage
import { Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
const API_URL = "https://api.example.com";
function App() {
return (
<Refine
dataProvider={dataProvider(API_URL)}
// ...
/>
);
}Multiple Data Providers
<Refine
dataProvider={{
default: simpleRestDataProvider("https://api.example.com"),
cms: strapiDataProvider("https://cms.example.com"),
}}
resources={[
{ name: "posts", meta: { dataProviderName: "cms" } },
{ name: "users" }, // uses "default"
]}
/>Custom Data Provider
import { DataProvider } from "@refinedev/core";
const customDataProvider: DataProvider = {
getList: async ({ resource, pagination, filters, sorters }) => {
const { current = 1, pageSize = 10 } = pagination ?? {};
const response = await fetch(
`${API_URL}/${resource}?_page=${current}&_limit=${pageSize}`
);
const data = await response.json();
const total = Number(response.headers.get("x-total-count"));
return { data, total };
},
getOne: async ({ resource, id }) => {
const response = await fetch(`${API_URL}/${resource}/${id}`);
const data = await response.json();
return { data };
},
create: async ({ resource, variables }) => {
const response = await fetch(`${API_URL}/${resource}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(variables),
});
const data = await response.json();
return { data };
},
update: async ({ resource, id, variables }) => {
const response = await fetch(`${API_URL}/${resource}/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(variables),
});
const data = await response.json();
return { data };
},
deleteOne: async ({ resource, id }) => {
const response = await fetch(`${API_URL}/${resource}/${id}`, {
method: "DELETE",
});
const data = await response.json();
return { data };
},
getMany: async ({ resource, ids }) => {
const data = await Promise.all(
ids.map(async (id) => {
const response = await fetch(`${API_URL}/${resource}/${id}`);
return response.json();
})
);
return { data };
},
deleteMany: async ({ resource, ids }) => {
const data = await Promise.all(
ids.map(async (id) => {
const response = await fetch(`${API_URL}/${resource}/${id}`, {
method: "DELETE",
});
return response.json();
})
);
return { data };
},
createMany: async ({ resource, variables }) => {
const data = await Promise.all(
variables.map(async (vars) => {
const response = await fetch(`${API_URL}/${resource}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(vars),
});
return response.json();
})
);
return { data };
},
updateMany: async ({ resource, ids, variables }) => {
const data = await Promise.all(
ids.map(async (id) => {
const response = await fetch(`${API_URL}/${resource}/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(variables),
});
return response.json();
})
);
return { data };
},
getApiUrl: () => API_URL,
};Response Formats
getList Response
{
data: TData[];
total: number;
}getOne Response
{
data: TData;
}create/update/delete Response
{
data: TData;
}Meta Parameter
All methods accept a meta parameter for custom options:
const { data } = useList({
resource: "posts",
meta: {
headers: { "X-Custom-Header": "value" },
// Provider-specific options
},
});React Query Integration
Data providers integrate with React Query (TanStack Query):
- Automatic caching
- Background refetching
- Optimistic updates
- Query invalidation on mutations
````markdown
Refine Hooks Reference
All Refine hooks are built on TanStack Query (React Query) and integrate with your data provider.
Data Hooks
useList — Paginated List
import { useList } from "@refinedev/core";
const { data, isLoading } = useList({
resource: "posts",
pagination: { current: 1, pageSize: 10 },
sorters: [{ field: "createdAt", order: "desc" }],
filters: [{ field: "status", operator: "eq", value: "published" }],
});
// data.data — records array, data.total — total countAs of 5.0.12, custom fields returned from your data provider's getList response are preserved on the hook result instead of being stripped away. Use this when your backend returns extra aggregates, cursors, or summary metadata together with the list.
useOne — Single Record
const { data } = useOne({ resource: "posts", id: 1 });
// data.data — the recorduseMany — Multiple Records by IDs
const { data } = useMany({ resource: "posts", ids: [1, 2, 3] });useCreate / useUpdate / useDelete — Mutations
import { useCreate, useUpdate, useDelete } from "@refinedev/core";
const { mutate: create } = useCreate();
create({ resource: "posts", values: { title: "New Post" } });
const { mutate: update } = useUpdate();
update({ resource: "posts", id: 1, values: { title: "Updated" } });
const { mutate: remove } = useDelete();
remove({ resource: "posts", id: 1 });Mutation Modes
mutate({
resource: "posts",
id: 1,
values: { title: "New" },
mutationMode: "pessimistic", // Wait for API (default)
// mutationMode: "optimistic", // Update UI immediately, rollback on error
// mutationMode: "undoable", // Show undo notification
undoableTimeout: 5000,
});Bulk Operations
import { useCreateMany, useUpdateMany, useDeleteMany } from "@refinedev/core";
const { mutate: createMany } = useCreateMany();
createMany({ resource: "posts", values: [{ title: "A" }, { title: "B" }] });
const { mutate: updateMany } = useUpdateMany();
updateMany({ resource: "posts", ids: [1, 2], values: { status: "published" } });
const { mutate: deleteMany } = useDeleteMany();
deleteMany({ resource: "posts", ids: [1, 2, 3] });Filter Operators
| Operator | Description | Operator | Description |
|---|---|---|---|
eq | Equal | ne | Not equal |
lt | Less than | gt | Greater than |
lte | ≤ | gte | ≥ |
in | In array | nin | Not in array |
contains | Contains | null | Is null |
between | Range | nnull | Is not null |
---
useForm — Create/Edit/Clone
import { useForm } from "@refinedev/mantine";
import { Create, Edit } from "@refinedev/mantine";
import { TextInput, Select } from "@mantine/core";
// Action auto-detected from route
const { getInputProps, saveButtonProps } = useForm({
initialValues: { title: "", status: "draft" },
validate: {
title: (v) => (v.length < 2 ? "Too short" : null),
},
});
// Create page
export const PostCreate = () => (
<Create saveButtonProps={saveButtonProps}>
<TextInput label="Title" {...getInputProps("title")} />
<Select
label="Status"
data={[
{ value: "draft", label: "Draft" },
{ value: "published", label: "Published" },
]}
{...getInputProps("status")}
/>
</Create>
);useForm Configuration
| Property | Type | Description |
|---|---|---|
action | "create" \ | "edit" \ |
id | string \ | number |
redirect | "list" \ | "edit" \ |
mutationMode | "pessimistic" \ | "optimistic" \ |
warnWhenUnsavedChanges | boolean | Browser prompt on leave |
useForm Return Values
| Property | Description |
|---|---|
getInputProps(field) | Props for Mantine inputs |
saveButtonProps | Props for submit button |
values / setValues | Form state |
errors / setFieldValue | Validation |
refineCore.formLoading | Loading state |
refineCore.queryResult | Fetched data (edit/clone) |
Auto-Save
const {
refineCore: { autoSaveProps },
} = useForm({
refineCoreProps: {
autoSave: { enabled: true, debounce: 1000 },
},
});
// autoSaveProps.status: "loading" | "error" | "idle" | "success"---
useTable — List with TanStack Table
npm install @refinedev/react-table @tanstack/react-tableimport { useTable } from "@refinedev/react-table";
import { ColumnDef, flexRender } from "@tanstack/react-table";
import { Table, Pagination, Group } from "@mantine/core";
interface Post {
id: number;
title: string;
status: string;
}
const columns: ColumnDef<Post>[] = [
{ id: "id", accessorKey: "id", header: "ID" },
{ id: "title", accessorKey: "title", header: "Title" },
{ id: "status", accessorKey: "status", header: "Status" },
];
export const PostList = () => {
const {
getHeaderGroups,
getRowModel,
getState,
setPageIndex,
getPageCount,
refineCore: { tableQuery, setFilters, setSorters },
} = useTable({
columns,
refineCoreProps: {
resource: "posts",
syncWithLocation: true, // URL sync
pagination: { pageSize: 10 },
sorters: { initial: [{ field: "createdAt", order: "desc" }] },
},
});
if (tableQuery.isLoading) return <div>Loading...</div>;
return (
<>
<Table striped highlightOnHover>
<Table.Thead>
{getHeaderGroups().map((hg) => (
<Table.Tr key={hg.id}>
{hg.headers.map((h) => (
<Table.Th key={h.id}>{flexRender(h.column.columnDef.header, h.getContext())}</Table.Th>
))}
</Table.Tr>
))}
</Table.Thead>
<Table.Tbody>
{getRowModel().rows.map((row) => (
<Table.Tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<Table.Td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</Table.Td>
))}
</Table.Tr>
))}
</Table.Tbody>
</Table>
<Group justify="flex-end" mt="md">
<Pagination total={getPageCount()} value={getState().pagination.pageIndex + 1} onChange={(p) => setPageIndex(p - 1)} />
</Group>
</>
);
};As of 5.0.12, useTable also preserves custom getList response fields, so list-level metadata can stay attached to the hook result rather than living only on the raw query object.
Filtering
const {
refineCore: { setFilters },
} = table;
// Set filters
setFilters([{ field: "status", operator: "eq", value: "published" }]);
// Merge filters
setFilters([{ field: "category", operator: "eq", value: "tech" }], "merge");Sortable Column Header
{
id: "title",
accessorKey: "title",
header: ({ column }) => (
<Group gap={4} style={{ cursor: "pointer" }} onClick={column.getToggleSortingHandler()}>
<span>Title</span>
{column.getIsSorted() === "asc" && <IconSortAscending size={16} />}
{column.getIsSorted() === "desc" && <IconSortDescending size={16} />}
</Group>
),
enableSorting: true,
}Actions Column
import { useNavigation, useDelete } from "@refinedev/core";
import { ActionIcon, Group } from "@mantine/core";
import { IconEdit, IconEye, IconTrash } from "@tabler/icons-react";
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const { show, edit } = useNavigation();
const { mutate: del } = useDelete();
const { id } = row.original;
return (
<Group gap={4}>
<ActionIcon onClick={() => show("posts", id)}><IconEye size={16} /></ActionIcon>
<ActionIcon onClick={() => edit("posts", id)}><IconEdit size={16} /></ActionIcon>
<ActionIcon color="red" onClick={() => del({ resource: "posts", id })}>
<IconTrash size={16} />
</ActionIcon>
</Group>
);
},
}---
useSelect — Dropdowns & Relations
import { useSelect } from "@refinedev/mantine";
import { Select } from "@mantine/core";
const { selectProps } = useSelect({
resource: "categories",
optionLabel: "name", // Display field (default: "title")
optionValue: "id", // Value field (default: "id")
});
<Select label="Category" {...selectProps} />;Searchable with Filters
const { selectProps } = useSelect({
resource: "users",
debounce: 500,
onSearch: (value) => [
{
operator: "or",
value: [
{ field: "firstName", operator: "contains", value },
{ field: "email", operator: "contains", value },
],
},
],
});
<Select {...selectProps} searchable clearable />;Dependent Selects
const [categoryId, setCategoryId] = useState<string>();
const { selectProps: categoryProps } = useSelect({ resource: "categories" });
const { selectProps: tagProps } = useSelect({
resource: "tags",
filters: [{ field: "category_id", operator: "eq", value: categoryId }],
queryOptions: { enabled: !!categoryId },
});
<Select label="Category" {...categoryProps} onChange={(v) => setCategoryId(v)} />
<Select label="Tag" {...tagProps} disabled={!categoryId} />Custom Labels
const { selectProps, queryResult } = useSelect({ resource: "users" });
const customData =
queryResult.data?.data.map((u) => ({
value: String(u.id),
label: `${u.firstName} ${u.lastName} (${u.email})`,
})) ?? [];
<Select {...selectProps} data={customData} />;In Forms
const {
getInputProps,
refineCore: { queryResult },
} = useForm();
const { selectProps } = useSelect({
resource: "categories",
defaultValue: queryResult?.data?.data?.category_id, // Pre-select in edit
});
<Select label="Category" {...selectProps} {...getInputProps("category_id")} />;---
Common Return Values
Query hooks (useList, useOne, useMany):
{
(data, isLoading, isFetching, isError, error, refetch);
}For useList/useTable, expect extra top-level fields from getList to remain available when the provider returns them.
Mutation hooks (useCreate, useUpdate, useDelete):
{
(mutate, mutateAsync, isLoading, isSuccess, isError, error, data);
}Query invalidation: Mutations auto-invalidate related queries. Override with invalidates: ["list", "detail"] | ["all"] | false. ````
i18n Provider
Refine supports internationalization through a pluggable i18nProvider interface.
Interface
interface I18nProvider {
translate: (key: string, options?: any, defaultMessage?: string) => string;
changeLocale: (locale: string, options?: any) => Promise<any>;
getLocale: () => string;
}Setup with react-i18next
npm install react-i18next i18next i18next-http-backend i18next-browser-languagedetector// src/i18n.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Backend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
supportedLngs: ["en", "ru", "de"],
fallbackLng: "en",
interpolation: { escapeValue: false },
});
export default i18n;Translation Files
// public/locales/en/common.json
{
"pages.posts.list": "Posts",
"buttons": { "create": "Create", "save": "Save", "delete": "Delete" },
"notifications": { "success": "Successful", "error": "Error" },
"warnWhenUnsavedChanges": "You have unsaved changes."
}i18nProvider Implementation
import { I18nProvider } from "@refinedev/core";
import { useTranslation } from "react-i18next";
export const useI18nProvider = (): I18nProvider => {
const { t, i18n } = useTranslation();
return {
translate: (key, options, defaultMessage) => t(key, { defaultValue: defaultMessage, ...options }),
changeLocale: (locale) => i18n.changeLanguage(locale),
getLocale: () => i18n.language,
};
};
// App.tsx
import "./i18n";
const i18nProvider = useI18nProvider();
<Refine i18nProvider={i18nProvider} />Hooks
import { useTranslation, useGetLocale, useSetLocale } from "@refinedev/core";
const { translate, changeLocale, getLocale } = useTranslation();
translate("pages.posts.list"); // Simple key
translate("greeting", { name: "John" }); // Interpolation: "Hello, {{name}}!"
translate("missing.key", {}, "Default Text"); // Fallback
const locale = useGetLocale(); // () => "en"
const setLocale = useSetLocale(); // (locale) => PromiseLocale Switcher
function LocaleSwitcher() {
const locale = useGetLocale();
const setLocale = useSetLocale();
return (
<select value={locale()} onChange={(e) => setLocale(e.target.value)}>
<option value="en">English</option>
<option value="ru">Русский</option>
</select>
);
}Refine Component Keys
Refine Mantine components auto-translate these keys:
{
"buttons": { "create": "Create", "save": "Save", "delete": "Delete", "edit": "Edit", "cancel": "Cancel" },
"actions": { "list": "List", "create": "Create", "edit": "Edit", "show": "Show" },
"notifications": { "success": "Successful", "error": "Error", "undoable": "You have {{seconds}} seconds to undo" },
"table": { "actions": "Actions" }
}Resource Labels
<Refine
resources={[{
name: "posts",
list: "/posts",
meta: { label: "pages.posts.title" }, // Translation key
}]}
/>Auto-pattern: ${resource.name}.${resource.name} → posts.posts
Date Formatting with Locale
import { useGetLocale } from "@refinedev/core";
import { format } from "date-fns";
import { enUS, ru, de } from "date-fns/locale";
const locales = { en: enUS, ru, de };
function FormattedDate({ date }: { date: Date }) {
const locale = useGetLocale()() as keyof typeof locales;
return <span>{format(date, "PPP", { locale: locales[locale] })}</span>;
}Inferencer (Code Generation)
Inferencer automatically generates CRUD pages based on your API response structure.
Installation
npm install @refinedev/inferencerBasic Usage
import {
MantineInferencer,
MantineListInferencer,
MantineShowInferencer,
MantineEditInferencer,
MantineCreateInferencer,
} from "@refinedev/inferencer/mantine";
// Combined inferencer (auto-detects action from route)
<Route path="/posts" element={<MantineInferencer resource="posts" />} />
// Or specific inferencers
<Route path="/posts" element={<MantineListInferencer resource="posts" />} />
<Route path="/posts/create" element={<MantineCreateInferencer resource="posts" />} />
<Route path="/posts/edit/:id" element={<MantineEditInferencer resource="posts" />} />
<Route path="/posts/show/:id" element={<MantineShowInferencer resource="posts" />} />Full App Example
import { Refine } from "@refinedev/core";
import { ThemedLayout } from "@refinedev/mantine";
import { MantineInferencer } from "@refinedev/inferencer/mantine";
import routerProvider from "@refinedev/react-router";
import dataProvider from "@refinedev/simple-rest";
import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
resources={[
{
name: "posts",
list: "/posts",
create: "/posts/create",
edit: "/posts/edit/:id",
show: "/posts/show/:id",
},
{
name: "categories",
list: "/categories",
show: "/categories/show/:id",
},
]}
>
<Routes>
<Route element={<ThemedLayout><Outlet /></ThemedLayout>}>
{/* Posts - full CRUD */}
<Route path="/posts" element={<MantineInferencer />} />
<Route path="/posts/create" element={<MantineInferencer />} />
<Route path="/posts/edit/:id" element={<MantineInferencer />} />
<Route path="/posts/show/:id" element={<MantineInferencer />} />
{/* Categories - list and show only */}
<Route path="/categories" element={<MantineInferencer />} />
<Route path="/categories/show/:id" element={<MantineInferencer />} />
</Route>
</Routes>
</Refine>
</BrowserRouter>
);
}How It Works
1. Inferencer fetches data from your API 2. Analyzes the response structure (field types, relations) 3. Generates appropriate components:
- Text fields →
<TextInput /> - Numbers →
<NumberInput /> - Booleans →
<Checkbox /> - Dates →
<DatePicker /> - Relations →
<Select />with data from related resource
4. Shows the generated code for you to copy
Viewing Generated Code
The inferencer displays the generated source code in a panel. You can:
- Copy the code
- Customize it for your needs
- Replace the inferencer with the generated component
// 1. Start with inferencer
<Route path="/posts" element={<MantineListInferencer />} />
// 2. Copy generated code from the UI panel
// 3. Create your own component with customizations
// posts/list.tsx (based on generated code)
export const PostList = () => {
// ... generated code with your modifications
};
// 4. Replace inferencer with your component
<Route path="/posts" element={<PostList />} />Configuration
Field Transformer
Customize field inference:
<MantineListInferencer
fieldTransformer={(field) => {
// Skip certain fields
if (field.key === "internal_id") {
return false;
}
// Modify field config
if (field.key === "status") {
return {
...field,
type: "select",
options: [
{ value: "draft", label: "Draft" },
{ value: "published", label: "Published" },
],
};
}
return field;
}}
/>Hide Code Viewer
<MantineInferencer hideCodeViewerInProduction />Supported Field Types
| Type | Generated Component |
|---|---|
string | <TextInput /> |
number | <NumberInput /> |
boolean | <Checkbox /> |
date | <DatePicker /> |
email | <TextInput type="email" /> |
url | <TextInput /> |
richtext | <Textarea /> |
relation | <Select /> with useSelect |
array | Multiple inputs |
object | Nested fields |
Workflow
1. Rapid Prototyping: Use inferencer to quickly scaffold CRUD pages 2. Copy Code: Get the generated code from the UI 3. Customize: Modify the code for your specific requirements 4. Replace: Swap inferencer with your custom components
Limitations
- Generated code is a starting point, not production-ready
- Complex validations need manual implementation
- Custom business logic must be added manually
- May not handle all edge cases perfectly
Best Practice
Use Inferencer for:
- Rapid prototyping
- Learning Refine patterns
- Starting point for new resources
Don't use Inferencer in production:
- Replace with actual components
- Add proper validation
- Implement business logic
- Optimize for your specific needs
Mantine UI Integration
Refine provides seamless integration with Mantine v5 for building admin interfaces.
Installation
npm install @refinedev/mantine @refinedev/react-table \
@mantine/core@5 @mantine/hooks@5 @mantine/form@5 @mantine/notifications@5 \
@emotion/react@11 @tabler/icons-react @tanstack/react-tableBasic Setup
import { Refine } from "@refinedev/core";
import { useNotificationProvider, RefineThemes } from "@refinedev/mantine";
import { MantineProvider } from "@mantine/core";
import { NotificationsProvider } from "@mantine/notifications";
import routerProvider from "@refinedev/react-router";
import dataProvider from "@refinedev/simple-rest";
import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<MantineProvider
theme={RefineThemes.Blue}
withNormalizeCSS
withGlobalStyles
>
<NotificationsProvider position="top-right">
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider("https://api.example.com")}
notificationProvider={useNotificationProvider}
resources={[
{
name: "posts",
list: "/posts",
create: "/posts/create",
edit: "/posts/edit/:id",
show: "/posts/show/:id",
},
]}
>
<Routes>
<Route path="/posts" element={<PostList />} />
<Route path="/posts/create" element={<PostCreate />} />
<Route path="/posts/edit/:id" element={<PostEdit />} />
<Route path="/posts/show/:id" element={<PostShow />} />
</Routes>
</Refine>
</NotificationsProvider>
</MantineProvider>
</BrowserRouter>
);
}Layout Components
ThemedLayout
import { ThemedLayout, ThemedSider, ThemedHeader } from "@refinedev/mantine";
<Routes>
<Route
element={
<ThemedLayout
Header={ThemedHeader}
Sider={ThemedSider}
Title={({ collapsed }) => (
<div>{collapsed ? "App" : "My Application"}</div>
)}
>
<Outlet />
</ThemedLayout>
}
>
<Route path="/posts" element={<PostList />} />
{/* ... */}
</Route>
</Routes>View Components
List View
import { List, EditButton, ShowButton, DeleteButton } from "@refinedev/mantine";
import { useTable } from "@refinedev/react-table";
import { ColumnDef, flexRender } from "@tanstack/react-table";
import { Table, Group, Pagination, ScrollArea } from "@mantine/core";
interface Post {
id: number;
title: string;
status: string;
}
export const PostList = () => {
const columns: ColumnDef<Post>[] = [
{ id: "id", header: "ID", accessorKey: "id" },
{ id: "title", header: "Title", accessorKey: "title" },
{ id: "status", header: "Status", accessorKey: "status" },
{
id: "actions",
header: "Actions",
accessorKey: "id",
cell: ({ getValue }) => {
const id = getValue() as number;
return (
<Group spacing="xs" noWrap>
<ShowButton hideText recordItemId={id} />
<EditButton hideText recordItemId={id} />
<DeleteButton hideText recordItemId={id} />
</Group>
);
},
},
];
const {
getHeaderGroups,
getRowModel,
refineCore: { setCurrent, pageCount, current },
} = useTable({ columns });
return (
<List>
<ScrollArea>
<Table highlightOnHover>
<thead>
{getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</Table>
</ScrollArea>
<Pagination
position="right"
total={pageCount}
page={current}
onChange={setCurrent}
mt="md"
/>
</List>
);
};Create View
import { Create, useForm } from "@refinedev/mantine";
import { TextInput, Select, Textarea } from "@mantine/core";
export const PostCreate = () => {
const { saveButtonProps, getInputProps, errors } = useForm({
initialValues: {
title: "",
status: "draft",
content: "",
},
validate: {
title: (value) => (value.length < 2 ? "Title is too short" : null),
},
});
return (
<Create saveButtonProps={saveButtonProps}>
<TextInput
mt="sm"
label="Title"
placeholder="Post title"
{...getInputProps("title")}
/>
<Select
mt="sm"
label="Status"
data={[
{ value: "draft", label: "Draft" },
{ value: "published", label: "Published" },
]}
{...getInputProps("status")}
/>
<Textarea
mt="sm"
label="Content"
placeholder="Post content"
minRows={4}
{...getInputProps("content")}
/>
</Create>
);
};Edit View
import { Edit, useForm } from "@refinedev/mantine";
import { TextInput, Select, Textarea } from "@mantine/core";
export const PostEdit = () => {
const { saveButtonProps, getInputProps } = useForm({
initialValues: {
title: "",
status: "",
content: "",
},
});
return (
<Edit saveButtonProps={saveButtonProps}>
<TextInput
mt="sm"
label="Title"
{...getInputProps("title")}
/>
<Select
mt="sm"
label="Status"
data={[
{ value: "draft", label: "Draft" },
{ value: "published", label: "Published" },
]}
{...getInputProps("status")}
/>
<Textarea
mt="sm"
label="Content"
minRows={4}
{...getInputProps("content")}
/>
</Edit>
);
};Show View
import { Show, TextField, NumberField, DateField, MarkdownField } from "@refinedev/mantine";
import { useShow } from "@refinedev/core";
import { Title, Text } from "@mantine/core";
export const PostShow = () => {
const { queryResult } = useShow();
const { data, isLoading } = queryResult;
const record = data?.data;
return (
<Show isLoading={isLoading}>
<Title order={5}>Title</Title>
<TextField value={record?.title} />
<Title mt="sm" order={5}>Status</Title>
<TextField value={record?.status} />
<Title mt="sm" order={5}>Created At</Title>
<DateField value={record?.createdAt} />
<Title mt="sm" order={5}>Content</Title>
<MarkdownField value={record?.content} />
</Show>
);
};Buttons
| Component | Purpose |
|---|---|
<CreateButton /> | Navigate to create page |
<EditButton /> | Navigate to edit page |
<ShowButton /> | Navigate to show page |
<ListButton /> | Navigate to list page |
<CloneButton /> | Navigate to clone page |
<DeleteButton /> | Delete with confirmation |
<SaveButton /> | Submit form |
<RefreshButton /> | Refresh data |
<ImportButton /> | Import data |
<ExportButton /> | Export data |
import {
CreateButton,
EditButton,
DeleteButton,
ShowButton,
} from "@refinedev/mantine";
// In List view header
<CreateButton />
// In table actions
<EditButton recordItemId={record.id} />
<ShowButton recordItemId={record.id} />
<DeleteButton recordItemId={record.id} />
// Hide text (icon only)
<EditButton hideText recordItemId={record.id} />Field Components
| Component | Description |
|---|---|
<TextField /> | Text display |
<NumberField /> | Formatted numbers |
<DateField /> | Formatted dates |
<BooleanField /> | Yes/No display |
<EmailField /> | Email link |
<UrlField /> | URL link |
<FileField /> | File download link |
<TagField /> | Tag/badge display |
<MarkdownField /> | Rendered markdown |
import {
TextField,
NumberField,
DateField,
BooleanField,
TagField,
} from "@refinedev/mantine";
<TextField value={record.name} />
<NumberField value={record.price} options={{ style: "currency", currency: "USD" }} />
<DateField value={record.createdAt} format="MMMM DD, YYYY" />
<BooleanField value={record.isActive} />
<TagField value={record.status} />Form Hooks
useForm
import { useForm } from "@refinedev/mantine";
const {
saveButtonProps,
getInputProps,
values,
setValues,
errors,
refineCore: { formLoading, queryResult },
} = useForm({
initialValues: { name: "", price: 0 },
validate: {
name: (value) => (value ? null : "Name is required"),
price: (value) => (value > 0 ? null : "Price must be positive"),
},
refineCoreProps: {
resource: "products",
action: "create", // "create" | "edit" | "clone"
redirect: "list",
onMutationSuccess: () => console.log("Success"),
},
});useSelect
import { useSelect } from "@refinedev/mantine";
import { Select } from "@mantine/core";
const { selectProps } = useSelect({
resource: "categories",
optionLabel: "title",
optionValue: "id",
});
<Select
label="Category"
placeholder="Select category"
{...selectProps}
{...getInputProps("categoryId")}
/>useModalForm
import { useModalForm } from "@refinedev/mantine";
import { Modal, TextInput, Button } from "@mantine/core";
const {
modal: { visible, close, title },
saveButtonProps,
getInputProps,
} = useModalForm({
refineCoreProps: { action: "create" },
initialValues: { name: "" },
});
<>
<Button onClick={() => modal.show()}>Create</Button>
<Modal opened={visible} onClose={close} title={title}>
<TextInput label="Name" {...getInputProps("name")} />
<Button {...saveButtonProps}>Save</Button>
</Modal>
</>useDrawerForm
import { useDrawerForm } from "@refinedev/mantine";
import { Drawer, TextInput, Button } from "@mantine/core";
const {
drawerProps,
saveButtonProps,
getInputProps,
} = useDrawerForm({
refineCoreProps: { action: "edit" },
});
<Drawer {...drawerProps}>
<TextInput label="Name" {...getInputProps("name")} />
<Button {...saveButtonProps}>Save</Button>
</Drawer>useStepsForm
import { useStepsForm } from "@refinedev/mantine";
import { Stepper, Button, TextInput } from "@mantine/core";
const {
saveButtonProps,
getInputProps,
steps: { currentStep, gotoStep },
} = useStepsForm({
initialValues: { name: "", email: "", bio: "" },
stepsProps: {
finish: saveButtonProps,
},
});
<Stepper active={currentStep}>
<Stepper.Step label="Basic">
<TextInput label="Name" {...getInputProps("name")} />
</Stepper.Step>
<Stepper.Step label="Contact">
<TextInput label="Email" {...getInputProps("email")} />
</Stepper.Step>
<Stepper.Step label="Profile">
<TextInput label="Bio" {...getInputProps("bio")} />
</Stepper.Step>
</Stepper>Theming
Built-in Themes
import { RefineThemes } from "@refinedev/mantine";
import { MantineProvider } from "@mantine/core";
// Available: Blue, Purple, Magenta, Red, Orange, Yellow, Green
<MantineProvider theme={RefineThemes.Blue}>
{/* ... */}
</MantineProvider>Dark Mode
import { RefineThemes } from "@refinedev/mantine";
import { MantineProvider, ColorSchemeProvider } from "@mantine/core";
import { useState } from "react";
function App() {
const [colorScheme, setColorScheme] = useState<"light" | "dark">("light");
const toggleColorScheme = () => {
setColorScheme((prev) => (prev === "light" ? "dark" : "light"));
};
return (
<ColorSchemeProvider
colorScheme={colorScheme}
toggleColorScheme={toggleColorScheme}
>
<MantineProvider
theme={{
...RefineThemes.Blue,
colorScheme,
}}
withNormalizeCSS
withGlobalStyles
>
{/* ... */}
</MantineProvider>
</ColorSchemeProvider>
);
}Auth Pages
import { AuthPage } from "@refinedev/mantine";
// Login
<Route path="/login" element={<AuthPage type="login" />} />
// Register
<Route path="/register" element={<AuthPage type="register" />} />
// Forgot Password
<Route path="/forgot-password" element={<AuthPage type="forgotPassword" />} />
// Reset Password
<Route path="/reset-password" element={<AuthPage type="updatePassword" />} />
// Customization
<AuthPage
type="login"
title="Welcome Back"
formProps={{
initialValues: { email: "", password: "" },
}}
rememberMe={true}
registerLink="/register"
forgotPasswordLink="/forgot-password"
/>Error Component
import { ErrorComponent } from "@refinedev/mantine";
<Route path="*" element={<ErrorComponent />} />Notification Provider Reference
Refine's notification system provides toast notifications for user feedback on CRUD operations.
NotificationProvider Interface
interface NotificationProvider {
open: (params: OpenNotificationParams) => void;
close: (key: string) => void;
}
interface OpenNotificationParams {
key?: string;
message: string;
description?: string;
type: "success" | "error" | "progress";
cancelMutation?: () => void;
undoableTimeout?: number;
}Built-in Mantine Notifications
Mantine package includes built-in notification provider:
import { Refine } from "@refinedev/core";
import { notificationProvider } from "@refinedev/mantine";
import { NotificationsProvider } from "@mantine/notifications";
function App() {
return (
<NotificationsProvider position="top-right">
<Refine
notificationProvider={notificationProvider}
// ...other providers
>
{/* Routes */}
</Refine>
</NotificationsProvider>
);
}useNotification Hook
Access notification methods anywhere in your app:
import { useNotification } from "@refinedev/core";
function MyComponent() {
const { open, close } = useNotification();
const showSuccess = () => {
open({
type: "success",
message: "Operation completed",
description: "Record saved successfully",
});
};
const showError = () => {
open({
type: "error",
message: "Operation failed",
description: "Could not save the record",
});
};
const showProgress = () => {
open({
key: "upload-progress",
type: "progress",
message: "Uploading...",
undoableTimeout: 5000,
cancelMutation: () => {
console.log("Upload cancelled");
},
});
};
const closeNotification = () => {
close("upload-progress");
};
return (
<>
<button onClick={showSuccess}>Show Success</button>
<button onClick={showError}>Show Error</button>
<button onClick={showProgress}>Show Progress</button>
</>
);
}Automatic Notifications
Refine data hooks trigger notifications automatically:
| Hook | Success | Error |
|---|---|---|
| useCreate | "Successfully created" | Shows error message |
| useUpdate | "Successfully updated" | Shows error message |
| useDelete | "Successfully deleted" | Shows error message |
Disable Auto Notifications
const { mutate } = useCreate();
mutate({
resource: "posts",
values: { title: "New Post" },
successNotification: false, // Disable success toast
errorNotification: false, // Disable error toast
});Custom Notification Messages
const { mutate } = useCreate();
mutate({
resource: "posts",
values: { title: "New Post" },
successNotification: (data, values, resource) => ({
message: "Post Created!",
description: `"${values.title}" was published.`,
type: "success",
}),
errorNotification: (error, values, resource) => ({
message: "Creation Failed",
description: error?.message || "Unknown error occurred",
type: "error",
}),
});Undoable Mutations
Enable undo for delete operations:
<Refine
options={{
mutationMode: "undoable",
undoableTimeout: 5000, // 5 seconds to undo
}}
/>With undoable mode: 1. User clicks delete 2. Notification shows with "Undo" button 3. After timeout, mutation executes 4. User can cancel during the timeout
const { mutate } = useDelete();
mutate({
resource: "posts",
id: 1,
mutationMode: "undoable", // Per-mutation override
undoableTimeout: 10000, // 10 seconds
});Custom Notification Provider
Create custom provider for React Toastify or other libraries:
import { toast, ToastContainer } from "react-toastify";
import type { NotificationProvider } from "@refinedev/core";
const notificationProvider: NotificationProvider = {
open: ({ key, message, description, type, cancelMutation, undoableTimeout }) => {
if (type === "progress") {
toast.loading(message, {
toastId: key,
autoClose: false,
});
if (undoableTimeout && cancelMutation) {
setTimeout(() => {
toast.dismiss(key);
}, undoableTimeout);
}
return;
}
toast[type](
<>
<strong>{message}</strong>
{description && <p>{description}</p>}
{cancelMutation && (
<button onClick={cancelMutation}>Undo</button>
)}
</>,
{ toastId: key }
);
},
close: (key) => {
toast.dismiss(key);
},
};
// Usage in App
function App() {
return (
<>
<ToastContainer />
<Refine notificationProvider={notificationProvider} />
</>
);
}Common Patterns
Manual Success/Error After Custom Logic
function CustomAction() {
const { open } = useNotification();
const apiUrl = useApiUrl();
const handleCustomAction = async () => {
try {
await fetch(`${apiUrl}/custom-endpoint`, {
method: "POST",
});
open({
type: "success",
message: "Custom action completed",
});
} catch (error) {
open({
type: "error",
message: "Custom action failed",
description: error.message,
});
}
};
return <button onClick={handleCustomAction}>Execute</button>;
}Notification with Key for Updates
function ProgressExample() {
const { open, close } = useNotification();
const startProgress = () => {
open({
key: "file-upload",
type: "progress",
message: "Uploading file...",
});
// Later, update to success
setTimeout(() => {
close("file-upload");
open({
type: "success",
message: "File uploaded successfully",
});
}, 3000);
};
return <button onClick={startProgress}>Upload</button>;
}Live / Realtime
Refine supports real-time data updates via liveProvider.
Provider Interface
import type { LiveProvider } from "@refinedev/core";
const liveProvider: LiveProvider = {
subscribe: ({ channel, types, params, callback }) => {
// Subscribe to real-time events
// Return unsubscribe function
return () => {
// Cleanup subscription
};
},
unsubscribe: (subscription) => {
// Unsubscribe from channel
},
publish: ({ channel, type, payload, date }) => {
// Publish event (optional)
},
};Usage
import { Refine } from "@refinedev/core";
import { liveProvider } from "./liveProvider";
function App() {
return (
<Refine
liveProvider={liveProvider}
options={{
liveMode: "auto", // "auto" | "manual" | "off"
}}
>
{/* ... */}
</Refine>
);
}Live Modes
| Mode | Description |
|---|---|
auto | Automatically refetch on events |
manual | Call onLiveEvent callback, no auto-refetch |
off | Disable live updates |
Ably Integration
npm install @refinedev/ably ablyimport { Refine } from "@refinedev/core";
import { liveProvider } from "@refinedev/ably";
import Ably from "ably";
const ablyClient = new Ably.Realtime("YOUR_ABLY_API_KEY");
function App() {
return (
<Refine
liveProvider={liveProvider(ablyClient)}
options={{ liveMode: "auto" }}
>
{/* ... */}
</Refine>
);
}Supabase Realtime
import { Refine } from "@refinedev/core";
import { dataProvider, liveProvider } from "@refinedev/supabase";
import { createClient } from "@supabase/supabase-js";
const supabaseClient = createClient(
"https://your-project.supabase.co",
"your-anon-key"
);
function App() {
return (
<Refine
dataProvider={dataProvider(supabaseClient)}
liveProvider={liveProvider(supabaseClient)}
options={{ liveMode: "auto" }}
>
{/* ... */}
</Refine>
);
}Custom WebSocket Provider
import type { LiveProvider } from "@refinedev/core";
const createWebSocketLiveProvider = (wsUrl: string): LiveProvider => {
const ws = new WebSocket(wsUrl);
const subscribers = new Map<string, Set<Function>>();
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
const { channel, type, payload } = data;
const channelSubscribers = subscribers.get(channel);
if (channelSubscribers) {
channelSubscribers.forEach((callback) => {
callback({ channel, type, payload, date: new Date() });
});
}
};
return {
subscribe: ({ channel, types, callback }) => {
if (!subscribers.has(channel)) {
subscribers.set(channel, new Set());
ws.send(JSON.stringify({ action: "subscribe", channel }));
}
subscribers.get(channel)?.add(callback);
return () => {
subscribers.get(channel)?.delete(callback);
if (subscribers.get(channel)?.size === 0) {
subscribers.delete(channel);
ws.send(JSON.stringify({ action: "unsubscribe", channel }));
}
};
},
unsubscribe: (unsubscribeFn) => {
unsubscribeFn();
},
publish: ({ channel, type, payload }) => {
ws.send(JSON.stringify({ action: "publish", channel, type, payload }));
},
};
};
const liveProvider = createWebSocketLiveProvider("wss://your-ws-server.com");Hook-Level Configuration
useList with Live Updates
import { useList } from "@refinedev/core";
const { data } = useList({
resource: "posts",
liveMode: "auto",
onLiveEvent: (event) => {
console.log("Live event:", event);
// { channel: "posts", type: "created", payload: {...}, date: Date }
},
});useOne with Live Updates
import { useOne } from "@refinedev/core";
const { data } = useOne({
resource: "posts",
id: 1,
liveMode: "auto",
onLiveEvent: (event) => {
if (event.type === "updated" && event.payload.id === 1) {
console.log("Post updated:", event.payload);
}
},
});Event Types
| Type | Description |
|---|---|
created | New record created |
updated | Record updated |
deleted | Record deleted |
* | All event types |
useSubscription Hook
For custom subscriptions:
import { useSubscription } from "@refinedev/core";
const MyComponent = () => {
useSubscription({
channel: "notifications",
types: ["created"],
onLiveEvent: (event) => {
console.log("New notification:", event.payload);
},
});
return <div>Listening for notifications...</div>;
};usePublish Hook
For publishing events:
import { usePublish } from "@refinedev/core";
const MyComponent = () => {
const publish = usePublish();
const handleAction = () => {
publish?.({
channel: "posts",
type: "custom-action",
payload: { message: "Hello!" },
date: new Date(),
});
};
return <button onClick={handleAction}>Publish Event</button>;
};Per-Resource Configuration
<Refine
resources={[
{
name: "posts",
list: "/posts",
meta: {
liveMode: "auto", // Live updates for this resource
},
},
{
name: "logs",
list: "/logs",
meta: {
liveMode: "off", // No live updates for logs
},
},
]}
/>Resources
Resources are the main building blocks of a Refine app. A resource represents an entity in your API (e.g., /posts, /users) and connects data from the API to pages in your app.
Basic Definition
import { Refine } from "@refinedev/core";
<Refine
dataProvider={dataProvider("https://api.example.com")}
resources={[
{
name: "posts",
list: "/posts",
create: "/posts/create",
edit: "/posts/edit/:id",
show: "/posts/show/:id",
},
{
name: "users",
list: "/users",
show: "/users/show/:id",
},
]}
/>Resource Properties
name (required)
The API endpoint name. Used for data provider requests:
name: "posts"→GET /posts,POST /posts, etc.
identifier
Differentiates resources with the same name but different configurations:
resources={[
{
name: "posts",
identifier: "posts",
},
{
name: "posts",
identifier: "featured-posts",
meta: {
dataProviderName: "cms",
filter: { featured: true },
},
},
]}
// Usage
useList({ resource: "featured-posts" });Action Routes
| Property | Description | Default Path |
|---|---|---|
list | List page route | /${name} |
create | Create page route | /${name}/create |
edit | Edit page route | /${name}/edit/:id |
show | Show/detail page route | /${name}/show/:id |
Each can be:
- String: Route path
- Component: Uses default path
- Object:
{ component: Component, path: "/custom-path" }
// String paths (recommended)
{
name: "posts",
list: "/posts",
create: "/posts/create",
edit: "/posts/edit/:id",
show: "/posts/show/:id",
}
// Custom paths with parameters
{
name: "posts",
list: "/:authorId/posts",
edit: "/:authorId/posts/:id/edit",
}meta Properties
label
Custom display name in menus (default: pluralized name):
{
name: "post",
meta: { label: "Blog Posts" },
}icon
Icon for menu:
import { IconArticle } from "@tabler/icons-react";
{
name: "posts",
meta: { icon: <IconArticle /> },
}canDelete
Show delete button in CRUD views:
{
name: "posts",
meta: { canDelete: true },
}parent
Nest resource under another (for hierarchical menus):
resources={[
{ name: "cms" },
{
name: "posts",
meta: { parent: "cms" },
},
{
name: "categories",
meta: { parent: "cms" },
},
]}dataProviderName
Specify data provider for multi-provider setups:
dataProvider={{
default: defaultDataProvider,
cms: cmsDataProvider,
}}
resources={[
{
name: "posts",
meta: { dataProviderName: "cms" },
},
]}hide
Hide from menu/sidebar:
{
name: "internal-logs",
meta: { hide: true },
}Full Example with All Options
import { Refine } from "@refinedev/core";
import { IconArticle, IconUsers, IconSettings } from "@tabler/icons-react";
<Refine
dataProvider={{
default: restDataProvider,
cms: cmsDataProvider,
}}
resources={[
{
name: "posts",
identifier: "posts",
list: "/posts",
create: "/posts/create",
edit: "/posts/edit/:id",
show: "/posts/show/:id",
meta: {
label: "Blog Posts",
icon: <IconArticle />,
canDelete: true,
dataProviderName: "cms",
},
},
{
name: "users",
list: "/users",
show: "/users/:id",
meta: {
label: "Team Members",
icon: <IconUsers />,
canDelete: false,
},
},
{
name: "settings",
list: "/settings",
edit: "/settings",
meta: {
icon: <IconSettings />,
hide: false, // Show in menu
},
},
{
name: "audit-logs",
list: "/logs",
meta: {
hide: true, // Hidden from menu
},
},
]}
/>Accessing Resource Info
useResource Hook
import { useResource } from "@refinedev/core";
const { resource, resources, identifier } = useResource();
// Get specific resource
const { resource: postsResource } = useResource("posts");useResourceParams Hook
import { useResourceParams } from "@refinedev/core";
const { resource, action, id } = useResourceParams();
// resource: current resource object
// action: "list" | "create" | "edit" | "show"
// id: record ID (for edit/show)Routes Setup (React Router)
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Refine } from "@refinedev/core";
import routerProvider from "@refinedev/react-router";
function App() {
return (
<BrowserRouter>
<Refine
routerProvider={routerProvider}
resources={[
{
name: "posts",
list: "/posts",
create: "/posts/create",
edit: "/posts/edit/:id",
show: "/posts/show/:id",
},
]}
>
<Routes>
<Route path="/posts" element={<PostList />} />
<Route path="/posts/create" element={<PostCreate />} />
<Route path="/posts/edit/:id" element={<PostEdit />} />
<Route path="/posts/show/:id" element={<PostShow />} />
</Routes>
</Refine>
</BrowserRouter>
);
}Navigation
Use useNavigation for programmatic navigation:
import { useNavigation } from "@refinedev/core";
const { list, create, edit, show } = useNavigation();
// Navigate to pages
list("posts"); // /posts
create("posts"); // /posts/create
edit("posts", 1); // /posts/edit/1
show("posts", 1); // /posts/show/1Or use navigation components (from UI package):
import { EditButton, ShowButton, DeleteButton } from "@refinedev/mantine";
<EditButton recordItemId={record.id} />
<ShowButton recordItemId={record.id} />
<DeleteButton recordItemId={record.id} />Routing
Refine integrates with React Router for navigation. The @refinedev/react-router package provides the router bindings.
Installation
npm install @refinedev/react-router react-router-domBasic Setup
import { Refine } from "@refinedev/core";
import routerProvider from "@refinedev/react-router";
import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom";
import dataProvider from "@refinedev/simple-rest";
function App() {
return (
<BrowserRouter>
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider("https://api.example.com")}
resources={[
{
name: "posts",
list: "/posts",
create: "/posts/create",
edit: "/posts/edit/:id",
show: "/posts/show/:id",
},
]}
>
<Routes>
<Route element={<Layout><Outlet /></Layout>}>
<Route path="/posts" element={<PostList />} />
<Route path="/posts/create" element={<PostCreate />} />
<Route path="/posts/edit/:id" element={<PostEdit />} />
<Route path="/posts/show/:id" element={<PostShow />} />
</Route>
</Routes>
</Refine>
</BrowserRouter>
);
}Navigation Hooks
useNavigation
Navigate to resource actions:
import { useNavigation } from "@refinedev/core";
const MyComponent = () => {
const { list, create, edit, show, clone, goBack } = useNavigation();
return (
<>
<button onClick={() => list("posts")}>Go to Posts</button>
<button onClick={() => create("posts")}>Create Post</button>
<button onClick={() => edit("posts", 1)}>Edit Post #1</button>
<button onClick={() => show("posts", 1)}>Show Post #1</button>
<button onClick={() => clone("posts", 1)}>Clone Post #1</button>
<button onClick={() => goBack()}>Go Back</button>
</>
);
};useGo
Lower-level navigation with query params:
import { useGo } from "@refinedev/core";
const MyComponent = () => {
const go = useGo();
return (
<>
{/* Navigate with path */}
<button
onClick={() =>
go({
to: "/posts",
query: { status: "published" },
type: "push", // "push" | "replace" | "path"
})
}
>
Published Posts
</button>
{/* Navigate with resource */}
<button
onClick={() =>
go({
to: {
resource: "posts",
action: "edit",
id: 1,
},
query: { tab: "comments" },
})
}
>
Edit Post
</button>
{/* Keep existing query params */}
<button
onClick={() =>
go({
to: "/posts",
query: { page: 2 },
options: { keepQuery: true },
})
}
>
Next Page
</button>
</>
);
};useGo Parameters
| Parameter | Type | Description |
|---|---|---|
to | string \ | object |
query | object | Query parameters |
type | "push" \ | "replace" \ |
hash | string | URL hash |
options.keepQuery | boolean | Merge with existing query |
options.keepHash | boolean | Keep existing hash |
useGetToPath
Get path without navigating:
import { useGetToPath } from "@refinedev/core";
const MyComponent = () => {
const getToPath = useGetToPath();
const editPath = getToPath({
resource: { name: "posts" },
action: "edit",
meta: { id: 1 },
});
// editPath = "/posts/edit/1"
};useParsed
Parse current route:
import { useParsed } from "@refinedev/core";
const MyComponent = () => {
const { resource, action, id, params, pathname } = useParsed();
// On /posts/edit/1?status=draft
// resource: { name: "posts", ... }
// action: "edit"
// id: "1"
// params: { status: "draft" }
// pathname: "/posts/edit/1"
};useResourceParams
Get resource info from current route:
import { useResourceParams } from "@refinedev/core";
const MyComponent = () => {
const { resource, action, id } = useResourceParams();
// Useful in page components to know which resource/action is active
};URL Sync
Sync Table State
<Refine
options={{
syncWithLocation: true, // Enable global URL sync
}}
/>
// Table state syncs to URL:
// /posts?current=2&pageSize=10&sorters[0][field]=title&sorters[0][order]=ascManual Sync in useTable
const table = useTable({
refineCoreProps: {
syncWithLocation: true,
},
});Protected Routes
import { Authenticated } from "@refinedev/core";
import { Navigate, Outlet } from "react-router-dom";
<Routes>
{/* Public routes */}
<Route path="/login" element={<LoginPage />} />
{/* Protected routes */}
<Route
element={
<Authenticated fallback={<Navigate to="/login" />}>
<Layout>
<Outlet />
</Layout>
</Authenticated>
}
>
<Route path="/posts" element={<PostList />} />
<Route path="/posts/create" element={<PostCreate />} />
{/* ... */}
</Route>
</Routes>Catch-All / 404 Route
import { ErrorComponent } from "@refinedev/mantine";
<Routes>
{/* ... other routes */}
<Route path="*" element={<ErrorComponent />} />
</Routes>Index Redirect
import { NavigateToResource } from "@refinedev/react-router";
<Routes>
<Route index element={<NavigateToResource resource="posts" />} />
{/* ... */}
</Routes>
// Redirects "/" to the list page of "posts" resourceCustom Route Params
Resources can have custom parameters:
// Resource definition
{
name: "posts",
list: "/:tenantId/posts",
edit: "/:tenantId/posts/:id/edit",
}
// Navigation with meta
const { edit } = useNavigation();
edit("posts", 1, undefined, { tenantId: "acme" });
// → /acme/posts/1/edit
// Using useGo
go({
to: {
resource: "posts",
action: "edit",
id: 1,
meta: { tenantId: "acme" },
},
});Route-Based Action Detection
Refine auto-detects the action based on route:
// In PostEdit component (rendered at /posts/edit/:id)
const { action } = useResourceParams();
// action = "edit"
// useForm auto-detects action
const form = useForm(); // Automatically uses "edit" action