Supabase avec React : Hooks, Contexte et Gestion d’État

Intégrer Supabase dans une application React (Vite, CRA ou standalone) demande une approche différente de Next.js. Ce guide couvre les patterns recommandés : hooks personnalisés, contexte Auth, React Query, et gestion optimiste de l’état.

Pour comprendre les fondations, consulte notre guide complet Supabase. Pour l’intégration SSR/SSG, voir Supabase avec Next.js. Pour l’authentification, voir Supabase Auth. Pour les données temps réel, voir Supabase RLS. Pour les JWT, voir Supabase JWT.

Installation

# Créer un projet React avec Vite
npm create vite@latest mon-app -- --template react-ts
cd mon-app

# Installer Supabase
npm install @supabase/supabase-js

Initialiser le client

// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import type { Database } from './types/supabase'

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  auth: {
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: true
  }
})

Context Auth Global

// src/contexts/AuthContext.tsx
import { createContext, useContext, useEffect, useState } from 'react'
import type { Session, User } from '@supabase/supabase-js'
import { supabase } from '@/lib/supabase'

type AuthContextType = {
  user: User | null
  session: Session | null
  loading: boolean
  signOut: () => Promise<void>
}

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null)
  const [session, setSession] = useState<Session | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    supabase.auth.getSession().then(({ data: { session } }) => {
      setSession(session)
      setUser(session?.user ?? null)
      setLoading(false)
    })

    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_event, session) => {
        setSession(session)
        setUser(session?.user ?? null)
        setLoading(false)
      }
    )

    return () => subscription.unsubscribe()
  }, [])

  const signOut = async () => {
    await supabase.auth.signOut()
  }

  return (
    <AuthContext.Provider value={{ user, session, loading, signOut }}>
      {children}
    </AuthContext.Provider>
  )
}

export const useAuth = () => useContext(AuthContext)

Hooks Personnalisés Supabase

useQuery : Fetch de données

export function useSupabaseQuery<T>(
  queryFn: () => Promise<any>
) {
  const [data, setData] = useState<T | null>(null)
  const [error, setError] = useState<any>(null)
  const [loading, setLoading] = useState(true)
  const [trigger, setTrigger] = useState(0)

  useEffect(() => {
    setLoading(true)
    queryFn().then(({ data, error }) => {
      setData(data)
      setError(error)
      setLoading(false)
    })
  }, [trigger])

  return { data, error, loading, refetch: () => setTrigger(t => t + 1) }
}

useRealtime : Données en temps réel

export function useRealtime<T extends { id: string }>(
  table: string,
  initialData: T[] = []
) {
  const [data, setData] = useState<T[]>(initialData)

  useEffect(() => {
    const channel = supabase
      .channel(`${table}-realtime`)
      .on(
        'postgres_changes',
        { event: '*', schema: 'public', table },
        (payload) => {
          setData(current => {
            switch (payload.eventType) {
              case 'INSERT': return [payload.new as T, ...current]
              case 'UPDATE': return current.map(item =>
                item.id === payload.new.id ? payload.new as T : item
              )
              case 'DELETE': return current.filter(item => item.id !== payload.old.id)
              default: return current
            }
          })
        }
      )
      .subscribe()

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

  return data
}

Intégration avec TanStack Query (React Query)

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

export function usePosts() {
  return useQuery({
    queryKey: ['posts'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('posts')
        .select('*, profiles(username)')
        .order('created_at', { ascending: false })
      
      if (error) throw error
      return data
    },
    staleTime: 1000 * 60 * 5,
  })
}

export function useCreatePost() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: async (post: { title: string; content: string }) => {
      const { data, error } = await supabase
        .from('posts')
        .insert(post)
        .select()
        .single()
      
      if (error) throw error
      return data
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] })
    }
  })
}

👉 Articles du guide Supabase

Cet article fait partie du Guide Complet Supabase 2026. Retrouvez les autres articles :

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *