I came up with my custom react hook for handling supabase, I don't know if using this hook everywhere would be the best practice as I have to deal with isLoading
every time when I use it.
import { useEffect, useState } from 'react'
import { useAuth } from '@clerk/nextjs'
import { createClient } from '@supabase/supabase-js'
import { SupabaseClient } from '@supabase/supabase-js'
const createSupabaseClient = (token: string) => {
if (!process.env.NEXT_PUBLIC_SUPABASE_URL) {
throw new Error('NEXT_PUBLIC_SUPABASE_URL is not defined')
}
if (!process.env.NEXT_PUBLIC_SUPABASE_KEY) {
throw new Error('NEXT_PUBLIC_SUPABASE_KEY is not defined')
}
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_KEY,
{
global: {
headers: {
Authorization: `Bearer ${token}`,
},
},
},
)
}
export const useSupabase = () => {
const { getToken } = useAuth()
const [supabase, setSupabase] = useState<SupabaseClient>()
const [loading, setLoading] = useState(true)
const [error, setError] = useState()
useEffect(() => {
const fetchTokenAndInitializeClient = async () => {
try {
const token = await getToken({ template: 'supabase' })
if (!token) {
throw new Error('No token found')
}
const supabase = createSupabaseClient(token)
setSupabase(supabase)
setLoading(false)
} catch (err) {
setError(err as any)
setLoading(false)
}
}
fetchTokenAndInitializeClient()
}, [getToken])
return { supabase, loading, error }
}
The problem with this approach is every time you call useSupabase
that useEffect
will run, because the lifecycle will kick off on every component you put this in.
You're correct that we should be dealing with this somewhere.
A good way to tackle this is to create a Provider with React.Context
.
const SupabaseContext = useContext({
supabase: undefined,
loading: false,
error: undefined
})
export const SupabaseProvider({ children }) {
const { getToken } = useAuth()
const [supabase, setSupabase] = useState<SupabaseClient>()
const [loading, setLoading] = useState(true)
const [error, setError] = useState()
useEffect(() => {
const fetchTokenAndInitializeClient = async () => {
try {
const token = await getToken({ template: 'supabase' })
if (!token) {
throw new Error('No token found')
}
const supabase = createSupabaseClient(token)
setSupabase(supabase)
} catch (err) {
setError(err as any)
} finally {
setLoading(false)
}
}
fetchTokenAndInitializeClient()
}, [getToken])
return (
<SupabaseContext.Provider value={{ supabase, loading, error }}>
{children}
</SupabaseContext.Provider>
)
}
Now you can wrap this around the top level of your application like this:
const App = () => (
<SupabaseProvider>
...
</SupabaseProvider>
)
And in your components lower down you can access supabase
(and it's loading and error states) by using a hook:
const { supabase, loading, error } = useContext(SupabaseContext);
If you don't want to worry about the loading
state in all your children components, you can use a guard statement higher up in your application to only render children component if loading
is false.
Here's an example of doing it at the top level in your SupabaseProvider
:
return (
<SupabaseContext.Provider value={{ supabase, loading, error }}>
{isloading ? <LoadingSupabase /> : children}
</SupabaseContext.Provider>
)