ReactQueryAndSupabase - Christin-paige/BuiltInPublic GitHub Wiki
The architecture of the client application consists of the following layers:
- Infrastructure - a handful of functions to create and return Supabase client instances.
- Repository - classes containing methods to perform CRUD operations for a given domain (User Profiles, Projects, etc.)
- Use Case - classes containing methods to perform business logic for a given domain (User Profiles, Projects, etc.)
- Query - the React Query (RQ) provider, and custom hooks that use RQ query and mutation hooks to interact with Use Cases.
- Presentation - components that use the query hooks to render data and handle user interactions.
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
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 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
}
}
}
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() });
},
});
}
Components that use the query hooks to render data and handle user interactions.