npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

swr-login

v0.4.0

Published

The best React Hook for auth state management. Works with any backend.

Readme

🔐 swr-login

The best React Hook for auth state management.

Works with Auth.js, Better Auth, Clerk, or your own backend.

npm bundle size license TypeScript

English · 中文


Features

  • SWR-Powered — Stale-while-revalidate strategy for instant auth state. No loading spinners.
  • 🔌 Plugin Architecture — Every login channel is an independent npm package. Install only what you need.
  • 🔄 Zero-Refresh Login/Logout — Token silent refresh, optimistic UI, cross-tab sync via BroadcastChannel.
  • 🪶 Tiny Core@swr-login/core is <3KB gzipped. No bloat.
  • 🛡️ Secure by Default — PKCE, CSRF state validation, HttpOnly cookie support, sensitive data cleanup.
  • 📦 100% TypeScript — Full generics, JSDoc on every API, AI-coding-assistant friendly.
  • 🎯 Framework Agnostic Core — React bindings in @swr-login/react, core logic is UI-free.
  • 🔗 Multi-Step Login — Built-in support for multi-step interactive login flows (MFA, SMS verification, class-code login, etc.).
  • 🪝 afterAuth Hook — Intercept post-login flow before fetchUser. Role-based redirect, skip fetchUser, or abort login.
  • 🛡️ fetchUser Error Handling — Built-in validateUserOnLogin, onFetchUserError callback with retry / logout / ignore strategies.

Quick Start

npm install swr-login react swr
import { SWRLoginProvider, useLogin, useUser, useLogout } from 'swr-login';
import { presets } from 'swr-login/presets';

// 1. One-line config with presets
const config = presets.password({
  loginUrl: '/api/login',
  userUrl: '/api/me',
});

function App() {
  return (
    <SWRLoginProvider config={config}>
      <MyApp />
    </SWRLoginProvider>
  );
}

// 2. Use hooks anywhere
function LoginButton() {
  const { login, isLoading } = useLogin('password');
  const { user, isAuthenticated } = useUser();
  const { logout } = useLogout();

  if (isAuthenticated) {
    return (
      <div>
        <span>Hello, {user.name}!</span>
        <button onClick={() => logout()}>Sign Out</button>
      </div>
    );
  }

  return (
    <button
      onClick={() => login({ username: 'alice', password: 'secret' })}
      disabled={isLoading}
    >
      Sign In
    </button>
  );
}

That's it. No page refresh. All components using useUser() update instantly.

What's Inside swr-login

The swr-login package is an all-in-one umbrella package — install it once and you get everything. No need to install sub-packages separately.

npm install swr-login
# That's all you need. Every adapter and plugin is included.

It bundles the following sub-packages and re-exports them via sub-path imports:

| Sub-path Import | Included Package | Description | |-----------------|-----------------|-------------| | swr-login | @swr-login/core + @swr-login/react | Core logic + React bindings (Provider, Hooks, AuthGuard) | | swr-login/presets | — | Ready-to-use config presets (password, social, passkey, full) | | swr-login/adapters/jwt | @swr-login/adapter-jwt | JWT token storage (localStorage / sessionStorage / memory) | | swr-login/adapters/session | @swr-login/adapter-session | Session storage (tab-scoped, cleared on close) | | swr-login/adapters/cookie | @swr-login/adapter-cookie | Cookie storage (BFF pattern, HttpOnly support) | | swr-login/plugins/password | @swr-login/plugin-password | Username / Password login | | swr-login/plugins/oauth-google | @swr-login/plugin-oauth-google | Google OAuth 2.0 + PKCE | | swr-login/plugins/oauth-github | @swr-login/plugin-oauth-github | GitHub OAuth | | swr-login/plugins/oauth-wechat | @swr-login/plugin-oauth-wechat | WeChat QR Code / H5 Web Auth | | swr-login/plugins/passkey | @swr-login/plugin-passkey | WebAuthn / Passkey (Biometric / Security Key) |

All adapters and plugins are declared as optionalDependencies, so your package manager installs them automatically but they won't break your build if a platform doesn't support one.

💡 Prefer granular control? You can still install individual @swr-login/* scoped packages — see Fine-grained Imports.

Architecture

┌─────────────────────────────────────────────────────┐
│                   Your React App                     │
├─────────────────────────────────────────────────────┤
│   Hook Layer (@swr-login/react)                      │
│   useLogin · useUser · useLogout · useSession        │
│   useMultiStepLogin · useAuthInjector                │
│   usePermission · AuthGuard                          │
├─────────────────────────────────────────────────────┤
│   Plugin Layer                                       │
│   🔑 password · 🔵 google · ⚫ github               │
│   🟢 wechat · 🔐 passkey                            │
├─────────────────────────────────────────────────────┤
│   Core Layer (@swr-login/core)                       │
│   StateMachine · TokenManager · PluginManager        │
│   EventEmitter · BroadcastSync                       │
├─────────────────────────────────────────────────────┤
│   Storage Adapters                                   │
│   JWT · Session · Cookie (BFF)                       │
└─────────────────────────────────────────────────────┘

Presets

Presets provide ready-to-use configurations for common auth scenarios. Just provide the essential parameters:

Password Login

import { presets } from 'swr-login/presets';

const config = presets.password({
  loginUrl: '/api/auth/login',
  logoutUrl: '/api/auth/logout',
  userUrl: '/api/me',
});

Social Login (OAuth)

const config = presets.social({
  providers: {
    github: { clientId: 'your-github-client-id' },
    google: { clientId: 'your-google-client-id' },
    wechat: { appId: 'wx_your_app_id' },
  },
  userUrl: '/api/me',
});

Passkey (WebAuthn)

const config = presets.passkey({
  registerOptionsUrl: '/api/auth/passkey/register/options',
  registerVerifyUrl: '/api/auth/passkey/register/verify',
  loginOptionsUrl: '/api/auth/passkey/login/options',
  loginVerifyUrl: '/api/auth/passkey/login/verify',
  userUrl: '/api/me',
});

Full (Password + Social + Passkey)

const config = presets.full({
  password: { loginUrl: '/api/auth/login' },
  providers: {
    github: { clientId: 'gh-client-id' },
    google: { clientId: 'google-client-id' },
  },
  passkey: {
    registerOptionsUrl: '/api/auth/passkey/register/options',
    registerVerifyUrl: '/api/auth/passkey/register/verify',
    loginOptionsUrl: '/api/auth/passkey/login/options',
    loginVerifyUrl: '/api/auth/passkey/login/verify',
  },
  userUrl: '/api/me',
});

All presets support extra options like onLogin, onLogout, onError, security, and adapterOptions.

createAuthConfig

For advanced use cases, use createAuthConfig to get full type-safety and IDE auto-completion when building config manually:

// auth.config.ts
import { createAuthConfig } from 'swr-login';
import { JWTAdapter } from 'swr-login/adapters/jwt';
import { PasswordPlugin } from 'swr-login/plugins/password';

export default createAuthConfig({
  adapter: JWTAdapter({ storage: 'localStorage' }),
  plugins: [PasswordPlugin({ loginUrl: '/api/auth/login' })],
  fetchUser: (token) =>
    fetch('/api/me', { headers: { Authorization: `Bearer ${token}` } })
      .then(r => r.json()),
  onLogin: (user) => console.log('Logged in:', user.name),

  // Intercept after plugin login, before fetchUser
  afterAuth: async ({ pluginName, authResponse, skipFetchUser }) => {
    const role = decodeJwt(authResponse.accessToken).role;
    if (role === 'admin') {
      skipFetchUser(); // admins don't need fetchUser
      window.location.href = '/admin';
    }
  },

  // Validate user immediately after login (default: true)
  validateUserOnLogin: true,

  // Handle fetchUser errors globally
  onFetchUserError: (error) => {
    if (error.message.includes('disabled')) return 'logout';
    return 'ignore';
  },

  // Customize SWR revalidation behavior for useUser()
  swrOptions: {
    revalidateOnFocus: false,     // disable revalidation on window focus
    revalidateOnReconnect: true,  // revalidate when network recovers
  },
});
// App.tsx
import { SWRLoginProvider } from 'swr-login';
import authConfig from './auth.config';

function App() {
  return (
    <SWRLoginProvider config={authConfig}>
      <MyApp />
    </SWRLoginProvider>
  );
}

Lifecycle Hooks

afterAuth

The afterAuth hook runs after a plugin's login() succeeds but before fetchUser is called. This gives you a chance to inspect the auth response, perform role-based redirects, or skip fetchUser entirely.

const config = createAuthConfig({
  // ...plugins, adapter, fetchUser...
  afterAuth: async ({ pluginName, authResponse, skipFetchUser }) => {
    // Example: redirect teachers to a different app
    if (pluginName === 'password') {
      const role = await checkUserRole(authResponse.accessToken);
      if (role === 'teacher') {
        skipFetchUser();
        window.location.href = '/teacher-admin';
        return;
      }
    }
    // Default: continue to fetchUser
  },
});

| Behavior | How | |----------|-----| | Continue to fetchUser | Return normally (don't call skipFetchUser) | | Skip fetchUser | Call context.skipFetchUser()login() resolves immediately | | Abort login & rollback tokens | Throw an error — tokens are cleared, login() rejects |

Note: afterAuth only runs during explicit login() calls, not during SWR background revalidation.

onFetchUserError

Handle errors from fetchUser globally — both during login validation and SWR background revalidation:

const config = createAuthConfig({
  // ...
  onFetchUserError: (error) => {
    if (error.message.includes('account disabled')) return 'logout';
    if (error.message.includes('network')) return 'retry';
    return 'ignore'; // keep current state, error stored in lastError
  },
});

| Strategy | Effect | |----------|--------| | 'retry' | Re-invoke fetchUser once (max 1 retry to prevent loops) | | 'logout' | Clear tokens, transition to unauthenticated | | 'ignore' | Keep current state; error is available via useUser().lastError |

validateUserOnLogin

When true (default), login() automatically calls fetchUser after the plugin succeeds. If fetchUser throws (e.g., "account disabled"), login() rejects and tokens are rolled back.

Set to false to skip this validation:

const config = createAuthConfig({
  validateUserOnLogin: false, // login() resolves without calling fetchUser
});

swrOptions

Customize the SWR behavior of the internal useUser() hook. Only a safe subset of SWR options is exposed — internal options like fetcher and shouldRetryOnError are managed by swr-login.

const config = createAuthConfig({
  // ...
  swrOptions: {
    revalidateOnFocus: false,      // default: true
    revalidateOnReconnect: false,  // default: true
    dedupingInterval: 5000,        // default: 2000 (ms)
    focusThrottleInterval: 10000,  // default: 5000 (ms)
    refreshInterval: 30000,        // default: 0 (disabled)
  },
});

| Option | Default | Description | |--------|---------|-------------| | revalidateOnFocus | true | Revalidate user data when window gets focused | | revalidateOnReconnect | true | Revalidate when browser regains network | | dedupingInterval | 2000 | Dedupe requests with same key in this time span (ms) | | focusThrottleInterval | 5000 | Throttle focus revalidation events (ms) | | refreshInterval | 0 | Polling interval (ms). 0 = disabled |

Tip: If useUser() was previously hardcoding revalidateOnFocus: true and you need to disable it (e.g., to avoid unnecessary API calls on tab switch), set swrOptions.revalidateOnFocus to false.

Core Hooks

| Hook | Purpose | |------|--------| | useLogin(pluginName?) | Trigger login flow via any registered plugin | | useMultiStepLogin(pluginName) | Drive multi-step login flows with step state management | | useAuthInjector() | Inject external auth state into swr-login (escape hatch) | | useUser<T>() | Get current user with SWR caching, auto-revalidation, lastError & clearError, lastChangeSource & lastChangeEvent | | useUserChange<T>() | Subscribe to user transitions as a discrete event stream (re-renders on each change) | | useUserChangeEffect(cb) | Register a side-effect callback on user transitions without causing re-renders | | useUserChangeOn(source, cb) | Filtered variant of useUserChangeEffect — fires only when source matches | | useLogout() | Secure logout with cross-tab broadcast | | useAdapter() | Synchronous hasAuth() check + raw adapter access (useful for homepage auto-redirect before SWR hydration) | | useSession() | Access raw tokens, expiry info | | usePermission() | Check roles & permissions declaratively |

User Change Source

useUser() now exposes lastChangeSource and lastChangeEvent so you can tell why the user value changed — not just that it changed.

UserChangeSource values

| Source | When it fires | |--------|---------------| | 'initial' | Provider first mount; fetchUser resolved for the first time | | 'login' | Explicit login() / multi-step finalize / injectAuth() call | | 'logout' | Explicit logout() / injectLogout() call | | 'revalidate' | SWR background revalidation (focus / reconnect / polling / manual mutate()) produced a different user | | 'external' | Cross-tab sync via BroadcastChannel / storage events |

Quick examples

// 1. Declarative snapshot via useUser()
const { user, isLoading, lastChangeSource } = useUser();

useEffect(() => {
  // Suppress welcome toast on cold-start page refresh
  if (user && lastChangeSource === 'login') {
    toast.success(`Welcome back, ${user.name}!`);
  }
}, [user, lastChangeSource]);
// 2. Subscribe to every transition without re-rendering
useUserChangeEffect((e) => {
  if (e.source === 'login') analytics.track('user_login', { userId: e.user?.id });
  if (e.source === 'logout') analytics.track('user_logout', { userId: e.previousUser?.id });
});
// 3. Filter by source — fires only on explicit login
useUserChangeOn('login', (e) => router.push('/dashboard'));

// 4. Multiple sources at once
useUserChangeOn(['login', 'external'], (e) => refreshSidebar());
// 5. Discrete event stream (re-renders on each change)
const change = useUserChange();
useEffect(() => {
  if (change?.source === 'external') refreshSidebar();
}, [change]);

Tip: Use lastChangeSource === 'initial' to detect an existing session on page load (e.g., to show a redirect overlay) without triggering the same logic after an explicit login.

AuthGuard Component

<AuthGuard
  permissions={['admin', 'editor']}
  requireAll={false}
  fallback={<Navigate to="/login" />}
  loadingComponent={<Skeleton />}
>
  <ProtectedContent />
</AuthGuard>

Official Plugins

| Package | Channel | Auth Method | |---------|---------|-------------| | swr-login/plugins/password | Username/Password | Form POST | | swr-login/plugins/oauth-google | Google | OAuth 2.0 + PKCE (Popup/Redirect) | | swr-login/plugins/oauth-github | GitHub | OAuth (Popup/Redirect) | | swr-login/plugins/oauth-wechat | WeChat | QR Code Scan / H5 Web Auth | | swr-login/plugins/passkey | Passkey/WebAuthn | Biometric / Security Key |

Storage Adapters

| Package | Strategy | Best For | |---------|----------|----------| | swr-login/adapters/jwt | localStorage / sessionStorage / memory | SPAs (default) | | swr-login/adapters/session | sessionStorage | Tab-scoped sessions | | swr-login/adapters/cookie | Cookie (SameSite + Secure) | BFF pattern |

Multi-Step Login

For login flows that require multiple steps with UI interaction in between (e.g., class-code login, MFA, SMS verification), use MultiStepLoginPlugin + useMultiStepLogin:

Define a Multi-Step Plugin

import type { MultiStepLoginPlugin } from 'swr-login';

const ClassCodePlugin: MultiStepLoginPlugin<{ classCode: string; loginCode: string }> = {
  name: 'class-code',
  type: 'multi-step',
  steps: [
    {
      name: 'verify-code',
      async execute({ classCode, loginCode }, ctx) {
        const res = await fetch('/api/class-login', {
          method: 'POST',
          body: JSON.stringify({ classCode, loginCode }),
        }).then(r => r.json());
        return { classLoginToken: res.class_login_token };
      },
    },
    {
      name: 'select-student',
      async execute({ classLoginToken }, ctx) {
        const res = await fetch(`/api/class-students?token=${classLoginToken}`)
          .then(r => r.json());
        return { students: res.list, classLoginToken };
      },
    },
    {
      name: 'get-token',
      async execute({ userId, classLoginToken }, ctx) {
        const res = await fetch('/api/class-student-token', {
          method: 'POST',
          body: JSON.stringify({ userId, classLoginToken }),
        }).then(r => r.json());
        return { skey: res.skey, userId: res.user_id };
      },
    },
  ],
  async finalizeAuth({ skey, userId }, ctx) {
    ctx.setTokens({ accessToken: skey, expiresAt: Date.now() + 86400000 });
    return {
      user: { id: userId, name: '' },
      accessToken: skey,
      expiresAt: Date.now() + 86400000,
    };
  },
  // Required by SWRLoginPlugin interface, but not used for multi-step
  async login() { throw new Error('Use useMultiStepLogin()'); },
};

Use in Components

import { useMultiStepLogin } from 'swr-login';

function ClassCodeLoginFlow() {
  const {
    currentStepName, stepData, start, next, back, isLoading, error, isComplete,
  } = useMultiStepLogin<{ classCode: string; loginCode: string }>('class-code');

  if (isComplete) return <Navigate to="/dashboard" />;

  if (currentStepName === 'select-student') {
    return (
      <StudentList
        students={stepData.students}
        onSelect={(userId) => next({ userId, classLoginToken: stepData.classLoginToken })}
        onBack={back}
        isLoading={isLoading}
      />
    );
  }

  // Default: show the code input form
  return <ClassCodeForm onSubmit={start} isLoading={isLoading} error={error} />;
}

Inject External Auth State

For cases where the login flow is entirely managed outside swr-login (e.g., third-party SDK, iframe login), use useAuthInjector as an escape hatch:

import { useAuthInjector } from 'swr-login';

function ExternalLoginCallback() {
  const { injectAuth } = useAuthInjector();

  const handleCallback = async (token: string, user: User) => {
    await injectAuth({
      user,
      accessToken: token,
      expiresAt: Date.now() + 86400000,
    });
    // All useUser() / AuthGuard components now recognize the login state
    router.push('/dashboard');
  };
}

Custom Plugin

Build your own login channel in minutes:

import type { SWRLoginPlugin } from 'swr-login';

const MyPlugin: SWRLoginPlugin<{ token: string }> = {
  name: 'my-sso',
  type: 'oauth',
  async login({ token }, ctx) {
    const res = await fetch('/api/sso/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token }),
    });
    const data = await res.json();
    ctx.setTokens({ accessToken: data.accessToken, expiresAt: data.expiresAt });
    return data;
  },
};

Comparison

| Feature | swr-login | Auth.js v5 | Clerk | Better Auth | |---------|:---------:|:----------:|:-----:|:-----------:| | Zero-refresh login/logout | ✅ Best | ⚠️ | ✅ | ⚠️ | | Bundle size (core) | <3KB | ~30KB | ~50KB | ~15KB | | Plugin architecture | ✅ | ❌ | ❌ | ✅ | | WeChat / Alipay | ✅ | ❌ | ❌ | ❌ | | Framework agnostic core | ✅ | ⚠️ | ❌ | ✅ | | Full TypeScript generics | ✅ | ⚠️ | ✅ | ✅ | | Free & open source | ✅ MIT | ✅ MIT | ❌ Paid | ✅ MIT |

Security

  • PKCE enforced for all OAuth flows
  • CSRF state parameter validation on every callback
  • BFF-friendly cookie adapter for HttpOnly token storage
  • Cross-tab sync logout via BroadcastChannel
  • Auto token cleanup on page visibility change (optional)

Fine-grained Imports

If you prefer installing only the packages you need, all sub-packages are available individually:

npm install @swr-login/react @swr-login/adapter-jwt @swr-login/plugin-password
import { SWRLoginProvider, useLogin } from '@swr-login/react';
import { JWTAdapter } from '@swr-login/adapter-jwt';
import { PasswordPlugin } from '@swr-login/plugin-password';

See the full list of scoped packages in the 中文文档.

License

MIT


Documentation · Examples · Discord

Made with ❤️ for the React community