Back to Blog
Tired of Reinventing the Admin Panel? Refine Might Be Your New Best Friend.
8 min readJun 13, 20266 views

Tired of Reinventing the Admin Panel? Refine Might Be Your New Best Friend.

Building internal tools often feels like Groundhog Day. You spend weeks wiring up forms, tables, and CRUD operations, only to repeat the exact same patterns on the next project. Refine aims to solve this by providing a flexible React framework that gets you to production faster, without sacrificing

FrontendReactToolingWeb Development
Share

by Sunil Band

The Internal Tool Treadmill

We've all been there: the boss needs an admin panel, a dashboard to track some metrics, or a portal for partners. It starts simple enough. You spin up a React app, pull in a component library, and start writing forms. Before you know it, you're knee-deep in data fetching, pagination, filtering, authentication, authorization, and an endless stream of UI state management. Each project feels like you're building the same thing from scratch, just with different data models.

This isn't just about speed; it's about quality and maintainability. When every internal tool is a bespoke creation, consistency suffers. Bugs crop up because patterns weren't properly abstracted or enforced. Developers get bogged down in boilerplate instead of focusing on the unique business logic that actually matters. This is where a framework like Refine steps in, offering a structured approach to common B2B application patterns without boxing you into a rigid design.

What Makes Refine Different?

Refine isn't a drag-and-drop page builder or a low-code platform. It's a headless React framework, meaning it handles the data layer, routing, authentication, and common UI logic, but leaves the actual presentation layer entirely up to you. This is a crucial distinction. Instead of generating a UI that you then have to fight to customize, Refine provides hooks and components that empower you to build your UI, leveraging its robust backend integration and lifecycle management.

Think of it as a highly opinionated but extremely flexible data-driven layer for your React app. It abstracts away the complexities of connecting to various data sources, handling mutations, and managing client-side state, allowing you to focus purely on the component tree and user experience. It works great with popular UI libraries like Ant Design, Material UI, and Chakra UI, or even your own custom components.

Getting Started with a List Page

Let's walk through a simple example. Imagine we need an admin panel to manage a list of posts. With Refine, setting this up is incredibly streamlined. First, you'd typically initialize a Refine project, which sets up the basic structure and dependencies.

bash
npx create-refine-app@latest -- -t refine-antd

This command uses the Ant Design template, one of Refine's officially supported UI integrations. Once the project is set up and dependencies are installed, you'll find an App.tsx where you configure your Refine app, primarily defining your data provider and resources.

typescript
// src/App.tsx
import { Refine } from "@refinedev/core";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import { notificationProvider, Layout, ErrorComponent } from "@refinedev/antd";
import routerBindings, { NavigateToResource, UnsavedChangesNotifier } from "@refinedev/react-router-v6";
import dataProvider from "@refinedev/simple-rest"; // A simple REST data provider
import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom";
import { PostList } from "./pages/posts/list";
import { PostCreate } from "./pages/posts/create";
import { PostEdit } from "./pages/posts/edit";
import { PostShow } from "./pages/posts/show";

function App() {
  return (
    <BrowserRouter>
      <RefineKbarProvider>
        <Refine
          dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
          routerProvider={routerBindings}
          notificationProvider={notificationProvider}
          resources={[
            {
              name: "posts",
              list: "/posts",
              create: "/posts/create",
              edit: "/posts/edit/:id",
              show: "/posts/show/:id",
              meta: { canDelete: true }, // Add metadata for permissions or other logic
            },
          ]}
          options={{
            syncWithLocation: true,
            warnWhenUnsavedChanges: true,
            use.</Refine>
          }}
        >
          <Routes>
            <Route
              element={
                <Layout>
                  <Outlet />
                </Layout>
              }
            >
              <Route index element={<NavigateToResource resource="posts" />} />
              <Route path="/posts">
                <Route index element={<PostList />} />
                <Route path="create" element={<PostCreate />} />
                <Route path="edit/:id" element={<PostEdit />} />
                <Route path="show/:id" element={<PostShow />} />
              </Route>
              <Route path="*" element={<ErrorComponent />} />
            </Route>
          </Routes>
          <RefineKbar />
          <UnsavedChangesNotifier />
        </Refine>
      </RefineKbarProvider>
    </BrowserRouter>
  );
}

export default App;

In this App.tsx, we define a resource named posts. This tells Refine about our data entity and its associated routes. The dataProvider is set to simple-rest, pointing to a fake API. Refine supports many data providers out of the box (REST, GraphQL, Supabase, Strapi, etc.), and you can easily write your own.

Now, let's create the PostList component. This is where Refine's power really shines.

typescript
// src/pages/posts/list.tsx
import React from "react";
import {
  IResourceComponentsProps,
  BaseRecord,
  useMany,
  useNavigation,
} from "@refinedev/core";
import {
  useTable,
  List,
  EditButton,
  ShowButton,
  DeleteButton,
  Space,
  Table,
  Tag,
  // other Ant Design components
} from "@refinedev/antd";
import { ColumnsType } from "antd/lib/table";

export const PostList: React.FC<IResourceComponentsProps> = () => {
  const { tableProps } = useTable({
    syncWithLocation: true, // Keep URL params in sync with table state (pagination, filters)
  });

  const { show, edit } = useNavigation(); // Hooks to navigate to show/edit pages

  const columns: ColumnsType<BaseRecord> = [
    {
      dataIndex: "id",
      title: "ID",
      sorter: true,
    },
    {
      dataIndex: "title",
      title: "Title",
      sorter: true,
    },
    {
      dataIndex: "status",
      title: "Status",
      render: (value: string) => <Tag>{value}</Tag>, // Custom rendering for status
    },
    {
      title: "Actions",
      dataIndex: "actions",
      render: (_, record: BaseRecord) => (
        <Space>
          <EditButton hideText size="small" onClick={() => edit("posts", record.id)} />
          <ShowButton hideText size="small" onClick={() => show("posts", record.id)} />
          <DeleteButton hideText size="small" recordItemId={record.id} />
        </Space>
      ),
    },
  ];

  return (
    <List canCreate>
      <Table {...tableProps} rowKey="id" columns={columns} />
    </List>
  );
};

With just a few lines, the useTable hook from Refine gives us pagination, sorting, and filtering out of the box, all synchronized with the URL. The List component provides a standard layout for list pages, including a 'Create' button if canCreate is set. The EditButton, ShowButton, and DeleteButton are also Refine components that automatically handle navigation and data mutations, wiring directly into the posts resource we defined. This is a massive productivity boost compared to implementing all this logic manually.

Data Providers: The Heart of Refine

Refine's true power lies in its data provider architecture. This is the layer that connects your React components to your backend. Refine provides an interface that any data provider must implement:

typescript
interface IDataProvider {
    getMany?: (
        params: GetManyParams
    ) => Promise<GetManyResponse>;
    getOne: (
        params: GetOneParams
    ) => Promise<GetOneResponse>;
    getList: (
        params: GetListParams
    ) => Promise<GetListResponse>;
    create: (
        params: CreateParams
    ) => Promise<CreateResponse>;
    createMany?: (
        params: CreateManyParams
    ) => Promise<CreateManyResponse>;
    update: (
        params: UpdateParams
    ) => Promise<UpdateResponse>;
    updateMany?: (
        params: UpdateManyParams
    ) => Promise<UpdateManyResponse>;
    deleteOne: (
        params: DeleteOneParams
    ) => Promise<DeleteOneResponse>;
    deleteMany?: (
        params: DeleteManyParams
    ) => Promise<DeleteManyResponse>;
    getApiUrl: () => string;
    custom?: (
        params: CustomParams
    ) => Promise<CustomResponse>;
}

By conforming to this interface, Refine can interact with any data source. It comes with official providers for popular services like REST, GraphQL, Supabase, Airtable, Strapi, and more. If your backend isn't directly supported, you can easily write a custom data provider. This separation of concerns means you can swap out your backend without rewriting your frontend data fetching logic.

This is a huge win for larger organizations or projects that might evolve their backend technology stack. Your React components remain largely decoupled from the specifics of how data is stored or retrieved, making your application more resilient to change.

Authentication and Authorization

Internal tools almost always require robust authentication and authorization. Refine has this covered with its auth provider concept, mirroring the flexibility of data providers. You define functions for login, logout, check (to verify authentication status), onError, and getPermissions.

typescript
// Example authProvider (simplified)
import { AuthProvider } from "@refinedev/core";

export const authProvider: AuthProvider = {
  login: async ({ email, password }) => {
    // Call your backend API to authenticate
    if (email === "admin@refine.dev" && password === "refine") {
      localStorage.setItem("my_app_token", "valid-token");
      return { success: true, redirectTo: "/" };
    }
    return { success: false, error: { name: "LoginError", message: "Invalid credentials" } };
  },
  logout: async () => {
    localStorage.removeItem("my_app_token");
    return { success: true, redirectTo: "/login" };
  },
  check: async () => {
    const token = localStorage.getItem("my_app_token");
    if (token) {
      return { authenticated: true };
    }
    return { authenticated: false, redirectTo: "/login" };
  },
  // ... other auth methods like `getPermissions` for role-based access control
};

Once configured, Refine's hooks like useCheckAuth and components like Authenticated will automatically handle redirects and UI rendering based on the user's authentication state. For authorization, the useCan hook allows you to query permissions, enabling fine-grained control over what users can see and do within your application.

This kind of comprehensive yet modular approach means you spend less time wiring up basic security features and more time implementing the actual business rules that define who can access what.

Trade-offs and Considerations

No tool is a silver bullet, and Refine is no exception. While it offers immense productivity gains, there are a few things to keep in mind:

  1. Learning Curve: While designed for flexibility, Refine does introduce its own set of concepts and hooks. If you're completely new to React or haven't worked with data-driven frameworks before, there might be a slight ramp-up time to understand its conventions, especially around data providers and resource management.
  2. Abstraction Layer: For extremely simple applications, the abstraction might feel like overkill. If you literally just need a single static page or a form with no backend interaction, Refine's structure might add unnecessary overhead. It truly shines when you have multiple CRUD operations, complex data relationships, and authentication.
  3. UI Library Coupling: While headless, Refine's documentation and examples often showcase integrations with specific UI libraries like Ant Design or Material UI. If you're committed to a highly custom design system or a less common component library, you'll need to do more work to integrate your components with Refine's hooks, though this is by design and part of its flexibility.

For most internal tools and B2B applications, these trade-offs are well worth the benefits. The consistency and speed you gain in development far outweigh the initial learning or configuration effort.

Wrapping up

If you're building data-intensive applications, especially internal tools, admin panels, or B2B portals, Refine is a serious contender. It solves the common pain points of data fetching, state management, routing, and authentication in a structured yet flexible way, letting you focus on the unique aspects of your product. It effectively abstracts the

More from the blog
Available for projectsReady to make something fun 🎈

Ready to build the next system?Wanna build something awesome together?

Currently accepting high-impact opportunities in frontend engineering and scalable web applications.Got a cool idea rattling around? Let's grab a virtual coffee and turn it into something people love. ☕