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.
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.
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.