TechLead
Lesson 8 of 8
5 min read
Supabase

Supabase with React

Integrate Supabase into React applications with best practices

Supabase with React

Learn how to integrate Supabase into your React applications following best practices for authentication, data fetching, and state management.

βš›οΈ React Integration

  • Client Setup: Configure Supabase client for React
  • Auth Context: Share auth state across components
  • Custom Hooks: Reusable data fetching patterns
  • Optimistic Updates: Smooth user experience

Project Setup

# Create React app
npx create-react-app my-app
# or with Vite
npm create vite@latest my-app -- --template react-ts

# Install Supabase
npm install @supabase/supabase-js

Create Supabase Client

// src/lib/supabase.js
import { createClient } from '@supabase/supabase-js'

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

export const supabase = createClient(supabaseUrl, supabaseKey)

Auth Context Provider

// src/contexts/AuthContext.jsx
import { createContext, useContext, useEffect, useState } from 'react'
import { supabase } from '../lib/supabase'

const AuthContext = createContext({})

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)

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

    // Listen for auth changes
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_event, session) => {
        setUser(session?.user ?? null)
      }
    )

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

  const signIn = (email, password) =>
    supabase.auth.signInWithPassword({ email, password })

  const signUp = (email, password) =>
    supabase.auth.signUp({ email, password })

  const signOut = () => supabase.auth.signOut()

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

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

Protected Routes

// src/components/ProtectedRoute.jsx
import { Navigate } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'

export function ProtectedRoute({ children }) {
  const { user, loading } = useAuth()

  if (loading) return <div>Loading...</div>

  if (!user) return <Navigate to="/login" />

  return children
}

Data Fetching Hook

// src/hooks/usePosts.js
import { useState, useEffect } from 'react'
import { supabase } from '../lib/supabase'

export function usePosts() {
  const [posts, setPosts] = useState([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    fetchPosts()
  }, [])

  async function fetchPosts() {
    try {
      setLoading(true)
      const { data, error } = await supabase
        .from('posts')
        .select('*')
        .order('created_at', { ascending: false })

      if (error) throw error
      setPosts(data)
    } catch (err) {
      setError(err.message)
    } finally {
      setLoading(false)
    }
  }

  async function createPost(title, content) {
    const { data, error } = await supabase
      .from('posts')
      .insert({ title, content })
      .select()
      .single()

    if (!error) setPosts([data, ...posts])
    return { data, error }
  }

  return { posts, loading, error, createPost, refetch: fetchPosts }
}

Realtime Hook

// src/hooks/useRealtimePosts.js
import { useState, useEffect } from 'react'
import { supabase } from '../lib/supabase'

export function useRealtimePosts() {
  const [posts, setPosts] = useState([])

  useEffect(() => {
    // Fetch initial data
    supabase.from('posts').select('*').then(({ data }) => {
      setPosts(data || [])
    })

    // Subscribe to changes
    const channel = supabase
      .channel('posts')
      .on('postgres_changes',
        { event: '*', schema: 'public', table: 'posts' },
        (payload) => {
          if (payload.eventType === 'INSERT') {
            setPosts(prev => [payload.new, ...prev])
          } else if (payload.eventType === 'DELETE') {
            setPosts(prev => prev.filter(p => p.id !== payload.old.id))
          } else if (payload.eventType === 'UPDATE') {
            setPosts(prev => prev.map(p =>
              p.id === payload.new.id ? payload.new : p
            ))
          }
        }
      )
      .subscribe()

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

  return posts
}

Login Component

// src/components/Login.jsx
import { useState } from 'react'
import { useAuth } from '../contexts/AuthContext'

export function Login() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState(null)
  const { signIn } = useAuth()

  const handleSubmit = async (e) => {
    e.preventDefault()
    setError(null)
    const { error } = await signIn(email, password)
    if (error) setError(error.message)
  }

  return (
    <form onSubmit={handleSubmit}>
      {error && <p className="text-red-500">{error}</p>}
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Sign In</button>
    </form>
  )
}

πŸ’‘ Key Takeaways

  • β€’ Use AuthContext to share auth state across components
  • β€’ Create custom hooks for data fetching patterns
  • β€’ Clean up realtime subscriptions on unmount
  • β€’ Protect routes based on authentication state

πŸ“š Learn More

Continue Learning