threshold1

React

This is for a plain client-side React app — Vite, Create React App, or similar, with no server-rendering pass to think about. If you're on Next.js, see the Next.js guide instead; the SSR timing concerns there don't apply here, but the underlying pattern (construct once, call methods from useEffect/handlers) is the same for a good reason: it's just correct React, not something Next.js-specific.

The pattern

import { useEffect, useState } from "react";
import { Threshold1 } from "@threshold1/auth";

// Module scope — constructed once, when this module first loads, reused
// across every render. Don't construct this inside the component body;
// that would create a new instance (and lose any in-memory session) on
// every single render.
const auth = new Threshold1({
  apiKey: "th_test_...",
  auth: { fallback: "smart" },
});

function LoginForm() {
  const [email, setEmail] = useState("");

  // Runs once after mount — the right place for anything that should
  // happen automatically on load, like picking up a magic-link redirect.
  useEffect(() => {
    auth.resumeSession().then((user) => {
      if (user) {
        // already logged in via a magic-link click
      }
    });
  }, []);

  // Triggered by a real user action — the right place for register()/login().
  async function handleSubmit() {
    const result = await auth.login(email);
    if (result.status === "success") {
      // logged in
    }
  }

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <button type="submit">Log in</button>
    </form>
  );
}

This exact pattern — live-verified end to end in a real Vite+React app against the real hosted API: real sendOtp()/verifyOtp() round trip, real email, real code, real session, zero console warnings or errors.

One real detail worth knowing rather than being surprised by: React's StrictMode (on by default in Vite's React template) double-invokes effects in development, so useEffect's resumeSession() call above genuinely runs twice on every page load in dev. That's expected and harmless here — resumeSession() simply resolves to null both times when there's no magic-link token to pick up — confirmed live, not assumed.

Not a shipped package

There's no @threshold1/react, and there won't be — the SDK is deliberately framework-agnostic (see Core Concepts), and framework guides like this one exist to show the correct usage pattern for the one real package, not to hand out a thin wrapper around it. Everything above is example code you own and can adapt, not an API surface threshold1 maintains.

What's next

  • Next.js — the same core pattern, plus the SSR-specific timing concern that doesn't apply to a plain React app.
  • Quickstart — the full real flow this pattern is built from.