Best way to get authenticated user on client side

Hi all, Trying to understand what is the best way to access authenticated user in my app. (using nextjs 15 and supabase) in client components. I thought having a context would be a good idea, but I could not get "onAuthStateChange" to work properly. My context works initially but does not update between logins. What is the best way to access current user in client components? Option 1: Getting the user in a useEffect and updating local state?
const { data: { user } } = await supabase.auth.getUser()
const { data: { user } } = await supabase.auth.getUser()
Option 2: Having a context and wrapping the app with it so all client components can access it.
'use client';

import { createClient } from '@/utils/supabase/client';
import { User } from '@supabase/supabase-js';
import React, { createContext, useContext, useEffect, useState } from 'react';

type UserContextType = {
user: User | null;
loading: boolean;
};

const UserContext = createContext<UserContextType | undefined>(undefined);

export const UserProvider: React.FC<{ children: React.ReactNode }> = ({
children
}) => {
const supabase = createClient();
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
const fetchUser = async () => {
setLoading(true);
const { data: user } = await supabase.auth.getUser();
setUser(user.user);
setLoading(false);
};

fetchUser();

// Listen for auth state changes
const { data: subscription } = supabase.auth.onAuthStateChange(() =>
fetchUser()
);

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

return (
<UserContext.Provider value={{ user, loading }}>
{children}
</UserContext.Provider>
);
};

export const useGetCurrentUser = () => {
const context = useContext(UserContext);
if (!context) throw new Error('useUser must be used within a UserProvider');
return context;
};
'use client';

import { createClient } from '@/utils/supabase/client';
import { User } from '@supabase/supabase-js';
import React, { createContext, useContext, useEffect, useState } from 'react';

type UserContextType = {
user: User | null;
loading: boolean;
};

const UserContext = createContext<UserContextType | undefined>(undefined);

export const UserProvider: React.FC<{ children: React.ReactNode }> = ({
children
}) => {
const supabase = createClient();
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
const fetchUser = async () => {
setLoading(true);
const { data: user } = await supabase.auth.getUser();
setUser(user.user);
setLoading(false);
};

fetchUser();

// Listen for auth state changes
const { data: subscription } = supabase.auth.onAuthStateChange(() =>
fetchUser()
);

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

return (
<UserContext.Provider value={{ user, loading }}>
{children}
</UserContext.Provider>
);
};

export const useGetCurrentUser = () => {
const context = useContext(UserContext);
if (!context) throw new Error('useUser must be used within a UserProvider');
return context;
};
7 Replies
static
static8mo ago
Out of curiosity, why not get the user on the server? You can pass whichever information you need to the client 🤷‍♂️
DevsrealmGuy
DevsrealmGuy8mo ago
You can use a state manager like mobx, then in any components you want to get the user, you do something like:
const { authStore } = useStore();
authStore.getUser();
const { authStore } = useStore();
authStore.getUser();
This is the way I structure most of my code, and it helps to abstract the business logic elsewhere, and very maintainable also.
enszrlu
enszrluOP8mo ago
@DevsrealmGuy how do you handle user changes in state manager ? Eg login logout I have so many client components I would not want to prop drill
static
static8mo ago
Pass it to one client component which contains a context?
DevsrealmGuy
DevsrealmGuy8mo ago
In the AuthStore, I am doing something like:
export class AuthStore implements StoreSubStore {

async logout(): Promise<boolean> {
try {
await supabase.auth.signOut();
this.store.reset();
return true;
} catch (error) {
return false;
}
}

async login(email: string, password: string): Promise<boolean> {
try {
const {data} = await supabase.auth.signInWithPassword({
email,
password
});

this.setSession(data.session);

return true;
} catch (error) {
return false;
}
}

}
export class AuthStore implements StoreSubStore {

async logout(): Promise<boolean> {
try {
await supabase.auth.signOut();
this.store.reset();
return true;
} catch (error) {
return false;
}
}

async login(email: string, password: string): Promise<boolean> {
try {
const {data} = await supabase.auth.signInWithPassword({
email,
password
});

this.setSession(data.session);

return true;
} catch (error) {
return false;
}
}

}
The in the logout component, I do something like:
const { authStore } = useStore();

const handleLogout = async () => {
const success = authStore.logout();
if (success) {
navigate('/login');
toast.success(t('loggedOut'));
} else {
toast.error(t('logoutError'));
}
};

<Button type="button" variant="outline" onClick={handleLogout}>{t('logout')}</Button>
const { authStore } = useStore();

const handleLogout = async () => {
const success = authStore.logout();
if (success) {
navigate('/login');
toast.success(t('loggedOut'));
} else {
toast.error(t('logoutError'));
}
};

<Button type="button" variant="outline" onClick={handleLogout}>{t('logout')}</Button>
It is as simple as that, anything authentication, login/logout, etc is placed in the AuthStore, if you have say a project table, I place the logic in a ProjectStore, something like that The cool thing about mobx though is that it can auto detect all your changes and propagate them out to where they are being used. So, useEffect isn't needed most of the time. It takes getting used to though, but once you get a hang of it, it is very cool
enszrlu
enszrluOP8mo ago
Makes sense. I will try it, thanks @DevsrealmGuy ohhh your implementation is a lot easier to handle as everything in one place. I wonder what is the supabase suggested way of handling this
DevsrealmGuy
DevsrealmGuy8mo ago
Yeah, it is a Domain Service encapsulation and well, there is no supabase way of handling this, what works for everyone is different you know 😉

Did you find this page helpful?