✨ Introducing to you: new hooks 🐺 - Authentication and Forms made easy.


useBetterAuth

Flexible authentication hook with customizable storage, fetcher, auto-refresh, and cross-tab sync.

Main Authentication
Not Authenticated
Demonstration of the main auth hook with customizable storage and auto-refresh

Demo credentials:

Email: user@example.com

Password: password

Storage Comparison
Compare localStorage vs sessionStorage behavior

localStorage

Persists across browser sessions

Active

sessionStorage

Cleared when tab closes

Inactive

Switch storage types and notice how authentication persists differently. Try logging in, switching tabs, or refreshing the page.

Installation

Props

PropTypeDefaultDescription
storageKeystring'better-auth'Storage key prefix for storing authentication data
storageStorageLikelocalStorageCustom storage implementation (localStorage, sessionStorage, etc.)
loginUrlstring'/api/login'Login endpoint URL
refreshUrlstringundefinedToken refresh endpoint URL
logoutUrlstringundefinedLogout endpoint URL for server-side logout
refreshIntervalnumberundefinedAuto-refresh interval in milliseconds
onAuthChange(token: string | null, user: TUser | null) => voidundefinedCallback fired when authentication state changes
fetcher(credentials: TCredentials, url: string) => Promise<BetterAuthResponse<TUser>>undefinedCustom fetcher function for authentication requests

Data

PropertyTypeDescription
tokenstring | nullCurrent authentication token
userTUser | nullCurrent user object
loadingbooleanLoading state for authentication operations
errorError | nullCurrent error state
isAuthenticatedbooleanComputed boolean indicating if user is authenticated
login(credentials: TCredentials) => Promise<void>Function to authenticate user
logout() => voidFunction to logout user
refresh() => Promise<void>Function to manually refresh authentication token
updateUser(user: TUser, token?: string) => voidFunction to update user data and optionally token
clearError() => voidFunction to clear current error state

Types

StorageLike

interface StorageLike {
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
  removeItem(key: string): void;
}

BetterAuthResponse

interface BetterAuthResponse<TUser> {
  token: string;
  user: TUser;
}

DefaultCredentials

interface DefaultCredentials extends Record<string, string | number | boolean> {
  email: string;
  password: string;
}

Key Features & Details

  • Customizable Storage: Use localStorage, sessionStorage, or provide your own storage implementation
  • Custom Fetcher: Completely customize how authentication requests are made
  • Automatic Token Refresh: Configure automatic token refresh at specified intervals
  • Cross-tab Synchronization: Authentication state syncs across browser tabs automatically
  • SSR Safe: Properly handles server-side rendering without hydration issues
  • Type Safety: Full TypeScript support with generic user and credentials types
  • Error Handling: Comprehensive error handling with clear error states
  • Debug Logging: Development mode logging for debugging authentication flows

Common Use Cases

  • Custom authentication APIs that don't use OAuth
  • Applications requiring specific storage mechanisms (sessionStorage, custom storage)
  • Token-based authentication with automatic refresh
  • Multi-tab applications requiring synchronized auth state
  • Applications with custom user credential structures
  • Development and testing environments with mock authentication

Examples

Basic Usage

interface User {
  id: string;
  name: string;
  email: string;
}
 
const auth = useBetterAuth<User>({
  loginUrl: '/api/auth/login',
  refreshUrl: '/api/auth/refresh',
});
 
const handleLogin = async () => {
  try {
    await auth.login({
      email: 'user@example.com',
      password: 'password',
    });
  } catch (error) {
    console.error('Login failed:', error);
  }
};
 
return (
  <div>
    {auth.isAuthenticated ? (
      <div>
        <p>Welcome, {auth.user?.name}!</p>
        <button onClick={auth.logout}>Logout</button>
      </div>
    ) : (
      <button onClick={handleLogin}>Login</button>
    )}
  </div>
);

With Custom Storage

const auth = useBetterAuth<User>({
  storage: sessionStorage, // Use sessionStorage instead of localStorage
  storageKey: 'my-app-auth',
  onAuthChange: (token, user) => {
    console.log('Auth state changed:', { token: !!token, user });
  },
});
 
return (
  <div>
    <p>Using sessionStorage - auth cleared when tab closes</p>
    {/* ... rest of component */}
  </div>
);

With Custom Fetcher

interface CustomCredentials extends Record<string, string | number | boolean> {
  username: string;
  password: string;
  organizationId: string;
}
 
const auth = useBetterAuth<User, CustomCredentials>({
  fetcher: async (credentials, url) => {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Organization-ID': credentials.organizationId,
      },
      body: JSON.stringify({
        username: credentials.username,
        password: credentials.password,
      }),
    });
 
    if (!response.ok) {
      throw new Error('Authentication failed');
    }
 
    const data = await response.json();
    return {
      token: data.access_token,
      user: data.user_profile,
    };
  },
});
 
const handleLogin = async () => {
  await auth.login({
    username: 'john_doe',
    password: 'secret123',
    organizationId: 'org_123',
  });
};

With Auto-Refresh

const auth = useBetterAuth<User>({
  loginUrl: '/api/auth/login',
  refreshUrl: '/api/auth/refresh',
  refreshInterval: 5 * 60 * 1000, // Refresh every 5 minutes
  onAuthChange: (token, user) => {
    if (!token && user) {
      // Token expired and refresh failed
      console.log('Session expired, redirecting to login');
      window.location.href = '/login';
    }
  },
});
 
return (
  <div>
    <p>Token refreshes automatically every 5 minutes</p>
    <button onClick={auth.refresh}>Manual Refresh</button>
  </div>
);

With Error Handling

const auth = useBetterAuth<User>({
  loginUrl: '/api/auth/login',
});
 
const handleLogin = async () => {
  try {
    await auth.login(credentials);
  } catch (error) {
    // Error is also available in auth.error
    console.error('Login error:', error.message);
  }
};
 
return (
  <div>
    {auth.error && (
      <div className="error">
        <p>Error: {auth.error.message}</p>
        <button onClick={auth.clearError}>Clear Error</button>
      </div>
    )}
 
    <button onClick={handleLogin} disabled={auth.loading}>
      {auth.loading ? 'Logging in...' : 'Login'}
    </button>
  </div>
);

Updating User Data

const auth = useBetterAuth<User>({
  loginUrl: '/api/auth/login',
});
 
const handleUpdateProfile = async (newUserData: Partial<User>) => {
  if (!auth.user) return;
 
  try {
    // Update user on server
    const response = await fetch('/api/user/profile', {
      method: 'PATCH',
      headers: {
        Authorization: `Bearer ${auth.token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(newUserData),
    });
 
    const updatedUser = await response.json();
 
    // Update local user state
    auth.updateUser(updatedUser);
  } catch (error) {
    console.error('Profile update failed:', error);
  }
};
 
return (
  <div>
    {auth.user && (
      <div>
        <p>Name: {auth.user.name}</p>
        <button onClick={() => handleUpdateProfile({ name: 'New Name' })}>
          Update Name
        </button>
      </div>
    )}
  </div>
);