refine-dev
Refine.dev headless React framework for CRUD apps: data providers, resources, routing, authentication, hooks, and forms.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install itechmeat-llm-code-refine-dev
Repository
Skill path: skills/refine-dev
Refine.dev headless React framework for CRUD apps: data providers, resources, routing, authentication, hooks, and forms.
Open repositoryBest for
Primary workflow: Analyze Data & AI.
Technical facets: Full Stack, Frontend, Data / AI.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: itechmeat.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install refine-dev into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/itechmeat/llm-code before adding refine-dev to shared team environments
- Use refine-dev for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: refine-dev
description: "Refine.dev headless React framework for CRUD apps: data providers, resources, routing, authentication, hooks, and forms."
version: "8.0.0"
release_date: "2026-01-19"
---
# 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.
## When to use
- Building admin panels, dashboards, or internal tools with React
- Need CRUD operations with various backends (REST, GraphQL, Supabase, Strapi, etc.)
- Want a headless approach with Mantine UI integration
- Setting up Vite-based React projects with data management
- Implementing authentication flows in React admin apps
## 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)
```bash
npm create refine-app@latest
# Select: Vite, Mantine, REST API (or your backend)
```
Or manual setup:
```bash
npm install @refinedev/core @refinedev/mantine @refinedev/react-router @mantine/core @mantine/hooks @mantine/form @mantine/notifications
```
## Minimal App Structure
```tsx
// 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
## References (Detailed Guides)
### Core
- [data-providers.md](references/data-providers.md) — Data provider interface, available providers, custom implementation
- [resources.md](references/resources.md) — Resource definition and configuration
- [routing.md](references/routing.md) — React Router integration and route patterns
### Hooks
- [hooks.md](references/hooks.md) — All hooks: useList, useOne, useCreate, useUpdate, useDelete, useForm, useTable, useSelect
### Security & Auth
- [auth.md](references/auth.md) — Auth provider, access control, RBAC/ABAC, Casbin/CASL integration
### UI & Components
- [mantine-ui.md](references/mantine-ui.md) — Mantine components integration
- [inferencer.md](references/inferencer.md) — Auto-generate CRUD pages from API schema
### Utilities & Features
- [notifications.md](references/notifications.md) — Notification provider and useNotification hook
- [i18n.md](references/i18n.md) — Internationalization with i18nProvider
- [realtime.md](references/realtime.md) — LiveProvider for websocket/realtime subscriptions
## Links
- Official docs: https://refine.dev/docs/
- GitHub: https://github.com/refinedev/refine
- Mantine integration: https://refine.dev/docs/ui-integrations/mantine/
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/data-providers.md
```markdown
# 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
```tsx
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
```tsx
<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
```tsx
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
```ts
{
data: TData[];
total: number;
}
```
### getOne Response
```ts
{
data: TData;
}
```
### create/update/delete Response
```ts
{
data: TData;
}
```
## Meta Parameter
All methods accept a `meta` parameter for custom options:
```tsx
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
```
### references/resources.md
```markdown
# 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
```tsx
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:
```tsx
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" }`
```tsx
// 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):
```tsx
{
name: "post",
meta: { label: "Blog Posts" },
}
```
### icon
Icon for menu:
```tsx
import { IconArticle } from "@tabler/icons-react";
{
name: "posts",
meta: { icon: <IconArticle /> },
}
```
### canDelete
Show delete button in CRUD views:
```tsx
{
name: "posts",
meta: { canDelete: true },
}
```
### parent
Nest resource under another (for hierarchical menus):
```tsx
resources={[
{ name: "cms" },
{
name: "posts",
meta: { parent: "cms" },
},
{
name: "categories",
meta: { parent: "cms" },
},
]}
```
### dataProviderName
Specify data provider for multi-provider setups:
```tsx
dataProvider={{
default: defaultDataProvider,
cms: cmsDataProvider,
}}
resources={[
{
name: "posts",
meta: { dataProviderName: "cms" },
},
]}
```
### hide
Hide from menu/sidebar:
```tsx
{
name: "internal-logs",
meta: { hide: true },
}
```
## Full Example with All Options
```tsx
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
```tsx
import { useResource } from "@refinedev/core";
const { resource, resources, identifier } = useResource();
// Get specific resource
const { resource: postsResource } = useResource("posts");
```
### useResourceParams Hook
```tsx
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)
```tsx
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:
```tsx
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/1
```
Or use navigation components (from UI package):
```tsx
import { EditButton, ShowButton, DeleteButton } from "@refinedev/mantine";
<EditButton recordItemId={record.id} />
<ShowButton recordItemId={record.id} />
<DeleteButton recordItemId={record.id} />
```
```
### references/routing.md
```markdown
# Routing
Refine integrates with React Router for navigation. The `@refinedev/react-router` package provides the router bindings.
## Installation
```bash
npm install @refinedev/react-router react-router-dom
```
## Basic Setup
```tsx
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:
```tsx
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:
```tsx
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 | Path or resource object |
| `query` | object | Query parameters |
| `type` | "push" \| "replace" \| "path" | Navigation type |
| `hash` | string | URL hash |
| `options.keepQuery` | boolean | Merge with existing query |
| `options.keepHash` | boolean | Keep existing hash |
### useGetToPath
Get path without navigating:
```tsx
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:
```tsx
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:
```tsx
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
```tsx
<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]=asc
```
### Manual Sync in useTable
```tsx
const table = useTable({
refineCoreProps: {
syncWithLocation: true,
},
});
```
## Protected Routes
```tsx
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
```tsx
import { ErrorComponent } from "@refinedev/mantine";
<Routes>
{/* ... other routes */}
<Route path="*" element={<ErrorComponent />} />
</Routes>
```
## Index Redirect
```tsx
import { NavigateToResource } from "@refinedev/react-router";
<Routes>
<Route index element={<NavigateToResource resource="posts" />} />
{/* ... */}
</Routes>
// Redirects "/" to the list page of "posts" resource
```
## Custom Route Params
Resources can have custom parameters:
```tsx
// 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:
```tsx
// In PostEdit component (rendered at /posts/edit/:id)
const { action } = useResourceParams();
// action = "edit"
// useForm auto-detects action
const form = useForm(); // Automatically uses "edit" action
```
```
### references/hooks.md
```markdown
````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
```tsx
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 count
```
### useOne — Single Record
```tsx
const { data } = useOne({ resource: "posts", id: 1 });
// data.data — the record
```
### useMany — Multiple Records by IDs
```tsx
const { data } = useMany({ resource: "posts", ids: [1, 2, 3] });
```
### useCreate / useUpdate / useDelete — Mutations
```tsx
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
```tsx
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
```tsx
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
```tsx
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" \| "clone" | Auto-detected from route |
| `id` | string \| number | Record ID (edit/clone) |
| `redirect` | "list" \| "edit" \| "show" \| false | After success |
| `mutationMode` | "pessimistic" \| "optimistic" \| "undoable" | Behavior |
| `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
```tsx
const { refineCore: { autoSaveProps } } = useForm({
refineCoreProps: {
autoSave: { enabled: true, debounce: 1000 },
},
});
// autoSaveProps.status: "loading" | "error" | "idle" | "success"
```
---
## useTable — List with TanStack Table
```bash
npm install @refinedev/react-table @tanstack/react-table
```
```tsx
import { 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>
</>
);
};
```
### Filtering
```tsx
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
```tsx
{
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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):
```ts
{ data, isLoading, isFetching, isError, error, refetch }
```
**Mutation hooks** (useCreate, useUpdate, useDelete):
```ts
{ mutate, mutateAsync, isLoading, isSuccess, isError, error, data }
```
**Query invalidation**: Mutations auto-invalidate related queries. Override with `invalidates: ["list", "detail"] | ["all"] | false`.
````
```
### references/auth.md
```markdown
````markdown
# Authentication & Access Control
## Auth Provider Interface
```tsx
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
```tsx
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
```tsx
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
```tsx
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)
```tsx
import { AuthPage } from "@refinedev/mantine";
<AuthPage
type="login" // "login" | "register" | "forgotPassword" | "updatePassword"
title="Welcome"
rememberMe={true}
forgotPasswordLink="/forgot-password"
registerLink="/register"
/>
```
---
## Access Control Provider
```tsx
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)
```tsx
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)
```tsx
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
```tsx
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
```tsx
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 } }` |
```tsx
// Hide instead of disable
<EditButton recordItemId={1} accessControl={{ hideIfUnauthorized: true }} />
```
## Casbin Integration
```tsx
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
```tsx
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
```tsx
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action }) => { /* ... */ },
options: {
queryOptions: {
staleTime: 5 * 60 * 1000, // Cache 5 min
cacheTime: 10 * 60 * 1000,
},
},
};
```
````
```
### references/mantine-ui.md
```markdown
# Mantine UI Integration
Refine provides seamless integration with Mantine v5 for building admin interfaces.
## Installation
```bash
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-table
```
## Basic Setup
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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 |
```tsx
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 |
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
import { ErrorComponent } from "@refinedev/mantine";
<Route path="*" element={<ErrorComponent />} />
```
```
### references/inferencer.md
```markdown
# Inferencer (Code Generation)
Inferencer automatically generates CRUD pages based on your API response structure.
## Installation
```bash
npm install @refinedev/inferencer
```
## Basic Usage
```tsx
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
```tsx
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
```tsx
// 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:
```tsx
<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
```tsx
<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
```
### references/notifications.md
```markdown
# Notification Provider Reference
Refine's notification system provides toast notifications for user feedback on CRUD operations.
## NotificationProvider Interface
```ts
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:
```tsx
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:
```tsx
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
```tsx
const { mutate } = useCreate();
mutate({
resource: "posts",
values: { title: "New Post" },
successNotification: false, // Disable success toast
errorNotification: false, // Disable error toast
});
```
### Custom Notification Messages
```tsx
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:
```tsx
<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
```tsx
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:
```tsx
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
```tsx
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
```tsx
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>;
}
```
```
### references/i18n.md
```markdown
# i18n Provider
Refine supports internationalization through a pluggable i18nProvider interface.
## Interface
```ts
interface I18nProvider {
translate: (key: string, options?: any, defaultMessage?: string) => string;
changeLocale: (locale: string, options?: any) => Promise<any>;
getLocale: () => string;
}
```
## Setup with react-i18next
```bash
npm install react-i18next i18next i18next-http-backend i18next-browser-languagedetector
```
```tsx
// 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
```json
// 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
```tsx
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
```tsx
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) => Promise
```
## Locale Switcher
```tsx
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:
```json
{
"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
```tsx
<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
```tsx
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>;
}
```
```
### references/realtime.md
```markdown
# Live / Realtime
Refine supports real-time data updates via `liveProvider`.
## Provider Interface
```tsx
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
```tsx
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
```bash
npm install @refinedev/ably ably
```
```tsx
import { 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
```tsx
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
```tsx
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
```tsx
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
```tsx
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:
```tsx
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:
```tsx
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
```tsx
<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
},
},
]}
/>
```
```