Why I switched to Refine
Built admin panels the hard way for years. Create React App, install routing library, build authentication from scratch, create CRUD pages one by one, handle forms manually, manage state with Redux. Weeks of work for a simple dashboard.
Tried Ant Design Pro - powerful but opinionated. Wanted to use Material UI? Tough luck. Custom layouts? Fight the framework.
Then found Refine. Headless React framework for building admin panels. Gives you CRUD, routing, authentication, but stays out of UI decisions. Use any UI library. Build layouts your way. Focus on business logic instead of boilerplate.
Built my last admin panel in a day. Not a joke. Start to finish, with authentication, data tables, forms, filtering, pagination. Would've taken two weeks before.
What Refine actually does
React framework for admin panels and internal tools. Auto-generates CRUD pages from your data. Handles routing, authentication, permissions, data fetching. Headless design means you choose the UI library (Ant Design, Material UI, Tailwind CSS, whatever). Connects to any REST or GraphQL API.
Installation and setup
Quickest way to start with Refine.
Option 1: Create new app (recommended)
# Create Refine app with interactive CLI
npm create refine-app@latest
# Follow the prompts:
# - Project name: my-admin-panel
# - Select framework: React
# - Select UI library: Ant Design / Material UI / Mantine
# - Select backend: REST API / GraphQL / Supabase / NestJS
# - Select example: Basic / CRUD
Option 2: Install into existing project
# Install core package
npm install @refinedev/core
# Install UI library (example: Ant Design)
npm install antd @refinedev/antd
# Install router (if not present)
npm install react-router-dom
Basic setup
// src/App.tsx
import { Refine } from "@refinedev/core";
import { RefineThemes } from "@refinedev/antd";
import { ConfigProvider } from "antd";
import routerProvider from "@refinedev/react-router-v6";
import dataProvider from "@refinedev/simple-rest";
function App() {
return (
{/* Your routes */}
);
}
Core concepts
Understand these and you understand Refine.
1. Data Providers
Abstraction layer between Refine and your backend. One interface, any backend.
// REST API
import dataProvider from "@refinedev/simple-rest";
const dataProvider = simpleRestDataProvider("https://api.example.com");
// GraphQL
import dataProvider from "@refinedev/graphql";
const dataProvider = graphqlDataProvider("https://api.example.com/graphql");
// Supabase
import dataProvider from "@refinedev/supabase";
const dataProvider = supabaseDataProvider(client);
// Use in Refine
Switch backends without changing your UI. Just swap data provider.
2. Resources
Define your data model and routes. Refine generates CRUD pages automatically.
const resources = [
{
name: "users",
list: UserList,
create: UserCreate,
edit: UserEdit,
show: UserShow,
meta: {
canDelete: true,
label: "Users",
},
},
{
name: "products",
list: ProductList,
create: ProductCreate,
edit: ProductEdit,
},
];
Each resource has list, create, edit, show pages. Omit what you don't need.
3. Hooks for data fetching
Built-in hooks for CRUD operations. No need to write fetch logic.
import { useTable, useForm, useShow } from "@refinedev/core";
// List page - fetch data with pagination, sorting, filtering
const { tableProps } = useTable({
resource: "users",
sorters: {
initial: [
{ field: "id", order: "desc" },
],
},
});
// Create/Edit form
const { onFinish, formLoading } = useForm({
resource: "users",
action: "create", // or "edit"
});
// Show page
const { queryResult } = useShow({
resource: "users",
id: userId,
});
4. Authentication
Built-in auth providers for common scenarios.
import { AuthProvider } from "@refinedev/core";
const authProvider: AuthProvider = {
login: async ({ username, password }) => {
const response = await fetch("https://api.example.com/login", {
method: "POST",
body: JSON.stringify({ username, password }),
});
const data = await response.json();
localStorage.setItem("token", data.token);
return Promise.resolve();
},
logout: async () => {
localStorage.removeItem("token");
return Promise.resolve();
},
checkAuth: async () => {
const token = localStorage.getItem("token");
return token ? Promise.resolve() : Promise.reject();
},
checkError: async (error) => {
if (error.status === 401) {
return Promise.reject();
}
return Promise.resolve();
},
getPermissions: async () => {
return Promise.resolve();
},
};
Building a CRUD app
Let's build a user management system from scratch.
Step 1: Define resource
// src/App.tsx
} />
} />
} />
} />
Step 2: List page
// src/pages/users/list.tsx
import { List, useTable } from "@refinedev/antd";
import { Table, Space } from "antd";
export const UserList: React.FC = () => {
const { tableProps } = useTable({
resource: "users",
sorters: {
initial: [{ field: "id", order: "desc" }],
},
});
return (
);
};
Step 3: Create page
// src/pages/users/create.tsx
import { Create, useForm } from "@refinedev/antd";
import { Form, Input, Select, Button } from "antd";
export const UserCreate: React.FC = () => {
const { formProps, saveButtonProps } = useForm({
resource: "users",
action: "create",
});
return (
);
};
Step 4: Edit page
// src/pages/users/edit.tsx
import { Edit, useForm } from "@refinedev/antd";
import { Form, Input, Select } from "antd";
export const UserEdit: React.FC = () => {
const { queryResult } = useShow();
const { data } = queryResult;
const record = data?.data;
const { formProps, saveButtonProps } = useForm({
resource: "users",
action: "edit",
id: record?.id,
});
return (
);
};
Advanced features
Beyond basic CRUD.
1. Filtering and search
import { useTable } from "@refinedev/antd";
import { FilterDropdown, Input, Select } from "@antd";
const { tableProps, searchFormProps } = useTable({
resource: "users",
onSearch: (params) => {
return [
{
field: "name",
operator: "contains",
value: params.name,
},
{
field: "role",
operator: "eq",
value: params.role,
},
];
},
});
(
)}
/>
2. Relationships and foreign keys
// User belongs to a company
{
// Fetch company data
const { data } = useOne({
resource: "companies",
id: value,
});
return data?.data?.name;
}}
/>
3. File uploads
import { Upload } from "antd";
{
try {
const response = await uploadFile(file);
onSuccess(response);
form.setFieldValue("avatar", response.url);
} catch (error) {
onError(error);
}
}}
>
4. Real-time updates
import { useLive } from "@refinedev/core";
const { data } = useLive({
resource: "users",
// WebSocket connection for real-time updates
liveMode: "auto",
});
// Table updates automatically when data changes
Backend integrations
Refine connects to anything.
REST API
import dataProvider from "@refinedev/simple-rest";
const dataProvider = simpleRestDataProvider("https://api.example.com");
GraphQL
import dataProvider from "@refinedev/graphql";
const dataProvider = graphqlDataProvider("https://api.example.com/graphql");
Supabase
import { dataProvider } from "@refinedev/supabase";
import { createClient } from "@supabase/supabase-js";
const supabaseClient = createClient("URL", "KEY");
const dataProvider = supabaseDataProvider(supabaseClient);
NestJS CRUD
import dataProvider from "@refinedev/nestjs-crud";
const dataProvider = nestjsCrudDataProvider("https://api.example.com");
Airtable
import dataProvider from "@refinedev/airtable";
const dataProvider = airtableDataProvider("API_KEY", "BASE_ID");
UI library choices
Refine is headless. Choose any UI library.
Ant Design (most popular)
npm install @refinedev/antd antd
Enterprise-grade components. Great for internal tools. Large component library.
Material UI
npm install @refinedev/mui @mui/material
Google's design system. Clean, modern look. Good documentation.
Mantine
npm install @refinedev/mantine @mantine/core
Modern, customizable. Great hooks library. Active community.
Headless (build your own)
npm install @refinedev/core
Complete control. Build UI with Tailwind CSS, Chakra UI, whatever.
Refine vs alternatives
Quick comparison with other admin frameworks.
| Feature | Refine | Ant Design Pro | React Admin |
|---|---|---|---|
| Learning curve | Moderate | Moderate | Moderate |
| UI flexibility | High (headless) | Low (Ant Design only) | Moderate (Material UI) |
| Backend support | REST, GraphQL, Supabase, Airtable | REST | REST, GraphQL |
| TypeScript support | Excellent | Good | Good |
| CRUD generation | Yes | Partial | Yes |
| Community | Growing | Large | Large |
Common issues and fixes
Stuff that breaks and how to fix it.
Issue: Data not loading in table
Fix: Check data provider URL. Verify API returns correct format. Check browser network tab. Ensure resource name matches API endpoint.
Issue: Create/Edit forms not saving
Fix: Verify form field names match API expected format. Check onFinish is being called. Ensure data provider supports create/update operations.
Issue: Authentication not working
Fix: Verify auth provider methods return correct values. Check checkAuth logic. Ensure login stores token correctly. Verify token format with backend.
Issue: Filters not applied
Fix: Check filter operator values. Verify backend supports filtering. Use correct filter format: { field: "name", operator: "contains", value: "John" }
Issue: TypeScript errors
Fix: Ensure types are imported correctly. Check generic types in hooks: useTable<IUser>(). Update to latest Refine version.
Best practices from production use
Lessons learned from real projects.
1. Start with CLI
Use npm create refine-app@latest for first project. Faster than manual setup. Good way to learn structure.
2. Define resources early
Map out all resources (users, products, orders) before coding. Less refactoring later. Use consistent naming conventions.
3. Create reusable components
Extract common table columns, form fields into reusable components. Reduces boilerplate across resources.
4. Use TypeScript
Refine has excellent TypeScript support. Define interfaces for your data models. Catches errors at compile time.
5. Implement error handling
Use useOnError to handle API errors globally. Show user-friendly notifications.
6. Add loading states
Use formLoading and tableProps.loading to show spinners during data fetching. Better UX.
7. Implement permissions
Use canAccess prop on resources to control access based on user roles. Protect sensitive operations.
Bottom line
Refine isn't magic. It's just a really well-designed abstraction layer for admin panels. But that abstraction saves weeks of development time.
CRUD pages that took days now take hours. Authentication is configured in minutes. Switching backends is a single line change. UI flexibility means my admin panels don't look like generic templates.
Best part: it stays out of your way. Not opinionated about UI. Choose your components, build your layouts. Refine handles the boring stuff - data fetching, routing, state management.
If you're building admin panels, internal tools, or dashboards, give Refine a shot. Start with their example project. Build a simple CRUD app. See how fast it goes.
Been using it for 6 months now. Built 5 admin panels. Each one faster than the last. Can't imagine going back to building from scratch.