ReactQueryAndSupabase - Christin-paige/BuiltInPublic GitHub Wiki

Working Working with React Query and Supabase

Introduction

The architecture of the client application consists of the following layers:

  1. Infrastructure - a handful of functions to create and return Supabase client instances.
  2. Repository - classes containing methods to perform CRUD operations for a given domain (User Profiles, Projects, etc.)
  3. Use Case - classes containing methods to perform business logic for a given domain (User Profiles, Projects, etc.)
  4. Query - the React Query (RQ) provider, and custom hooks that use RQ query and mutation hooks to interact with Use Cases.
  5. Presentation - components that use the query hooks to render data and handle user interactions.

NOTE: the code examples below are merely illustrative and should not be used as-is.

Infrastructure Layer

Functions to create and return Supabase client instances. These are split into server and client-side instances, with the server-side instances having two variations:

  • standard client, which observes RLS policies, using the anon key
  • admin client, which can access all data (regardless of ownership), using the service role key

Repository Layer

Classes containing methods to perform CRUD operations for a given domain (User Profiles, Projects, etc.) Constructors should accept a Supabase client instance as a parameter, this allows us to keep consistent data access and manipulation patterns in both the client and server side.

Example:

//repositories/types.ts

interface Repository<T> {
  getById(id: string): Promise<T | null>;
  getAll(): Promise<T[]>;
  create(data: Partial<T>): Promise<T>;
  update(id: string, data: Partial<T>): Promise<T>;
  delete(id: string): Promise<void>;
}

//repositories/userProfile.ts

import { Repository } from './types';
import {SupabaseClient} from '@supabase/supabase-js';
import { UserProfile, Database } from '@/supabase/supabase.types.ts';

class UserProfileRepository implements Repository<UserProfile> {
  constructor(private supabase: SupabaseClient<Database>) {}

  async getById(id: string): Promise<UserProfile | null> {
    const { data, error } = await this.supabase
      .from('users')
      .select('*')
      .eq('id', id)
      .single();

    if (error) throw error;
    return data;
  }

  async getAll(): Promise<UserProfile[]> {
    const { data, error } = await this.supabase
      .from('users')
      .select('*');

    if (error) throw error;
    return data;
  }

  async create(data: Partial<UserProfile>): Promise<User> {
    const { data: createdUser, error } = await this.supabase
      .from('users')
      .insert(data)
      .single();

    if (error) throw error;
    return createdUser;
  }

  async update(id: string, data: Partial<UserProfile>): Promise<User> {
    const { data: updatedUser, error } = await this.supabase
      .from('users')
      .update(data)
      .eq('id', id)
      .single();

    if (error) throw error;
    return updatedUser;
  }

  async delete(id: string): Promise<void> {
    const { error } = await this.supabase
      .from('users')
      .delete()
      .eq('id', id);

    if (error) throw error;
  }
}

Use Cases

  1. Use Case - classes containing methods to perform business logic for a given domain (User Profiles, Projects, etc.)

The demo code below shows an example Use Case for creating a user

import { UserProfileRepository } from './repositories/UserProfileRepository'

export class CreateUserUseCase {
  constructor(
    private readonly userProfileRepository: UserProfileRepository
  ) {}

  async execute(email: string, password: string) {
    // Email validation regex pattern
    const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/

    // Validate email format
    if (!emailPattern.test(email)) {
      throw new Error('Invalid email format')
    }

    try {
      // Create user profile using repository
      const newUser = await this.userProfileRepository.create({
        email,
        password,
        // Add any other required user fields
      })

      return newUser
    } catch (error) {
      // Handle specific error cases
      if (error instanceof Error) {
        throw new Error(`Failed to create user: ${error.message}`)
      }
      throw error
    }
  }
}

Query Layer

The React Query (RQ) provider, and custom hooks that use RQ query and mutation hooks to interact with repositories and handle business logic.

export function useCreateUser() {
  const queryClient = useQueryClient();
  const supabase = createClient();
  const repository = new UserProfileRepository(supabase);
  const useCase = new CreateUserUseCase(repository);

  return useMutation({
    mutationFn: (newUser: Partial<User>) => useCase.execute(newUser),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: userKeys.lists() });
    },
  });
}

Presentation Layer

Components that use the query hooks to render data and handle user interactions.

⚠️ **GitHub.com Fallback** ⚠️