SUPER EARLY WIP — USE AT YOUR OWN RISK■
shoo
GitHubCOMING SOON
API Reference

@shoojs/react

React hook and Convex adapter for Shoo

Install

npm install @shoojs/react

Peer dependency: react >= 18. Both exports are safe to import and render on the server. They create the browser client after mount.

useShooAuth(options?)

import { useShooAuth } from "@shoojs/react";

export function AuthStatus() {
  const { status } = useShooAuth();
  return <p>{status}</p>;
}

Options

Accepts all ShooAuthOptions, plus:

OptionTypeDefaultDescription
autoHandleCallbackbooleantrueFinish a pending sign-in on mount
autoSessionMonitorbooleantrueCheck the token with Shoo on an interval
sessionMonitorIntervalMsnumber60000Check interval

Options are read once on mount. The monitor options can change later.

Return value

PropertyTypeDescription
status"loading" | "signed_in" | "signed_out" | "expired"Current state. Use this to choose what to render.
identityShooIdentityuserId (the pairwise_sub) and token
claimsIdentityClaims | nullDecoded claims for display. Not verified.
sessionState"unknown" | "active" | "login_required"Last result from Shoo's session check
loadingbooleanSame as status === "loading"
errorstring | nullLast sign-in or callback error
signIn(params?) => Promise<void>Navigate to Shoo. Accepts StartSignInOptions.
clearIdentity() => voidApp sign-out in this browser
checkSession() => Promise<SessionCheckResult>Check with Shoo now
handleCallback(params?) => Promise<TokenResponse | null>Use when autoHandleCallback is false
refreshIdentity() => voidRe-read stored identity
authClientShooAuthClient | nullThe underlying client, after mount

Behavior

  • One callback makes one exchange, even under Strict Mode or with several hook consumers.
  • All consumers in this tab and other tabs update when the identity changes.
  • When the token expires, status becomes "expired". The hook never navigates by itself. Call signIn() from a user action.
  • signIn() keeps the profile scope of the previous token unless you pass requestPii. If the previous token had no profile data, the next sign-in does not ask for it.
  • signIn() rejects when it cannot start, for example when sessionStorage is not writable. It also sets error, so you can render the message. If you do not catch the rejection, the browser logs it as unhandled.
  • Each mounted useShooAuth runs its own session monitor and its own requests to Shoo. Call the hook once in a shared provider and pass the result down with context. Or set autoSessionMonitor: false on the other calls.
  • If a session check returns login_required, the hook clears the stored identity.
  • checkSession() always returns the result. If the token changed or the component unmounted before the result arrived, the hook does not apply it to its state.
  • The known limits of @shoojs/auth also apply: device clock, one SDK copy per page, and sign-in state in sessionStorage.
Profile.tsx
import { useShooAuth } from "@shoojs/react";

export function Profile() {
  const { status, identity, claims, error, signIn, clearIdentity } = useShooAuth();

  if (status === "loading") return <p>Loading…</p>;
  if (status !== "signed_in") {
    return (
      <div>
        {error && <p role="alert">{error}</p>}
        <button
          onClick={() => {
            void signIn().catch(() => {
              // The hook sets error, which is rendered above.
            });
          }}
        >
          {status === "expired" ? "Sign in again" : "Sign in"}
        </button>
      </div>
    );
  }

  return (
    <div>
      <p>ID: {identity.userId}</p>
      {claims?.email && <p>Email: {claims.email}</p>}
      <button onClick={clearIdentity}>Sign out</button>
    </div>
  );
}

createShooConvexAuth(options)

Call once at module scope. Pass useAuth to ConvexProviderWithAuth. See the Convex guide.

src/shoo.ts
import { createShooConvexAuth } from "@shoojs/react";

export const { useAuth, signIn, signOut } = createShooConvexAuth({
  callbackPath: "/shoo/callback"
});

Return value

PropertyTypeDescription
useAuth() => ShooConvexAuthStateHook for ConvexProviderWithAuth
signIn(opts?: StartSignInOptions) => Promise<void>Navigate to Shoo
signOut() => voidClear the stored identity. All consumers update.

useAuth()

PropertyTypeDescription
isLoadingbooleantrue until the client is ready
isAuthenticatedbooleantrue when status === "signed_in"
fetchAccessToken(args) => Promise<string | null>The stored token, or null. Never navigates.
statusShooAuthStatusSame values as useShooAuth
errorstring | nullLast callback error

fetchAccessToken gets a new reference when the signed-in account changes, so Convex sets the new token. It never starts a sign-in.

Re-exports

import { createShooAuth, decodeIdentityClaims } from "@shoojs/react";
import type {
  HandleCallbackOptions,
  SessionCheckResult,
  SessionMonitorOptions,
  ShooAuthClient,
  ShooAuthOptions,
  ShooAuthStatus,
  ShooIdentity,
  StartSignInOptions,
  TokenResponse
} from "@shoojs/react";