This post was created using version 3.x.x of refine. Although we plan to update it with the latest version of refine as soon as possible, you can still benefit from the post in the meantime.
You should know that refine version 4.x.x is backward compatible with version 3.x.x, so there is no need to worry. If you want to see the differences between the two versions, check out the migration guide.
Just be aware that the source code examples in this post have been updated to version 4.x.x.
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
- Day 1 - Pilot & refine architecture
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 authProvider
s 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 -- -b v3
create refine-app
presents us with a set of questions for choosing the libraries and frameworks we want to work with. We'll initialize a refine project with craco
to manage our configurations. Other important things are to be able to customize our Ant Design theme and layout.
So, I chose the following options:
✔ Choose a project template · refine(CRA)
✔ 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 want to add dark mode support?: · no
✔ Do you want to customize the Ant Design layout?: · yes
✔ Do you need i18n (Internationalization) support?: · no
✔ Do you want to add kbar command interface support?: · no
✔ Would you mind sending us your choices so that we can improve superplate? · yes
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
:
"dependencies": {
"@refinedev/antd": "^4.7.0",
"@refinedev/core": "^3.18.0",
"@refinedev/react-router-v6": "^3.18.0",
"@refinedev/supabase": "^3.18.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:3000
, and lo and behold! we have a refine app:
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:
import React from "react";
import { Refine } from "@refinedev/core";
import {
AuthPage,
notificationProvider,
ReadyPage,
ErrorComponent,
} from "@refinedev/antd";
import "@refinedev/antd/dist/reset.css";
import { dataProvider, liveProvider } from "@refinedev/supabase";
import routerProvider from "@refinedev/react-router-v6";
import { supabaseClient } from "utility";
import { Layout } from "components/layout";
import authProvider from "./authProvider";
function App() {
return (
<Refine
dataProvider={dataProvider(supabaseClient)}
liveProvider={liveProvider(supabaseClient)}
authProvider={authProvider}
routerProvider={{
...routerProvider,
routes: [
{
path: "/register",
element: <AuthPage type="register" />,
},
{
path: "/forgot-password",
element: <AuthPage type="forgotPassword" />,
},
{
path: "/update-password",
element: <AuthPage type="updatePassword" />,
},
],
}}
LoginPage={() => (
<AuthPage
type="login"
providers={[
{
name: "google",
label: "Sign in with Google",
},
]}
formProps={{
initialValues: {
email: "info@refine.dev",
password: "refine-supabase",
},
}}
/>
)}
notificationProvider={notificationProvider}
ReadyPage={ReadyPage}
catchAll={<ErrorComponent />}
Layout={Layout}
/>
);
}
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.
<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)
:
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, metaData }) => Promise,
createMany: ({ resource, variables, metaData }) => Promise,
deleteOne: ({ resource, id, variables, metaData }) => Promise,
deleteMany: ({ resource, ids, variables, metaData }) => Promise,
getList: ({
resource,
pagination,
hasPagination,
sort,
filters,
metaData,
}) => Promise,
getMany: ({ resource, ids, metaData }) => Promise,
getOne: ({ resource, id, metaData }) => Promise,
update: ({ resource, id, variables, metaData }) => Promise,
updateMany: ({ resource, ids, variables, metaData }) => Promise,
custom: ({
url,
method,
sort,
filters,
payload,
query,
headers,
metaData,
}) => 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
const dataProvider = (
supabaseClient: SupabaseClient,
): Required<DataProvider> => {
return {
getList: async ({
resource,
hasPagination = true,
pagination = { current: 1, pageSize: 10 },
filters,
sort,
metaData,
}) => {
const { current = 1, pageSize = 10 } = pagination ?? {};
const query = supabaseClient
.from(resource)
.select(metaData?.select ?? "*", {
count: "exact",
});
if (hasPagination) {
query.range((current - 1) * pageSize, current * pageSize - 1);
}
sort?.map((item) => {
const [foreignTable, field] = item.field.split(/\.(.*)/);
if (foreignTable && field) {
query
.select(
metaData?.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, metaData }) => {
const { data, error } = await supabaseClient
.from(resource)
.select(metaData?.select ?? "*")
.in(metaData?.id ?? "id", ids);
if (error) {
return handleError(error);
}
return {
data: data || [],
} as any;
},
create: async ({ resource, variables }) => {
const { data, error } = await supabaseClient
.from(resource)
.insert(variables);
if (error) {
return handleError(error);
}
return {
data: (data || [])[0] as any,
};
},
createMany: async ({ resource, variables }) => {
const { data, error } = await supabaseClient
.from(resource)
.insert(variables);
if (error) {
return handleError(error);
}
return {
data: data as any,
};
},
update: async ({ resource, id, variables, metaData }) => {
const query = supabaseClient.from(resource).update(variables);
if (metaData?.id) {
query.eq(metaData?.id, id);
} else {
query.match({ id });
}
const { data, error } = await query;
if (error) {
return handleError(error);
}
return {
data: (data || [])[0] as any,
};
},
updateMany: async ({ resource, ids, variables, metaData }) => {
const response = await Promise.all(
ids.map(async (id) => {
const query = supabaseClient
.from(resource)
.update(variables);
if (metaData?.id) {
query.eq(metaData?.id, id);
} else {
query.match({ id });
}
const { data, error } = await query;
if (error) {
return handleError(error);
}
return (data || [])[0] as any;
}),
);
return {
data: response,
};
},
getOne: async ({ resource, id, metaData }) => {
const query = supabaseClient
.from(resource)
.select(metaData?.select ?? "*");
if (metaData?.id) {
query.eq(metaData?.id, 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, metaData }) => {
const query = supabaseClient.from(resource).delete();
if (metaData?.id) {
query.eq(metaData?.id, 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, metaData }) => {
const response = await Promise.all(
ids.map(async (id) => {
const query = supabaseClient.from(resource).delete();
if (metaData?.id) {
query.eq(metaData?.id, 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:
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:
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:
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
import { AuthProvider } from "@refinedev/core";
import { supabaseClient } from "utility";
export const authProvider: AuthProvider = {
login: async ({ email, password, providerName }) => {
// sign in with oauth
if (providerName) {
const { data, error } = await supabaseClient.auth.signInWithOAuth({
provider: providerName,
});
if (error) {
return Promise.reject(error);
}
if (data?.url) {
return Promise.resolve(false);
}
}
// sign in with email and password
const { data, error } = await supabaseClient.auth.signInWithPassword({
email,
password,
});
if (error) {
return Promise.reject(error);
}
if (data?.user) {
return Promise.resolve();
}
return Promise.resolve();
},
register: async ({ email, password }) => {
const { data, error } = await supabaseClient.auth.signUp({
email,
password,
});
if (error) {
return Promise.reject(error);
}
if (data) {
return Promise.resolve();
}
},
forgotPassword: async ({ email }) => {
const { data, error } = await supabaseClient.auth.resetPasswordForEmail(
email,
{
redirectTo: `${window.location.origin}/update-password`,
},
);
if (error) {
return Promise.reject(error);
}
if (data) {
return Promise.resolve();
}
},
updatePassword: async ({ password }) => {
const { data, error } = await supabaseClient.auth.updateUser({
password,
});
if (error) {
return Promise.reject(error);
}
if (data) {
return Promise.resolve("/");
}
},
logout: async () => {
const { error } = await supabaseClient.auth.signOut();
if (error) {
return Promise.reject(error);
}
return Promise.resolve("/");
},
checkError: () => Promise.resolve(),
checkAuth: async () => {
await supabaseClient.auth.getSession();
return Promise.resolve();
},
getPermissions: async () => {
const { data } = await supabaseClient.auth.getUser();
const { user } = data;
if (user) {
return Promise.resolve(user.role);
}
},
getUserIdentity: async () => {
const { data } = await supabaseClient.auth.getUser();
const { user } = data;
if (user) {
return Promise.resolve({
...user,
name: user.email,
});
}
return Promise.reject();
},
};
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 authoProvider
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.