Protected Routes in React Router 6 with Supabase Authentication and OAuth

Alex Vallejo
4 min readJan 19, 2024

--

I haven’t seemingly been able to find a straightforward solution for wiring up Supabase Authentication with React Router 6’s createBrowserRouter. I ended up with an endless loop of logging in, not seeing my session token, and routing back to my login page. I came across a distinct missing piece of the puzzle that I thought I’d shine light on in case anyone else is stuck. If this missing piece is obvious to the reader, then consider me dumb. Read on!

Photo by rc.xyz NFT gallery on Unsplash

For this demo, I’ll attempt to log into my app with Google OAuth. Supabase’s documentation runs through the initial setup of generating your access keys on your Google account and adding the Supabase callback URL so that Google knows your Supabase database is to be trusted and can start authenticating users. I won’t go into detail there as it’s pretty well documented.

What’s not well documented is incorporating the OAuth login as part of your React routes in a way that is scalable across your application. The main documentation has this simple code sample:

supabase.auth.signInWithOAuth({
provider: 'google',
})

Great, but what do we do with that? Supabase does offer some additional documentation around React which offers a different approach to listen to authentication state change from your Supabase client:

import './index.css'
import { useState, useEffect } from 'react'
import { createClient } from '@supabase/supabase-js'
import { Auth } from '@supabase/auth-ui-react'
import { ThemeSupa } from '@supabase/auth-ui-shared'

const supabase = createClient('https://<project>.supabase.co', '<your-anon-key>')

export default function App() {
const [session, setSession] = useState(null)

useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session)
})

const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session)
})

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

if (!session) {
return (<Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} />)
}
else {
return (<div>Logged in!</div>)
}
}

So a couple notes here. First off, Supabase is asking us to install their Auth UI library, which is pretty great. Second, it’s utilizing its onAuthStateChange listener. This allows us to just plop the Auth component in and authenticate. But what we really want to do is incorporate this into an AuthProvider component that we can wrap around our entire application.

I’m going to skip the part that had me in a loop and just go straight to the solution.

import { Session, User } from '@supabase/supabase-js';
import { createContext, useState, useEffect, useContext } from 'react';
import { supabase } from '../services/useSupabase';

const AuthContext = createContext({
user: null
});

export const useAuth = () => {
return useContext(AuthContext);
};

const AuthProvider = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState<boolean>(true);

useEffect(() => {
const { data: listener } = supabase.auth.onAuthStateChange(
(_event, session) => {
console.log('session onAuthStateChange: ', session);
setSession(session);
setUser(session?.user || null);
setLoading(false);
}
);
return () => {
listener?.subscription.unsubscribe();
};
}, []);

// In case we want to manually trigger a signIn (instead of using Auth UI)
const signIn = async () => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: { skipBrowserRedirect: false },
});
console.log('data: ', data);
console.log('error: ', error);
return { data, error };
};

const signOut = async () => {
const { error } = await supabase.auth.signOut();
console.log('error: ', error);
if (!error) {
setUser(null);
setSession(null);
}
return { error };
};

return (
<AuthContext.Provider value={{ user, signIn, signOut }}>
{!loading ? children : `<div>Loading...</div>`}
</AuthContext.Provider>
);
};

export default AuthProvider;

In order to properly test this with React Router, let’s create the following components:

  • A <Root /> component that wraps our entire app. This is shown regardless of whether you’re authenticated. Note I’m using Tailwind here, but it could be something really basic that just provides an <Outlet />
export default function Root() {
return (
<main className='flex'>
<div className='w-full'>
<Outlet />
</div>
</main>
);
}
  • A <ProtectedPage /> component that wraps all of our protected routes. For this, I’m just doing a simple check on our user object but you could do some additional validation here against the session token.
import { Navigate } from 'react-router-dom';
import Layout from './Layout';
import { useAuth } from './AuthProvider';

const ProtectedPage = () => {
const { user } = useAuth();
console.log('user at ProtectedPage: ', user);

if (!user) {
return <Navigate to='/login' replace />;
}

return <Layout />;
};

export default ProtectedPage;
  • The <Layout /> component has just a sidebar menu that allows you to navigate protected routes and sign out.
  • Finally, the <Login /> component can simply have the Auth UI component:
import { Auth } from '@supabase/auth-ui-react';
import { ThemeSupa } from '@supabase/auth-ui-shared';
import { supabase } from '../services/useSupabase';

export const Login = () => {
return (
<div className='md:flex md:justify-center'>
<div className='flex-col prose mt-7'>
<h1>Log in to your app!</h1>
<div className='items-center justify-center mx-12'>
<Auth
supabaseClient={supabase}
appearance={{ theme: ThemeSupa }}
providers={['google']}
/>
</div>
</div>
</div>
);
};

And now, let’s put it all together in our createBrowserRouter in our main component.

import React from 'react';
import ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import './index.css';
import Root from './routes/root';
import ErrorPage from './error-page';
import { Login } from './routes/login.tsx';
import ProtectedPage from './components/ProtectedPage.tsx';
import SomeProtectedComponent, { loader as someProtectedLoader } from './routes/someProtectedComponent.tsx';
import AuthProvider from './components/AuthProvider.tsx';

const router = createBrowserRouter([
{
path: '/',
element: <Root />,
errorElement: <ErrorPage />,
children: [
{
path: 'login',
element: <Login />,
},
{
path: '/',
element: <ProtectedPage />,
children: [
{
path: 'some-protected-path',
element: <SomeProtectedComponent />,
loader: someProtectedLoader,
},
],
},
],
},
]);

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
</React.StrictMode>
);

If this doesn’t work for you, leave a comment and I will try to address it.

Thank you for reading!

--

--

Alex Vallejo

web developer from boston. live in miami. work @ Bowst