π₯· Spinner Assassination Recipe
π¨βπ³ Prep Time: 5 minutes
π οΈ Tools Needed: Supabase, React, useContext, and caffeine
π§Ύ Ingredients
1 AuthContext with user and initialized state
1 getUserProfile() or similar async loader
1 <ProtectedLayout> checking initialized before showing protected content
1 Spinner component (to murder)
β
π£ Step-by-Step Instructions
1. Create AuthContext.tsx Carefully
Ensure initialized is only set to true after:
the Supabase session is checked,
the user is set, and
any required async profile loading is done.
ts
useEffect(() => {
const init = async () => {
const { data: { session } } = await supabase.auth.getSession();
if (session?.user) {
setUser(session.user);
try {
// OPTIONAL: load or create user profile
await getUserProfile(session.user.id);
} catch (e) {
console.error("β Failed to load profile", e);
}
}
setInitialized(true); // β Only after session + profile load
};
init();
const { data: listener } = supabase.auth.onAuthStateChange((event, session) => {
setUser(session?.user ?? null);
});
return () => {
listener?.subscription.unsubscribe();
};
}, []);
2. Update ProtectedLayout.tsx
Donβt render the app until initialized is true.
tsx
const ProtectedLayout: React.FC = () => {
const { user, initialized } = useAuth();
if (!initialized) return <Spinner />; // π Wait patiently
if (!user) return <Navigate to="/" replace />; // πͺKick out guests
return (
<div>
<Header />
<Outlet />
</div>
);
};
3. Bonus: Add a key to Protected Routes
To fully force rerender when initialized flips from false β true:
tsx
<Route path="/app/*" element={<AppLayout key={initialized ? "ready" : "loading"} />} />
4. Confirm the Kill
Check your console on refresh. You should see:
yaml
CopyEdit
π§ Initializing auth...
π Auth event: SIGNED_IN
π‘ Loading or creating profile for: user-id
β Auth initialized: true
π₯ Spinner gone!
If not β your spinner lives. Be ashamed. πͺ
π§ Pro Tip
Name the Spinner something like WaitAFreakingSecond.tsx to stay humble.
π Result
β No more false starts
β
ββ Fast refreshes
β
ββ Spinner dies honorably, as it should
β
β
// π₯· Spinner Assassination Guide
// React + Supabase + AuthContext + ProtectedRoute
// ---- 1. AuthContext.tsx ----
import { createContext, useContext, useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';
const AuthContext = createContext({ user: null, initialized: false });
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [initialized, setInitialized] = useState(false);
useEffect(() => {
const init = async () => {
console.log('π§ Initializing auth...');
const { data: { session } } = await supabase.auth.getSession();
if (session?.user) {
setUser(session.user);
try {
console.log('π‘ Loading or creating profile for:', session.user.id);
await getUserProfile(session.user.id); // Optional profile fetch
} catch (err) {
console.warn('β οΈ Failed to load profile:', err);
}
}
setInitialized(true);
};
init();
const { data: listener } = supabase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user ?? null);
});
return () => listener.subscription.unsubscribe();
}, []);
return (
<AuthContext.Provider value={{ user, initialized }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
// ---- 2. ProtectedLayout.tsx ----
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
const Spinner = () => (
<div className="flex h-screen items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
);
export const ProtectedLayout = () => {
const { user, initialized } = useAuth();
if (!initialized) return <Spinner />;
if (!user) return <Navigate to="/" replace />;
return (
<div className="min-h-screen bg-white">
<Outlet />
</div>
);
};
// ---- 3. App.tsx Route Fix ----
import { useAuth } from '@/contexts/AuthContext';
const App = () => {
const { initialized } = useAuth();
return (
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/app/*" element={<AppLayout key={initialized ? 'ready' : 'loading'} />} />
{/* other routes */}
</Routes>
</AuthProvider>
</BrowserRouter>
);
};