TIL: React & Supabase
TIL stands for “Today I Learned,” in case you didn’t know — like me until recently.
I usually don’t work much with the React & Supabase stack. However, I find it quite interesting — the ease of use of Supabase Auth and Supabase Edge Functions from any frontend framework is very appealing. Utilizing PostgreSQL’s RLS (Row Level Security) to ensure that users can’t access data they’re not eligible for is a smart and quick way to implement authorization.
Yet, one must still be very cautious when integrating the Supabase SDK into a React application. If you’ve ever built something in React, you’ve probably encountered issues with unnecessary re-rendering just because you weren’t careful when setting state. And this can happen rather easily with Supabase.
Let’s imagine the following snippet:
const [session, setSession] = useState(null)
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session)
})
// Listen for changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
setSession(session)
// React to specific events
if (event === 'SIGNED_IN') {
// redirect, fetch user profile, etc.
}
if (event === 'SIGNED_OUT') {
// clear local state, redirect to login
}
if (event === 'PASSWORD_RECOVERY') {
// show password reset form
}
}
)
// Cleanup on unmount
return () => subscription.unsubscribe()
}, [])
The onAuthStateChange is a Supabase listener that allows you to react to any auth-related events. You’d typically set up such logic in a context or a wrapper component around your main application component so any component can access the session and user information. Easy, right?
The problem is the following line in the onAuthStateChange callback:
setSession(session)
Even though the user might still be the same, the session is a new object, so React is obliged to re-render all the components relying on this piece of state. Now, combine it with this statement from the Supabase documentation:
– SIGNED_IN
– Emitted each time a user session is confirmed or re-established, including on user sign in and when refocusing a tab.
– Avoid making assumptions as to when this event is fired, this may occur even when the user is already signed in. Instead, check the user object attached to the event to see if a new user has signed in and update your application’s UI.
– This event can fire very frequently depending on the number of tabs open in your application.
And you’ve cooked yourself a nice little bug. The key is to make some guard checks before blindly re-setting the state. For example:
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
sessionRef.current = session
setUser(session?.user ?? null)
setLoading(false)
})
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
const newUserId = session?.user?.id ?? null
const currentUserId = sessionRef.current?.user?.id ?? null
sessionRef.current = session
if (event === 'SIGNED_IN') {
setUser(prev => {
if (prev?.id === newUserId) return prev
return session?.user ?? null
})
}
if (event === 'SIGNED_OUT') {
setUser(null)
}
if (event === 'USER_UPDATED') {
setUser(session?.user ?? null)
}
}
)
return () => subscription.unsubscribe()
}, [])
We check whether the user changed, and if not, we don’t modify the state — therefore we don’t trigger a re-render of the underlying components.
Such bugs are pretty common when you’re not 100% familiar with the stack, but it’s a pleasure when you’re able to understand the issue and solve it. And the current era of coding agents is helping a lot.