#refineweek: Setting Up the Client App

#refineweek: 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. 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:

// package.json

"dependencies": {
  "@pankod/refine-antd": "^4.7.0",
  "@pankod/refine-core": "^3.18.0",
  "@pankod/refine-react-router-v6": "^3.18.0",
  "@pankod/refine-supabase": "^3.18.0",
}

We are going to use Ant Design components for our UI thanks to the @pankod/refine-antd module. @pankod/refine-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:

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 React from "react";
import { Refine } from "@pankod/refine-core";
import {
    AuthPage,
    notificationProvider,
    ReadyPage,
    ErrorComponent,
} from "@pankod/refine-antd";
import "@pankod/refine-antd/dist/reset.css";

import { dataProvider, liveProvider } from "@pankod/refine-supabase";
import routerProvider from "@pankod/refine-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):

// src/App.tsx

dataProvider={dataProvider(supabaseClient)}

The returned object, also called the dataProvider object, has the following 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 @pankod/refine-supabase package, dataProvider={dataProvider(supabaseClient)} makes the following object available to us:

@pankod/refine-supabase/src/index.ts

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 "@pankod/refine-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:

// title="App.tsx"

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:

// title="App.tsx"
authProvider={authProvider}

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

// src/authProvider.ts

import { AuthProvider } from "@pankod/refine-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();
            }
        }

        // 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();
        }

        // for third-party login
        return Promise.resolve(false);
    },
    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.