Skip to main content
πŸ‘€ Interested in the latest enterprise backend features of refine? πŸ‘‰ Join now and get early access!
Setting Up the Client App
Fullstack Developer
16 min read

Setting Up the Client App

In this episode, we initialize our Pixels app using refine and get familiar with the boilerplate code to be created with the create refine-app CLI tool.

This is Day 2 of the refineWeek series. refineWeek is a seven-part tutorial that aims to help developers learn the ins-and-outs of refine's powerful capabilities and get going with refine within a week.

refineWeek series​

Overview​

In the previous post, we got a preview of refine's underlying architecture, especially on how refine's core modules abstract and divide an app's logic inside individual providers and allow their methods to be easily accessed and executed with hooks from inside consumer components. This abstraction at the providers layer is where refine shines and require extensive configuration to begin with.

In this part, we will get into the details of two important providers: namely, the dataProvider and authProvider props of our <Refine /> component. We will be building on this knowledge in the coming episodes.

The providers will be generated by the create refine-app CLI tool based on our choice, so we'll start off with setting up the Pixels app right away.

Project Setup​

For this project, we are using a PostgreSQL database hosted in the Supabase cloud. refine comes with an optional package for Supabase that gives us dataProvider and authProviders out-of-the-box for handling requests for CRUD actions, authentication and authorization against models hosted in a Supabase server.

We are going to include refine's Ant Design package for the UI side.

Let's go ahead and use the create refine-app CLI tool to interactively initialize the project. Navigate to a folder of your choice and run:

npm create refine-app@latest pixels

create refine-app presents us with a set of questions for choosing the libraries and frameworks we want to work with.

So, I chose the following options:

βœ” Choose a project template Β· refine(Vite)
βœ” What would you like to name your project?: Β· pixels
βœ” Choose your backend service to connect: Β· Supabase
βœ” Do you want to use a UI Framework?: Β· Ant Design
βœ” Do you want to add example pages?: Β· no
βœ” Do you need i18n (Internationalization) support?: Β· no
βœ” Choose a package manager: Β· npm

This should create a rudimentary refine app that supports Ant Design in the UI and Supabase in the backend. If we open the app in our code editor, we can see that refine's optional packages for Ant Design and Supabase are added to package.json:

package.json
"dependencies": {
"@refinedev/antd": "^5.7.0",
"@refinedev/core": "^4.5.8",
"@refinedev/react-router-v6": "^4.1.0",
"@refinedev/supabase": "^5.0.0",
}

We are going to use Ant Design components for our UI thanks to the @refinedev/antd module. @refinedev/supabase module allows us to use refine's Supabase auth and data providers.

We'll cover these Supabase related providers as we add features to our app in the upcoming episodes. However, let's try building the app for now, and check what we have in the browser after running the development server. In the terminal, run the following command:

npm run dev

After that, navigate to http://localhost:5173, and lo and behold! we have a refine app:

react crud app welcome

Exploring the App​

Let's now see what refine scaffolded for us during initialization.

Our main point of focus is the src folder. And for now, especially the <App /> component.

If we look inside the App.tsx file, we can see a <Refine /> component crowded with passed in props:

src/App.tsx
import { GitHubBanner, Refine, WelcomePage } from "@refinedev/core";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";

import { notificationProvider } from "@refinedev/antd";
import "@refinedev/antd/dist/reset.css";

import routerBindings, {
DocumentTitleHandler,
UnsavedChangesNotifier,
} from "@refinedev/react-router-v6";
import { dataProvider, liveProvider } from "@refinedev/supabase";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import authProvider from "./authProvider";
import { ColorModeContextProvider } from "./contexts/color-mode";
import { supabaseClient } from "./utility";

function App() {
return (
<BrowserRouter>
<GitHubBanner />
<RefineKbarProvider>
<ColorModeContextProvider>
<Refine
dataProvider={dataProvider(supabaseClient)}
liveProvider={liveProvider(supabaseClient)}
authProvider={authProvider}
routerProvider={routerBindings}
notificationProvider={notificationProvider}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
}}
>
<Routes>
<Route index element={<WelcomePage />} />
</Routes>
<RefineKbar />
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
</ColorModeContextProvider>
</RefineKbarProvider>
</BrowserRouter>
);
}

export default App;

Today, we'll examine a few of these props so that we are ready to move to the next episode.

The <Refine /> Component​

The <Refine /> component is the entry point of a refine app. In order to leverage the power of refine's abstraction layers, we need to have the <Refine /> component.

Then we have to configure the <Refine /> component with the provider objects we want to use in our app. We can see that create refine-app already added the props for us inside <Refine /> out-of-the-box.

We will be using them in our Pixels app. Some provider objects like the routerProvider or the dataProvider are defined for us by refine's core or support modules and some like the accessControlProvider have to be defined by ourselves.

CAUTION

<Refine /> comes with dark mode support out-of-the-box. However, we will not be using it in this series. So, we will be replace the ColorModeContextProvider with the ConfigProvider.

Also You can remove src/context/color-mode that comes with create refine-app.

src/App.tsx
// ...
- import { ColorModeContextProvider } from "./contexts/color-mode";
+ import { ConfigProvider } from "antd";

function App() {
return (
// ...
- <ColorModeContextProvider>
+ <ConfigProvider>
<Refine
// ...
>
{/* ... */}
</Refine>
- </ColorModeContextProvider>
+ </ConfigProvider>
// ...
);
}

<Refine />'s dataProvider Prop​

refine's data provider is the context which allows the app to communicate with a backend API via a HTTP client. It subsequently makes response data returned from HTTP requests available to consumer components via a set of refine data hooks.

If we look closely, our dataProvider prop derives a value from a call to dataProvider(supabaseClient):

src/App.tsx
import { Refine } from "@refinedev/core";
import { dataProvider } from "@refinedev/supabase";

import { supabaseClient } from "./utility";

function App() {
return <Refine dataProvider={dataProvider(supabaseClient)} />;
}

The returned object, also called the dataProvider object, has the following signature:

Show data provider object signature

const dataProvider = {
create: ({ resource, variables, meta }) => Promise,
createMany: ({ resource, variables, meta }) => Promise,
deleteOne: ({ resource, id, variables, meta }) => Promise,
deleteMany: ({ resource, ids, variables, meta }) => Promise,
getList: ({ resource, pagination, hasPagination, sort, filters, meta }) =>
Promise,
getMany: ({ resource, ids, meta }) => Promise,
getOne: ({ resource, id, meta }) => Promise,
update: ({ resource, id, variables, meta }) => Promise,
updateMany: ({ resource, ids, variables, meta }) => Promise,
custom: ({ url, method, sort, filters, payload, query, headers, meta }) =>
Promise,
getApiUrl: () => "",
};

Each item in this object is a method that has to be defined by us or refine's packages.

refine supports 15+ backend dataProvider integrations as optional packages that come with distinct definitions of these methods that handle CRUD operations according to their underlying architectures. The full list can be found here.

Normally, for our own backend API, we have to define each method we need for sending http requests inside a dataProvider object as above. But since we are using the @refinedev/supabase package, dataProvider={dataProvider(supabaseClient)} makes the following object available to us:

Show refine supabase data provider source code

@refinedev/supabase/src/index.ts
import { DataProvider } from "@refinedev/core";
import { SupabaseClient } from "@supabase/supabase-js";
import { generateFilter, handleError } from "../utils";

export const dataProvider = (
supabaseClient: SupabaseClient,
): Required<DataProvider> => {
return {
getList: async ({ resource, pagination, filters, sorters, meta }) => {
const {
current = 1,
pageSize = 10,
mode = "server",
} = pagination ?? {};

const query = supabaseClient
.from(resource)
.select(meta?.select ?? "*", {
count: "exact",
});

if (mode === "server") {
query.range((current - 1) * pageSize, current * pageSize - 1);
}

sorters?.map((item) => {
const [foreignTable, field] = item.field.split(/\.(.*)/);

if (foreignTable && field) {
query
.select(meta?.select ?? `*, ${foreignTable}(${field})`)
.order(field, {
ascending: item.order === "asc",
foreignTable: foreignTable,
});
} else {
query.order(item.field, {
ascending: item.order === "asc",
});
}
});

filters?.map((item) => {
generateFilter(item, query);
});

const { data, count, error } = await query;

if (error) {
return handleError(error);
}

return {
data: data || [],
total: count || 0,
} as any;
},

getMany: async ({ resource, ids, meta }) => {
const query = supabaseClient
.from(resource)
.select(meta?.select ?? "*");

if (meta?.idColumnName) {
query.in(meta.idColumnName, ids);
} else {
query.in("id", ids);
}

const { data, error } = await query;

if (error) {
return handleError(error);
}

return {
data: data || [],
} as any;
},

create: async ({ resource, variables, meta }) => {
const query = supabaseClient.from(resource).insert(variables);

if (meta?.select) {
query.select(meta.select);
}

const { data, error } = await query;

if (error) {
return handleError(error);
}

return {
data: (data || [])[0] as any,
};
},

createMany: async ({ resource, variables, meta }) => {
const query = supabaseClient.from(resource).insert(variables);

if (meta?.select) {
query.select(meta.select);
}

const { data, error } = await query;

if (error) {
return handleError(error);
}

return {
data: data as any,
};
},

update: async ({ resource, id, variables, meta }) => {
const query = supabaseClient.from(resource).update(variables);

if (meta?.idColumnName) {
query.eq(meta.idColumnName, id);
} else {
query.match({ id });
}

if (meta?.select) {
query.select(meta.select);
}

const { data, error } = await query;
if (error) {
return handleError(error);
}

return {
data: (data || [])[0] as any,
};
},

updateMany: async ({ resource, ids, variables, meta }) => {
const response = await Promise.all(
ids.map(async (id) => {
const query = supabaseClient
.from(resource)
.update(variables);

if (meta?.idColumnName) {
query.eq(meta.idColumnName, id);
} else {
query.match({ id });
}

if (meta?.select) {
query.select(meta.select);
}

const { data, error } = await query;
if (error) {
return handleError(error);
}

return (data || [])[0] as any;
}),
);

return {
data: response,
};
},

getOne: async ({ resource, id, meta }) => {
const query = supabaseClient
.from(resource)
.select(meta?.select ?? "*");

if (meta?.idColumnName) {
query.eq(meta.idColumnName, id);
} else {
query.match({ id });
}

const { data, error } = await query;
if (error) {
return handleError(error);
}

return {
data: (data || [])[0] as any,
};
},

deleteOne: async ({ resource, id, meta }) => {
const query = supabaseClient.from(resource).delete();

if (meta?.idColumnName) {
query.eq(meta.idColumnName, id);
} else {
query.match({ id });
}

const { data, error } = await query;
if (error) {
return handleError(error);
}

return {
data: (data || [])[0] as any,
};
},

deleteMany: async ({ resource, ids, meta }) => {
const response = await Promise.all(
ids.map(async (id) => {
const query = supabaseClient.from(resource).delete();

if (meta?.idColumnName) {
query.eq(meta.idColumnName, id);
} else {
query.match({ id });
}

const { data, error } = await query;
if (error) {
return handleError(error);
}

return (data || [])[0] as any;
}),
);

return {
data: response,
};
},

getApiUrl: () => {
throw Error("Not implemented on refine-supabase data provider.");
},

custom: () => {
throw Error("Not implemented on refine-supabase data provider.");
},
};
};

We don't have to get into the mind of the people at refine yet, but if we skim over closely, the dataProvider object above has pretty much every method we need to perform all CRUD operations against a Supabase database. Notable methods we are going to use in our app are: create(), getOne(), getList() and update().

For the details of how these methods work, please take your time to scan through the dataProvider API reference.

In order to get the Supabase dataProvider object to deliver, first a supabaseClient has to be set up.

refine's supabaseClient​

If we look inside src/utility/, we have a supabaseClient.ts file containing the credentials of a client that provides us access to a Supabase backend:

src/utility/supabaseClient.ts
import { createClient } from "@refinedev/supabase";

const SUPABASE_URL = "https://ifbdnkfqbypnkmwcfdes.supabase.co";
const SUPABASE_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImlmYmRua2ZxYnlwbmttd2NmZGVzIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NzA5MTgzOTEsImV4cCI6MTk4NjQ5NDM5MX0.ThQ40H-xay-Hi5cf7H9mKccMCvAX3iCvYVJDe0KiHtw";

export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY, {
db: {
schema: "public",
},
auth: {
persistSession: true,
},
});

This file was also generated for us by create refine-app using refine's Supabase package.

Inside <Refine /> component, we are getting the value of the dataProvider prop by passing in supabaseClient to the dataProvider() function imported from this package:

App.tsx
import { Refine } from "@refinedev/core";
import { dataProvider } from "@refinedev/supabase";

import { supabaseClient } from "./utility";

function App() {
return <Refine dataProvider={dataProvider(supabaseClient)} />;
}

We need to tweak the supabaseClient.ts file with our own credentials, which we will do when we add resources to our app.

If we inspect further, setting up Supabase with refine helps us enable not only the dataProvider prop, but also the authProvider and liveProvider props inside <Refine />. This is because they all depend on supabaseClient to send http requests. We'll explore the liveProvider prop on Day 4, but let's also look at the authProvider here to enhance our understanding.

<Refine />'s authProvider Prop​

We can clearly see in our <Refine /> component that create refine-app already enabled the authProvider prop by passing in the corresponding object for us:

App.tsx
authProvider = { authProvider };

Earlier on, the authProvider object was already created by create refine-app inside the authProvider.ts file:

Show refine supabase auth provider source code

src/authProvider.ts
import { AuthBindings } from "@refinedev/core";

import { supabaseClient } from "../utility";

export const authProvider: AuthBindings = {
login: async ({ email, password, providerName }) => {
try {
// sign in with oauth
if (providerName) {
const { data, error } =
await supabaseClient.auth.signInWithOAuth({
provider: providerName,
});

if (error) {
return {
success: false,
error,
};
}

if (data?.url) {
return {
success: true,
};
}
}

// sign in with email and password
const { data, error } =
await supabaseClient.auth.signInWithPassword({
email,
password,
});

if (error) {
return {
success: false,
error,
};
}

if (data?.user) {
return {
success: true,
};
}
} catch (error: any) {
return {
success: false,
error,
};
}

return {
success: false,
error: {
message: "Login failed",
name: "Invalid email or password",
},
};
},
register: async ({ email, password }) => {
try {
const { data, error } = await supabaseClient.auth.signUp({
email,
password,
});

if (error) {
return {
success: false,
error,
};
}

if (data) {
return {
success: true,
};
}
} catch (error: any) {
return {
success: false,
error,
};
}

return {
success: false,
error: {
message: "Register failed",
name: "Invalid email or password",
},
};
},
forgotPassword: async ({ email }) => {
try {
const { data, error } =
await supabaseClient.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/update-password`,
});

if (error) {
return {
success: false,
error,
};
}

if (data) {
return {
success: true,
};
}
} catch (error: any) {
return {
success: false,
error,
};
}

return {
success: false,
error: {
message: "Forgot password failed",
name: "Invalid email",
},
};
},
updatePassword: async ({ password }) => {
try {
const { data, error } = await supabaseClient.auth.updateUser({
password,
});

if (error) {
return {
success: false,
error,
};
}

if (data) {
return {
success: true,
redirectTo: "/",
};
}
} catch (error: any) {
return {
success: false,
error,
};
}

return {
success: false,
error: {
message: "Update password failed",
name: "Invalid password",
},
};
},
logout: async () => {
const { error } = await supabaseClient.auth.signOut();

if (error) {
return {
success: false,
error,
};
}

return {
success: true,
redirectTo: "/",
};
},
onError: async (_error: any) => ({}),
check: async () => {
try {
const { data } = await supabaseClient.auth.getSession();
const { session } = data;

if (!session) {
return {
authenticated: false,
error: {
message: "Check failed",
name: "Session not found",
},
logout: true,
};
}
} catch (error: any) {
return {
authenticated: false,
error: error,
logout: true,
};
}

return {
authenticated: true,
};
},
getPermissions: async () => {
try {
const user = await supabaseClient.auth.getUser();

if (user) {
return user.data.user?.role;
}
} catch (error) {
console.error(error);
return;
}
},
getIdentity: async () => {
try {
const { data } = await supabaseClient.auth.getUser();

if (data?.user) {
return {
...data.user,
name: data.user.email,
};
}

return null;
} catch (error: any) {
console.error(error);

return null;
}
},
};

This object has all the methods we need to implement an email / password based authentication and authorization system in our app.

Notice, as mentioned before, that authProvider relies on supabaseClient to connect to our Supabase database. So, in this case, our authProvider was generated as part of the Supabase package.

As we can infer by now, although we have stated that refine performs and manages a lot of heavylifting and simplifies the app logic by dividing concerns into separate contexts, providers and hooks, configuring all these providers is a heavy task itself.

It, fortunately, makes configuration easier by composing individual providers inside a single object.

These are pretty much the essentials we should get familiar with in order to accept the invitation to add resources to the <Refine /> component.

Summary​

In this post, we went through the process of initializing our Pixels app with a Supabase hosted PostgreSQL database and Ant Design UI framework.

We then explored the boilerplate code created by create refine-app using refine's Supabase support package, especially the files related to dataProvider and authProvider props of the <Refine /> component. We touched on setting supabaseClient which is used by these providers to send HTTP requests to the Supabase backend.

In the next article, we will use these providers to implement RESTful CRUD actions for creating a canvas, showing a canvas, drawing pixels on it and showing a public gallery that lists canvases. We will also add authentication to our app.

Click here to read "Adding CRUD Actions and Authentication" article. β†’

Related Articles

Dynamic Forms with React Hook Form

How to build dynamic forms with React hook form in React CRUD apps.

Setting Up the Invoicer App

We start with setting up the Invoicer app by choosing Ant Design as a UI framework and Strapi as a dataprovider

Build internal tools using Low-Code with refine, React-based framework

Why you should be using low-code app Refine to build internal tools? Learn how to build low-code apps using Refine, React and Ant Design.