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.