Skip to main content
Refine’s headless approach allows you to build completely custom user interfaces without being tied to any specific UI framework. All the business logic, data fetching, routing, and state management are handled by Refine’s core hooks.

Why Headless?

Using Refine in headless mode gives you:
  • Complete Design Freedom: Build any UI you want with any styling approach
  • Framework Agnostic: Use with any React UI library or custom components
  • Minimal Bundle Size: Only include the core logic without UI dependencies
  • Full Control: Control every aspect of the markup and styling
  • Easy Integration: Integrate Refine into existing applications

Installation

npm install @refinedev/core

Basic Setup

The headless setup only requires the core package and a data provider:
import { Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";

function App() {
  return (
    <Refine
      dataProvider={dataProvider("https://api.example.com")}
      resources={[
        {
          name: "products",
          list: "/products",
          create: "/products/create",
          edit: "/products/edit/:id",
          show: "/products/show/:id",
        },
      ]}
    >
      {/* Your custom components */}
    </Refine>
  );
}

Core Hooks

Refine provides powerful hooks for building CRUD interfaces:

Data Hooks

useList

Fetch and display lists of records with filtering, sorting, and pagination:
import { useList } from "@refinedev/core";

function ProductList() {
  const { data, isLoading } = useList({
    resource: "products",
    pagination: { current: 1, pageSize: 10 },
    sorters: [{ field: "createdAt", order: "desc" }],
    filters: [
      { field: "status", operator: "eq", value: "active" },
    ],
  });

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <h1>Products</h1>
      <ul>
        {data?.data.map((product) => (
          <li key={product.id}>
            {product.name} - ${product.price}
          </li>
        ))}
      </ul>
    </div>
  );
}

useOne

Fetch a single record:
import { useOne } from "@refinedev/core";

function ProductShow({ id }: { id: string }) {
  const { data, isLoading } = useOne({
    resource: "products",
    id,
  });

  if (isLoading) return <div>Loading...</div>;

  const product = data?.data;

  return (
    <div>
      <h1>{product.name}</h1>
      <p>Price: ${product.price}</p>
      <p>Description: {product.description}</p>
    </div>
  );
}

useCreate

Create new records:
import { useCreate } from "@refinedev/core";
import { useState } from "react";

function ProductCreate() {
  const [name, setName] = useState("");
  const [price, setPrice] = useState("");
  const { mutate, isLoading } = useCreate();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    mutate({
      resource: "products",
      values: { name, price: parseFloat(price) },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <h1>Create Product</h1>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Product name"
      />
      <input
        value={price}
        onChange={(e) => setPrice(e.target.value)}
        placeholder="Price"
        type="number"
      />
      <button type="submit" disabled={isLoading}>
        {isLoading ? "Creating..." : "Create"}
      </button>
    </form>
  );
}

useUpdate

Update existing records:
import { useUpdate, useOne } from "@refinedev/core";
import { useState, useEffect } from "react";

function ProductEdit({ id }: { id: string }) {
  const { data } = useOne({ resource: "products", id });
  const [name, setName] = useState("");
  const { mutate, isLoading } = useUpdate();

  useEffect(() => {
    if (data?.data) {
      setName(data.data.name);
    }
  }, [data]);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    mutate({
      resource: "products",
      id,
      values: { name },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <h1>Edit Product</h1>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <button type="submit" disabled={isLoading}>
        {isLoading ? "Saving..." : "Save"}
      </button>
    </form>
  );
}

useDelete

Delete records:
import { useDelete } from "@refinedev/core";

function DeleteButton({ id }: { id: string }) {
  const { mutate, isLoading } = useDelete();

  const handleDelete = () => {
    if (window.confirm("Are you sure?")) {
      mutate({ resource: "products", id });
    }
  };

  return (
    <button onClick={handleDelete} disabled={isLoading}>
      {isLoading ? "Deleting..." : "Delete"}
    </button>
  );
}

Form Hooks

useForm

Handle forms with validation and submission:
import { useForm } from "@refinedev/core";

function ProductForm() {
  const {
    refineCore: { onFinish, formLoading },
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    refineCoreProps: {
      resource: "products",
      action: "create",
    },
  });

  return (
    <form onSubmit={handleSubmit(onFinish)}>
      <input
        {...register("name", { required: "Name is required" })}
        placeholder="Product name"
      />
      {errors.name && <span>{errors.name.message}</span>}

      <input
        {...register("price", { required: "Price is required" })}
        placeholder="Price"
        type="number"
      />
      {errors.price && <span>{errors.price.message}</span>}

      <button type="submit" disabled={formLoading}>
        Submit
      </button>
    </form>
  );
}

useNavigation

Navigate between pages programmatically:
import { useNavigation } from "@refinedev/core";

function ProductActions({ id }: { id: string }) {
  const { edit, show, list, create } = useNavigation();

  return (
    <div>
      <button onClick={() => list("products")}>Back to List</button>
      <button onClick={() => show("products", id)}>View</button>
      <button onClick={() => edit("products", id)}>Edit</button>
      <button onClick={() => create("products")}>Create New</button>
    </div>
  );
}

Table Hooks

useTable

Manage table state with sorting, filtering, and pagination:
import { useTable } from "@refinedev/core";

function ProductTable() {
  const {
    tableQueryResult: { data, isLoading },
    current,
    pageSize,
    setCurrent,
    filters,
    setFilters,
    sorters,
    setSorters,
  } = useTable({
    resource: "products",
    pagination: { current: 1, pageSize: 10 },
  });

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <input
        placeholder="Search products..."
        onChange={(e) =>
          setFilters([
            { field: "name", operator: "contains", value: e.target.value },
          ])
        }
      />

      <table>
        <thead>
          <tr>
            <th onClick={() => setSorters([{ field: "name", order: "asc" }])}>
              Name
            </th>
            <th onClick={() => setSorters([{ field: "price", order: "asc" }])}>
              Price
            </th>
          </tr>
        </thead>
        <tbody>
          {data?.data.map((product) => (
            <tr key={product.id}>
              <td>{product.name}</td>
              <td>${product.price}</td>
            </tr>
          ))}
        </tbody>
      </table>

      <div>
        <button
          onClick={() => setCurrent(current - 1)}
          disabled={current === 1}
        >
          Previous
        </button>
        <span>Page {current}</span>
        <button onClick={() => setCurrent(current + 1)}>Next</button>
      </div>
    </div>
  );
}

Advanced Features

Authentication

import { useLogin, useLogout, useGetIdentity } from "@refinedev/core";

function AuthButtons() {
  const { mutate: login } = useLogin();
  const { mutate: logout } = useLogout();
  const { data: user } = useGetIdentity();

  if (user) {
    return (
      <div>
        Welcome, {user.name}!
        <button onClick={() => logout()}>Logout</button>
      </div>
    );
  }

  return (
    <button onClick={() => login({ email: "[email protected]" })}>
      Login
    </button>
  );
}

Access Control

import { useCan } from "@refinedev/core";

function ProductActions({ id }: { id: string }) {
  const { data: canEdit } = useCan({
    resource: "products",
    action: "edit",
    params: { id },
  });

  const { data: canDelete } = useCan({
    resource: "products",
    action: "delete",
    params: { id },
  });

  return (
    <div>
      {canEdit?.can && <button>Edit</button>}
      {canDelete?.can && <button>Delete</button>}
    </div>
  );
}

Optimistic Updates

import { useUpdate } from "@refinedev/core";

function ToggleStatus({ id, currentStatus }: { id: string; currentStatus: boolean }) {
  const { mutate } = useUpdate();

  const handleToggle = () => {
    mutate(
      {
        resource: "products",
        id,
        values: { status: !currentStatus },
      },
      {
        optimisticUpdateMap: {
          list: true,
          detail: true,
        },
      },
    );
  };

  return (
    <button onClick={handleToggle}>
      {currentStatus ? "Active" : "Inactive"}
    </button>
  );
}

Integration Examples

function ProductCard({ product }) {
  return (
    <div className="bg-white rounded-lg shadow-md p-6">
      <h2 className="text-2xl font-bold mb-2">{product.name}</h2>
      <p className="text-gray-600 mb-4">{product.description}</p>
      <span className="text-xl font-semibold text-blue-600">
        ${product.price}
      </span>
    </div>
  );
}

Best Practices

  1. Type Safety: Use TypeScript for better development experience
  2. Error Handling: Always handle loading and error states
  3. Optimistic Updates: Use for better perceived performance
  4. Code Splitting: Lazy load routes and components
  5. Memoization: Use React.memo and useMemo for expensive computations

Next Steps

Core Hooks

Explore all available hooks

Data Provider

Learn about data providers

Authentication

Implement authentication

Examples

See headless examples

Build docs developers (and LLMs) love