Web Development

My React/Laravel Auth Setup: No More Polling /me!

Tired of polling /me every few seconds in your React/Laravel app? Learn a modern, event-driven auth setup to sync state across tabs and boost performance.

A

Alex Carter

Senior full-stack developer specializing in building scalable React and Laravel applications.

7 min read17 views

Tired of your React app constantly asking your Laravel API, "Are you logged in yet?"

We’ve all been there. You spin up a beautiful React single-page application (SPA) on top of a robust Laravel backend. Everything feels fast and modern... until you open your browser’s network tab. There it is: a relentless, repeating call to /api/user or /api/me, firing every few seconds or on every route change. It’s the digital equivalent of a kid in the back seat asking, "Are we there yet?"

This common pattern, known as polling, feels necessary to keep the UI in sync with the server's authentication state. What if the user’s session expires? What if they log out in another tab? Polling seems like the only way to know. But it’s inefficient, clogs your network logs, and puts unnecessary strain on your server. There’s a better way—a more elegant, event-driven approach that creates a silent, responsive, and robust authentication system.

In this post, I’ll walk you through the exact setup I use to ditch the polling madness. We'll leverage the power of Laravel Sanctum, React Context, and a little-known browser superpower to build an auth system that just *knows* when things change, across every tab, without ever having to ask.

The Old Way: A Cycle of Uncertainty

The traditional approach to auth in SPAs is born from a fundamental disconnect: the frontend is stateless, while the backend holds the truth. To bridge this gap, we often resort to constant verification.

A typical flow looks like this:

  1. User logs in. Laravel returns a session cookie.
  2. React stores a simple flag, like isAuthenticated: true.
  3. On every important navigation or page refresh, React can't trust its own state.
  4. React makes a GET /api/user request to ask Laravel, "Is this cookie still valid? Who is this user?"
  5. If the call succeeds, the UI proceeds. If it fails with a 401, React logs the user out.

This works, but it comes with significant downsides:

  • High Server Load: Every active user is hitting your API constantly, even when nothing has changed.
  • Poor Performance: UI rendering can be blocked or delayed waiting for the /user call to resolve, especially on slow networks.
  • No Cross-Tab Sync: If a user logs out in Tab A, Tab B remains blissfully unaware, thinking it's still logged in until its next poll. This leads to a confusing user experience where an action suddenly fails with a 401 error.

We can do so much better.

Our Tools for a Smarter Setup

Advertisement

To build our new system, we'll rely on three key pieces of technology that work together beautifully.

Laravel Sanctum: Our Secure Foundation

Laravel Sanctum is the perfect authentication solution for SPAs. It provides lightweight, cookie-based session authentication that feels just like traditional web auth but is designed for modern JavaScript frontends. It handles CSRF protection and session management out of the box, giving us a secure, stateful connection to our SPA without the complexity of managing JWTs in local storage.

React Context API: The Global State Manager

We need a way to provide authentication status (like the user object and a boolean `isLoggedIn` flag) to our entire React component tree without "prop drilling." The React Context API is the built-in solution for this. We'll create an AuthProvider that wraps our application and makes the auth state and methods (like `login` and `logout`) available to any component that needs them.

The Secret Sauce: Browser Storage Events

This is the magic ingredient that eliminates polling. All modern browsers fire a 'storage' event on a window whenever a change is made to localStorage or sessionStorage *from another tab or window on the same origin*. This is our hook! By making a tiny, insignificant change to localStorage when an auth event happens (like login or logout), we can notify all other open tabs instantly.

The New Flow: An Event-Driven Approach

Let's put the pieces together. Our goal is to create a system that fetches the user *once* on initial load and then only reacts to specific events.

Step 1: Configuring Laravel Sanctum

First, ensure Sanctum is configured for SPA authentication. In your .env file, specify which domains your SPA will run on:

SANCTUM_STATEFUL_DOMAINS=localhost:3000,your-app.com
SESSION_DOMAIN=localhost

In your config/cors.php, make sure your settings allow credentials and match your paths:

'paths' => ['api/*', 'login', 'logout'],
'allowed_origins' => ['http://localhost:3000'],
'supports_credentials' => true,

Finally, protect your API routes in routes/api.php with Sanctum's middleware. This will ensure only authenticated users can access them.

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

This /api/user route is crucial, but we promise we won't be calling it every five seconds.

Step 2: Building the React Auth Provider

In your React app, create an AuthContext. This will hold our user data and loading state.

// AuthContext.js
import { createContext, useState, useContext } from 'react';

const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
    const [user, setUser] = useState(null);
    const [isLoggedIn, setIsLoggedIn] = useState(false);

    // We will add more logic here soon...

    const value = { user, isLoggedIn, setUser, setIsLoggedIn };

    return (
        <AuthContext.Provider value={value}>
            {children}
        </AuthContext.Provider>
    );
};

export const useAuth = () => useContext(AuthContext);

We'll also need `login` and `logout` functions that interact with our Laravel API and, critically, notify other tabs.

// Inside AuthProvider...

const login = async (credentials) => {
    await axios.get('/sanctum/csrf-cookie');
    await axios.post('/login', credentials);
    // Fetch the user and update state
    refetchUser();
};

const logout = async () => {
    await axios.post('/logout');
    setUser(null);
    setIsLoggedIn(false);
    // **The notification part!**
    window.localStorage.setItem('logout', Date.now());
};

const refetchUser = async () => {
    try {
        const { data } = await axios.get('/api/user');
        setUser(data);
        setIsLoggedIn(true);
        // **The notification part!**
        window.localStorage.setItem('login', Date.now());
    } catch (error) {
        setUser(null);
        setIsLoggedIn(false);
    }
};

Notice how after a successful logout or refetch (after login), we write a key to localStorage. The key name itself is the event (`'logout'`, `'login'`), and the value is just a timestamp to ensure it's a new change.

Step 3: The One-Time Check on Load

We still need to know the user's status when the app first loads a tab. We do this with a single `useEffect` hook in our `AuthProvider`. This runs *only once* when the component mounts.

// Inside AuthProvider...
import { useEffect } from 'react';

// ... state and functions

useEffect(() => {
    // On initial load, try to fetch the user
    refetchUser();
}, []); // Empty dependency array means this runs once on mount

This is our replacement for polling. Instead of asking repeatedly, we ask once. If the user has a valid session cookie, `refetchUser` will succeed and populate our state. If not, it will fail, and our state will correctly reflect a logged-out user.

Step 4: Syncing Across Tabs with Browser Events

Here's the final piece of the puzzle. We add another `useEffect` to listen for the `storage` event. This hook will fire in all other tabs when `localStorage` is changed by our `login` or `logout` functions.

// Inside AuthProvider...

useEffect(() => {
    const syncAuth = (event) => {
        if (event.key === 'logout') {
            console.log('Logged out from another tab!');
            setUser(null);
            setIsLoggedIn(false);
        }
        if (event.key === 'login') {
            console.log('Logged in from another tab!');
            // Re-fetch user to get the latest data
            refetchUser();
        }
    };

    window.addEventListener('storage', syncAuth);

    return () => {
        window.removeEventListener('storage', syncAuth);
    };
}, []); // Runs once on mount

And that’s it! When a user logs out in Tab A, it sets the `'logout'` item in `localStorage`. Tab B, C, and D, which are all listening, will have their `syncAuth` function triggered. They'll see the `'logout'` key, immediately clear their local auth state, and update the UI to a logged-out view—all without a single API call.

Polling vs. Event-Driven: A Quick Comparison

Let's visualize the difference.

FeaturePolling ApproachEvent-Driven Approach
PerformanceSlower; UI can be blocked by frequent API calls.Faster; API is only called when necessary. UI is snappy.
Server LoadHigh and constant, even for idle users.Extremely low; only handles explicit auth events.
User ExperienceCan be janky; cross-tab state is out of sync.Smooth and instantaneous; state is synced across all tabs.
Code ComplexitySimpler to grasp initially, but harder to manage.Slightly more setup, but much cleaner and more robust long-term.

A Quieter, Smarter Frontend

By shifting from a "pull" model (constantly polling for changes) to a "push" model (reacting to events), we've created an authentication system that is more performant, scalable, and provides a significantly better user experience. Your network tab will be blissfully quiet, your server will thank you for the reduced load, and your users will appreciate an app that feels seamlessly in sync with their actions, no matter which tab they're in.

This pattern isn't just limited to authentication. You can use the same `storage` event technique to sync any kind of global state across tabs, like theme changes or user preferences. So go ahead, give your /api/me endpoint a well-deserved break and embrace the quiet efficiency of an event-driven frontend.

Tags

You May Also Like