Web Development

Stop Polling /me: Sync Auth in React & Laravel Sanctum

Tired of polling your /me endpoint? Learn how to instantly sync authentication state across browser tabs in your React and Laravel Sanctum app for a seamless UX.

A

Alexandre Dubois

Full-stack developer specializing in building robust, scalable applications with React and Laravel.

7 min read19 views

Stop Polling /me: Sync Auth in React & Laravel Sanctum

Picture this: you’ve built a beautiful React single-page application (SPA) powered by a robust Laravel Sanctum backend. A user has your app open in two browser tabs. They work for a bit, then click “Logout” in Tab A. They switch to Tab B. What do they see?

Crickets. Tab B still shows them as logged in. The UI is full of personalized data, menus, and actions that are no longer valid. It’s a confusing and jarring user experience that only resolves itself after a page refresh or a failed API call. Sound familiar?

The common “fix” is to poll an endpoint like /api/user or /me every 30 seconds from every open tab. While this eventually works, it’s a clumsy, inefficient solution that spams your server with unnecessary requests. It’s a band-aid, not a cure.

Today, we’re ripping off that band-aid. We’ll explore a modern, event-driven approach using built-in browser APIs to keep authentication state perfectly and instantly in sync across all tabs. Let’s stop polling and start building a truly responsive user experience.

The Polling Trap: Why Constant /me Checks Are a Problem

Before we jump to the solution, let's be clear about why the polling approach is an anti-pattern. Developers often implement something like this in their main layout or context provider:

// The common (but inefficient) way
import { useEffect } from 'react';
import { useAuth } from './AuthContext';

function App() {
  const { checkAuthStatus } = useAuth();

  useEffect(() => {
    // Check on mount
    checkAuthStatus();

    // And check again every 30 seconds
    const intervalId = setInterval(checkAuthStatus, 30000);

    return () => clearInterval(intervalId);
  }, [checkAuthStatus]);

  // ... rest of the app
}

This pattern introduces several significant problems:

  • Network Chatter: If a user has 5 tabs open, that’s 5 API requests to your server every 30 seconds, just to confirm something that likely hasn't changed. This adds up quickly across many users.
  • Server Load: Each of these requests hits your Laravel backend, spinning up PHP, running middleware, and querying the database just to return the same user object. It's death by a thousand cuts for your server resources.
  • Delayed UX: The user experience is only as good as your polling interval. With a 30-second interval, a user who logs out in one tab will see a stale, logged-in state for up to 30 seconds in another. That’s an eternity in web time.
  • Wasted Client Resources: While minor, it's still unnecessary work for the browser to be constantly firing off network requests in the background.

We’re not asking for information that changes frequently. We’re asking for a state that changes on specific, user-driven events: login and logout. So why not just listen for those events?

The Solution: Event-Driven Synchronization with BroadcastChannel

Instead of constantly *pulling* state from the server, we can have our application tabs *push* state changes to each other. Modern browsers provide a beautiful, purpose-built API for this: the Broadcast Channel API.

The BroadcastChannel allows scripts from the same origin (e.g., https://yourapp.com) to send and receive messages between different browser tabs, windows, or iframes. It’s like a private chat room for your application's open instances.

Advertisement

Our strategy will be simple:

  1. When a user logs in or out in one tab, that tab will broadcast a message (e.g., { type: 'LOGOUT' }) to the channel.
  2. All other open tabs listening to the channel will receive this message.
  3. Upon receiving the message, they will update their own local state accordingly (e.g., clear the user data and redirect to the login page).

This process is nearly instantaneous and involves zero server requests beyond the initial logout call.

Building a `useAuthSync` Hook in React

To make this logic reusable and clean, we'll encapsulate it in a custom React hook. Let's call it useAuthSync.

The `useAuthSync` Hook Code

First, let's create a shared file for our channel and message-posting logic. This ensures we use the same channel instance everywhere.

// src/lib/auth-channel.js

// We create a single instance of the BroadcastChannel.
// The name 'auth-channel' is arbitrary, but should be unique to this feature.
const channel = new BroadcastChannel('auth-channel');

/**
 * Posts a message to the auth channel.
 * @param {object} message - The message to send.
 */
export const postAuthMessage = (message) => {
  channel.postMessage(message);
};

// We export the channel itself to be used in our hook.
export default channel;

Now, we can create the hook that listens for messages on this channel.

// src/hooks/useAuthSync.js
import { useEffect } from 'react';
import channel from '../lib/auth-channel';

/**
 * A hook to sync authentication state across tabs.
 * @param {() => void} onLogin - Callback for when another tab logs in.
 * @param {() => void} onLogout - Callback for when another tab logs out.
 */
export const useAuthSync = (onLogin, onLogout) => {
  useEffect(() => {
    const handleMessage = (event) => {
      console.log('Received auth message:', event.data);
      if (event.data.type === 'LOGOUT') {
        onLogout();
      }
      if (event.data.type === 'LOGIN') {
        onLogin();
      }
    };

    channel.addEventListener('message', handleMessage);

    // Clean up the listener when the component unmounts.
    return () => {
      channel.removeEventListener('message', handleMessage);
    };
  }, [onLogin, onLogout]); // Re-run effect if callbacks change
};

Integrating with Your Auth Context

The hook is useless on its own. It needs to be integrated into your global authentication state management, whether that's React Context, Zustand, or Redux. Here’s how you might do it with a standard React Context.

Imagine you have an AuthProvider that manages the user state and provides login and logout functions.

// src/contexts/AuthContext.js (simplified)
import React, { createContext, useState, useContext, useCallback } from 'react';
import { useAuthSync } from '../hooks/useAuthSync';
import { postAuthMessage } from '../lib/auth-channel';
import api from '../lib/api'; // Your configured Axios/fetch instance

const AuthContext = createContext(null);

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

  // This is the function that will be called by other tabs.
  const handleRemoteLogout = useCallback(() => {
    setUser(null);
    // Optionally, redirect to login page
    // window.location.href = '/login';
  }, []);

  // This function would refetch user data.
  const handleRemoteLogin = useCallback(async () => {
    const { data } = await api.get('/api/user');
    setUser(data);
  }, []);

  // Set up the listener
  useAuthSync(handleRemoteLogin, handleRemoteLogout);

  const logout = async () => {
    try {
      await api.post('/logout');
      // Clear local state
      setUser(null);
      // Tell other tabs to log out!
      postAuthMessage({ type: 'LOGOUT' });
    } catch (error) {
      console.error('Failed to log out', error);
    }
  };

  // ... other functions like login, etc.

  const value = { user, logout };

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

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

With this setup, when logout() is called in one tab:

  1. It sends a request to your Laravel Sanctum /logout endpoint.
  2. The server invalidates the session and removes the HTTPOnly session cookie.
  3. On success, it clears the local user state.
  4. Crucially, it calls postAuthMessage({ type: 'LOGOUT' }).
  5. Every other tab, via the useAuthSync hook, receives this message and runs handleRemoteLogout, clearing its own state instantly.

The result? A perfectly synchronized, snappy logout experience across all tabs.

What About Laravel Sanctum?

It's important to understand that this is a client-side solution to a client-side problem. Your Laravel Sanctum backend is already doing its job perfectly.

When you use Sanctum's SPA authentication, it relies on Laravel's standard cookie-based sessions. When your React app calls the /logout endpoint, Laravel invalidates the session on the server and sends back headers to instruct the browser to delete the session cookie. This is secure and robust.

The problem we solved isn't a security one; it's a UX one. Without our BroadcastChannel solution, Tab B is still unaware of the logout. Its next API request would correctly fail with a 401 Unauthorized or 419 Authentication Timeout, but our solution ensures the UI updates *before* that failed request ever happens.

Conclusion: A Smoother, More Responsive UX

By replacing the inefficient polling anti-pattern with an event-driven approach using BroadcastChannel, we achieve several key benefits:

  • Instantaneous UI Updates: Authentication state is synchronized across tabs in real-time.
  • Reduced Server Load: We eliminate countless unnecessary /api/user requests.
  • Improved User Experience: The application feels more robust, responsive, and professional.
  • Cleaner Code: The logic is neatly encapsulated in a reusable hook.

This small architectural change has a massive impact on the perceived quality of your application. It’s a simple yet powerful technique that moves your SPA one step closer to feeling like a seamless, native-like experience. So go ahead, stop polling, and give your users the responsive application they deserve.

You May Also Like