Bỏ qua

Client Best Practices

Data Fetching Patterns

Server vs Client Fetching

┌─────────────────────────────────────────────────────────────┐
│                FETCHING STRATEGIES                           │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  SERVER COMPONENTS (preferred):                             │
│  ✅ Faster initial load (no client JS)                      │
│  ✅ Better SEO                                               │
│  ✅ Direct database access                                   │
│  ✅ No loading states needed                                 │
│                                                              │
│  CLIENT COMPONENTS:                                         │
│  ✅ Interactive features                                     │
│  ✅ Real-time updates                                        │
│  ✅ User-triggered fetches                                   │
│  ❌ Needs loading/error states                              │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Pattern: Server Fetch, Client Hydrate

// app/tasks/page.tsx (Server Component)
import { createClient } from '@/lib/supabase/server';
import { TaskList } from './task-list';

export default async function TasksPage() {
  const supabase = await createClient();

  const { data: tasks } = await supabase
    .from('tasks')
    .select('*')
    .order('created_at', { ascending: false });

  // Pass to client component for interactivity
  return <TaskList initialTasks={tasks || []} />;
}

// app/tasks/task-list.tsx (Client Component)
'use client';

import { useState, useEffect } from 'react';
import { createClient } from '@/lib/supabase/client';

export function TaskList({ initialTasks }: { initialTasks: Task[] }) {
  const [tasks, setTasks] = useState(initialTasks);
  const supabase = createClient();

  // Subscribe to realtime updates
  useEffect(() => {
    const channel = supabase
      .channel('tasks')
      .on('postgres_changes', { event: '*', table: 'tasks' }, (payload) => {
        // Handle realtime updates
      })
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, []);

  return (
    <ul>
      {tasks.map((task) => (
        <li key={task.id}>{task.title}</li>
      ))}
    </ul>
  );
}

Query Optimization

Select Only Needed Columns

// ❌ BAD: Select all columns
const { data } = await supabase
  .from('tasks')
  .select('*');

// ✅ GOOD: Select only needed columns
const { data } = await supabase
  .from('tasks')
  .select('id, title, status');

// ✅ GOOD: Select with relations
const { data } = await supabase
  .from('tasks')
  .select(`
    id,
    title,
    project:projects(id, name)
  `);

Use Count for Pagination

// ✅ GOOD: Get count without fetching all data
const { count } = await supabase
  .from('tasks')
  .select('*', { count: 'exact', head: true })
  .eq('project_id', projectId);

// Then fetch current page
const { data } = await supabase
  .from('tasks')
  .select('id, title, status')
  .eq('project_id', projectId)
  .range(offset, offset + pageSize - 1);

Error Handling Pattern

Centralized Error Handler

// lib/supabase/error-handler.ts
import { PostgrestError } from '@supabase/supabase-js';

export function handleSupabaseError(error: PostgrestError): never {
  // Log for debugging
  console.error('Supabase error:', {
    code: error.code,
    message: error.message,
    details: error.details,
    hint: error.hint,
  });

  // User-friendly messages
  const errorMessages: Record<string, string> = {
    '23505': 'This item already exists',
    '23503': 'Cannot delete - item is in use',
    '42501': 'You do not have permission for this action',
    'PGRST116': 'Item not found',
  };

  const message = errorMessages[error.code] || 'An error occurred';
  throw new Error(message);
}

Usage

const { data, error } = await supabase
  .from('tasks')
  .select('*')
  .eq('id', taskId)
  .single();

if (error) {
  handleSupabaseError(error);
}

return data;

TypeScript Patterns

Typed Query Results

// Define types
type Task = Database['public']['Tables']['tasks']['Row'];
type TaskInsert = Database['public']['Tables']['tasks']['Insert'];
type TaskUpdate = Database['public']['Tables']['tasks']['Update'];

// Use in queries
async function createTask(task: TaskInsert): Promise<Task> {
  const { data, error } = await supabase
    .from('tasks')
    .insert(task)
    .select()
    .single();

  if (error) throw error;
  return data;
}

async function updateTask(id: string, updates: TaskUpdate): Promise<Task> {
  const { data, error } = await supabase
    .from('tasks')
    .update(updates)
    .eq('id', id)
    .select()
    .single();

  if (error) throw error;
  return data;
}

Custom Query Types

// For queries with relations
type TaskWithProject = Task & {
  project: Pick<Project, 'id' | 'name'> | null;
};

const { data } = await supabase
  .from('tasks')
  .select(`
    *,
    project:projects(id, name)
  `)
  .returns<TaskWithProject[]>();

Caching Strategies

React Query Integration

// hooks/use-tasks.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { createClient } from '@/lib/supabase/client';

export function useTasks(projectId: string) {
  const supabase = createClient();

  return useQuery({
    queryKey: ['tasks', projectId],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('tasks')
        .select('*')
        .eq('project_id', projectId);

      if (error) throw error;
      return data;
    },
    staleTime: 1000 * 60, // 1 minute
  });
}

export function useCreateTask() {
  const supabase = createClient();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (task: TaskInsert) => {
      const { data, error } = await supabase
        .from('tasks')
        .insert(task)
        .select()
        .single();

      if (error) throw error;
      return data;
    },
    onSuccess: (data) => {
      // Invalidate and refetch
      queryClient.invalidateQueries({
        queryKey: ['tasks', data.project_id]
      });
    },
  });
}

Security Best Practices

Never Expose Service Role Key

// ❌ NEVER do this in client code
const supabase = createClient(url, SUPABASE_SERVICE_ROLE_KEY);

// ✅ Service role only in server-side code
// api/admin/route.ts
import { createClient } from '@supabase/supabase-js';

const adminClient = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!  // Server-only env
);

Validate User Session

// Always verify user in sensitive operations
async function deleteTask(taskId: string) {
  const supabase = await createClient();

  // Get current user
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    throw new Error('Unauthorized');
  }

  // RLS will enforce ownership, but explicit check is clearer
  const { error } = await supabase
    .from('tasks')
    .delete()
    .eq('id', taskId)
    .eq('user_id', user.id);  // Extra safety

  if (error) throw error;
}

Performance Tips

Batch Operations

// ✅ GOOD: Single batch insert
await supabase
  .from('tasks')
  .insert([task1, task2, task3]);

// ❌ BAD: Multiple separate inserts
await supabase.from('tasks').insert(task1);
await supabase.from('tasks').insert(task2);
await supabase.from('tasks').insert(task3);

Parallel Queries

// ✅ GOOD: Parallel queries
const [tasksResult, projectsResult] = await Promise.all([
  supabase.from('tasks').select('*'),
  supabase.from('projects').select('*'),
]);

// ❌ BAD: Sequential queries
const tasks = await supabase.from('tasks').select('*');
const projects = await supabase.from('projects').select('*');

Tổng kết

Best Practices Checklist

Data Fetching:
✅ Server Components for initial data
✅ Client Components for interactivity
✅ Select only needed columns
✅ Use count for pagination meta

Error Handling:
✅ Centralized error handler
✅ User-friendly messages
✅ Proper logging

TypeScript:
✅ Generate and use types
✅ Type query results
✅ Define insert/update types

Security:
✅ Never expose service key
✅ Always verify user
✅ Trust RLS but verify

Performance:
✅ Batch operations
✅ Parallel queries
✅ Use caching (React Query)

Q&A

  1. Data fetching pattern hiện tại?
  2. Đã dùng React Query chưa?
  3. Error handling như thế nào?