build-a-saas-with-next-js-supabase-and-stripe
build-a-saas-with-next-js-supabase-and-stripe copied to clipboard
User Authentication State doesn't match on Server and Client when Refresh Page
The user state doesn't match on the server and the client
When I logged in and refresh a page, I see the following error in my browser console
next-dev.js?3515:32 Warning: Text content did not match. Server: "Auth" Client: "Exit"
In my browser, my user state returns a boolean true that shows I'm authenticated; however, in my server, my user state returns a boolean false that shows I'm not authenticated
If I navigate via the client side without refreshing the page, the auth status works fine
Anyone knows why is my server and client authentication status different? I followed the example in the youtube video. Anything I missed out? Thanks
AuthContext.tsx
interface AuthUser extends User {
is_subscribed: boolean;
interval: string;
}
export interface IAuthContext {
// setUser: Dispatch<SetStateAction<any>>;
user: AuthUser | null;
loginWithMagicLink: (email: string) => Promise<{ error: any | null }>;
signOut: () => Promise<{ error: ApiError | null } | undefined>;
isLoading: boolean;
}
export const AuthContext = createContext<IAuthContext>(null!);
interface IProps {
supabaseClient: SupabaseClient;
}
const AuthProvider: FC<IProps> = ({ children }) => {
const [user, setUser] = useState<AuthUser | null>(
supabase.auth.user() as AuthUser
);
const [isLoading, setIsLoading] = useState<boolean>(true);
const { push } = useRouter();
useEffect(() => {
const getUserProfile = async () => {
const sessionUser = supabase.auth.user();
if (sessionUser) {
const { data: profile } = await supabase
.from("profile")
.select("*")
.eq("id", sessionUser.id)
.single();
setUser({
...sessionUser,
...profile,
});
setIsLoading(false);
}
};
getUserProfile();
supabase.auth.onAuthStateChange(() => {
getUserProfile();
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
try {
axios.post(`/api/set-supabase-cookie`, {
event: user ? "SIGNED_IN" : "SIGNED_OUT",
session: supabase.auth.session(),
});
} catch (error) {
console.log(error);
}
}, [user]);
const loginWithMagicLink = async (email: string) => {
const data = await supabase.auth.signIn({ email });
return data;
};
const signOut = async () => {
try {
const data = await supabase.auth.signOut();
setUser(null);
push("/auth");
return data;
} catch (error) {
console.log(error);
}
};
return (
<AuthContext.Provider
value={{
loginWithMagicLink,
user,
signOut,
isLoading,
}}
>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error(`useUser must be used within a UserContextProvider.`);
}
return context;
};
export default AuthProvider;
Navbar.tsx
export const Navbar = ({ title = "L A B" }: Props) => {
const { user, signOut } = useAuth();
console.log("nav isAuthenticated", !!user);
return (
<div className="navbar bg-base-100 shadow-lg">
<div className="flex-1">
<Link href="/">
<a className="btn btn-ghost normal-case text-xl">
<span className="text-lg font-bold tracking-widest">{title}</span>
</a>
</Link>
</div>
<div className="flex-none">
<ul className="menu menu-horizontal p-0">
{links.map((l) => (
<li key={l.title} className="hidden md:block">
<Link href={l.url}>
<a className="cursor-pointer">{l.title}</a>
</Link>
</li>
))}
{!!user ? (
<li className="hidden md:block">
<a onClick={signOut}>Exit</a>
</li>
) : (
<li className="hidden md:block">
<Link href="/auth">
<a className="cursor-pointer">Auth</a>
</Link>
</li>
)}
<li tabIndex={0} className="md:hidden">
<a>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="inline-block w-5 h-5 stroke-current"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h16M4 18h16"
></path>
</svg>
</a>
<ul className="bg-base-100">
{links.map((l) => (
<li key={l.title}>
<Link href={l.url}>
<a
className="cursor-pointer tooltip tooltip-left"
data-tip={l.tip}
>
{l.icon}
</a>
</Link>
</li>
))}
{!!user ? (
<li>
<a
onClick={signOut}
className="cursor-pointer tooltip tooltip-left"
data-tip="Exit"
>
<AiFillAliwangwang size={40} color="red" />
</a>
</li>
) : (
<li>
<Link href="/auth">
<a
className="cursor-pointer tooltip tooltip-left"
data-tip="Auth"
>
<AiFillAccountBook size={40} color="green" />
</a>
</Link>
</li>
)}
</ul>
</li>
</ul>
<ThemeChanger />
</div>
</div>
);
};
I was experiencing the same issue during refresh and when completing or cancelling a payment.
I believe it is caused by the user being set to supabase.auth.user() in the user.js context file when setting the initial useState. I changed this to null and the error has been eliminated. I haven't seen any other negative issue or error since making this change.
`const Provider = ({ children }) => {
const [user, setUser] = useState(null); //<---- this changed to null
const [isLoading, setIsLoading] = useState(true);
const router = useRouter(); .... `
You could also just set the user state back to supabase.auth.user()
on logout so it keeps it consistent and provide a null user attribute if the user is logged out.
const signOut = async () => {
try {
const data = await supabase.auth.signOut();
setUser(supabase.auth.user()); // will result in { user: null }
push("/auth");
return data;
} catch (error) {
console.log(error);
}
};