Authentication Flow
A robust authentication flow using a state machine to coordinate login, registration, and password reset. This example demonstrates how to separate UI form state from business logic, and how to handle async effects and error propagation in a predictable way.
Source Code
Section titled “Source Code”import { matchina, defineStates } from "matchina";
const states = defineStates({ LoggedOut: () => ({}),
LoginForm: ({ email = "demo@example.com", password = "password123", error = undefined, }: { email?: string; password?: string; error?: string | null; } = {}) => ({ email, password, error }),
RegisterForm: ({ name = "Demo User", email = "demo@example.com", error = undefined, }: { name?: string; email?: string; error?: string | null; } = {}) => ({ name, email, error }),
PasswordResetForm: ({ email = "demo@example.com", error = undefined, }: { email?: string; error?: string | null; } = {}) => ({ email, error }),
PasswordResetSent: ({ email }: { email: string }) => ({ email }),
LoggingIn: ({ email, password }: { email: string; password: string }) => ({ email, password, }), Registering: ({ name, email }: { name: string; email: string }) => ({ name, email, }), RequestingPasswordReset: ({ email }: { email: string }) => ({ email }),
LoggedIn: ({ user, }: { user: { id: string; name: string; email: string; avatar?: string; }; }) => ({ user }),});
export const createAuthMachine = () => { return matchina( states, { LoggedOut: { showLogin: "LoginForm", showRegister: "RegisterForm", },
LoginForm: { login: "LoggingIn", goToRegister: "RegisterForm", goToPasswordReset: "PasswordResetForm", cancel: "LoggedOut", },
RegisterForm: { register: "Registering", goToLogin: "LoginForm", cancel: "LoggedOut", }, PasswordResetForm: { requestReset: "RequestingPasswordReset", goToLogin: "LoginForm", cancel: "LoggedOut", },
LoggingIn: { success: "LoggedIn", failure: (error: string) => ({ from }) => { return states.LoginForm({ email: from.data.email, password: from.data.password, error, }); }, },
Registering: { success: "LoggedIn", failure: (error: string) => ({ from }) => states.RegisterForm({ name: from.data.name, email: from.data.email, error, }), }, RequestingPasswordReset: { success: "PasswordResetSent", failure: (error: string) => ({ from }) => states.PasswordResetForm({ email: from.data.email, error, }), },
PasswordResetSent: { goToLogin: "LoginForm", },
LoggedIn: { logout: "LoggedOut", }, }, states.LoggedOut() );};
export type AuthMachine = ReturnType<typeof createAuthMachine>;
import { useMachine } from "matchina/react";import { type AuthMachine } from "./machine";import React, { useState } from "react";
function LoginFormView({ data, machine, handleAutoSuccess }: any) { const [email, setEmail] = useState(data.email || ""); const [password, setPassword] = useState(data.password || ""); React.useEffect(() => { setEmail(data.email || ""); setPassword(data.password || ""); }, [data.email, data.password]); return ( <div> <h2 className="text-2xl font-bold mb-4">Log In</h2> <div className="space-y-4"> <div> <label className="block text-sm font-medium mb-1">Email</label> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="w-full px-3 py-2 border border-current/20 rounded bg-transparent focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> <div> <label className="block text-sm font-medium mb-1">Password</label> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className="w-full px-3 py-2 border border-current/20 rounded bg-transparent focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> {/* Show error if exists */} {data.error && ( <div className="text-red-500 mb-4"> <p className="text-sm">{data.error}</p> </div> )} <button onClick={() => handleAutoSuccess(() => machine.login({ email, password })) } className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" > Log In </button> <div className="text-center space-y-2"> <button onClick={() => machine.goToPasswordReset()} className="text-blue-500 hover:underline text-sm" > Forgot Password? </button> <div> <span className="text-sm opacity-70">Don't have an account? </span> <button onClick={() => machine.goToRegister()} className="text-blue-500 hover:underline text-sm" > Register </button> </div> <button onClick={() => machine.cancel()} className="text-current/50 hover:underline text-sm" > Cancel </button> </div> </div> </div> );}
function RegisterFormView({ data, machine, handleAutoSuccess }: any) { const [name, setName] = useState(data.name || ""); const [email, setEmail] = useState(data.email || ""); React.useEffect(() => { setName(data.name || ""); setEmail(data.email || ""); }, [data.name, data.email]); return ( <div> <h2 className="text-2xl font-bold mb-4">Register</h2> <div className="space-y-4"> <div> <label className="block text-sm font-medium mb-1">Name</label> <input type="text" value={name} onChange={(e) => setName(e.target.value)} className="w-full px-3 py-2 border border-current/20 rounded bg-transparent focus:outline-none focus:ring-2 focus:ring-green-500" /> </div> <div> <label className="block text-sm font-medium mb-1">Email</label> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="w-full px-3 py-2 border border-current/20 rounded bg-transparent focus:outline-none focus:ring-2 focus:ring-green-500" /> </div> {/* Show error if exists */} {data.error && ( <div className="text-red-500 mb-4"> <p className="text-sm">{data.error}</p> </div> )} <button onClick={() => handleAutoSuccess(() => machine.register({ name, email })) } className="w-full px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600" > Register </button> <div className="text-center space-y-2"> <div> <span className="text-sm opacity-70"> Already have an account?{" "} </span> <button onClick={() => machine.goToLogin()} className="text-blue-500 hover:underline text-sm" > Log In </button> </div> <button onClick={() => machine.cancel()} className="text-current/50 hover:underline text-sm" > Cancel </button> </div> </div> </div> );}
function PasswordResetFormView({ data, machine, handleAutoSuccess }: any) { const [email, setEmail] = useState(data.email || ""); React.useEffect(() => { setEmail(data.email || ""); }, [data.email]); return ( <div> <h2 className="text-2xl font-bold mb-4">Reset Password</h2> <div className="space-y-4"> <div> <label className="block text-sm font-medium mb-1">Email</label> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="w-full px-3 py-2 border border-current/20 rounded bg-transparent focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> {/* Show error if exists */} {data.error && ( <div className="text-red-500 mb-4"> <p className="text-sm">{data.error}</p> </div> )} <button onClick={() => handleAutoSuccess(() => machine.requestReset({ email })) } className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" > Send Reset Link </button> <div className="text-center space-y-2"> <button onClick={() => machine.goToLogin()} className="text-blue-500 hover:underline text-sm" > Back to Log In </button> <br /> <button onClick={() => machine.cancel()} className="text-current/50 hover:underline text-sm" > Cancel </button> </div> </div> </div> );}
export const AuthFlowView = ({ machine }: { machine: AuthMachine }) => { useMachine(machine); const currentState = machine.getState();
const handleAutoSuccess = async (action: () => void) => { action();
// Auto-success after short delay setTimeout(() => { const state = machine.getState(); if (state.is("LoggingIn")) { const data = state.data as { email: string; password: string }; machine.success({ user: { id: "user-123", name: "Demo User", email: data.email, avatar: "https://i.pravatar.cc/150?u=demo", }, }); } else if (state.is("Registering")) { const data = state.data as { name: string; email: string }; machine.success({ user: { id: "user-123", name: data.name, email: data.email, avatar: "https://i.pravatar.cc/150?u=" + data.email, }, }); } else if (state.is("RequestingPasswordReset")) { const data = state.data as { email: string }; machine.success({ email: data.email }); } }, 1500); };
return ( <div className="max-w-md mx-auto bg-transparent rounded-lg border border-current/20 p-6"> {currentState.match({ LoggedOut: () => ( <div className="text-center"> <h2 className="text-2xl font-bold mb-6">Welcome</h2> <div className="space-y-3"> <button onClick={() => machine.showLogin()} className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" > Log In </button> <button onClick={() => machine.showRegister()} className="w-full px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600" > Register </button> </div> </div> ),
LoginForm: (data) => ( <LoginFormView data={data} machine={machine} handleAutoSuccess={handleAutoSuccess} /> ),
RegisterForm: (data) => ( <RegisterFormView data={data} machine={machine} handleAutoSuccess={handleAutoSuccess} /> ),
PasswordResetForm: (data) => ( <PasswordResetFormView data={data} machine={machine} handleAutoSuccess={handleAutoSuccess} /> ),
LoggingIn: (data) => ( <div className="text-center"> <h2 className="text-2xl font-bold mb-4">Logging In...</h2> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div> <p className="mb-6 opacity-70">Authenticating {data.email}</p>
{/* Manual Controls */} <div className="space-y-2"> <p className="text-sm opacity-50 mb-3">Manual Controls:</p> <div className="flex space-x-2"> <button onClick={() => machine.success({ user: { id: "user-123", name: "Demo User", email: data.email, avatar: "https://i.pravatar.cc/150?u=demo", }, }) } className="flex-1 px-3 py-2 bg-green-500 text-white rounded hover:bg-green-600 text-sm" > Login Success </button> <button onClick={() => machine.failure("Invalid credentials")} className="flex-1 px-3 py-2 bg-red-500 text-white rounded hover:bg-red-600 text-sm" > Login Failed </button> </div> </div> </div> ),
Registering: (data) => ( <div className="text-center"> <h2 className="text-2xl font-bold mb-4">Creating Account...</h2> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-500 mx-auto mb-4"></div> <p className="mb-6 opacity-70">Registering {data.email}</p>
{/* Manual Controls */} <div className="space-y-2"> <p className="text-sm opacity-50 mb-3">Manual Controls:</p> <div className="flex space-x-2"> <button onClick={() => machine.success({ user: { id: "user-123", name: data.name, email: data.email, avatar: "https://i.pravatar.cc/150?u=" + data.email, }, }) } className="flex-1 px-3 py-2 bg-green-500 text-white rounded hover:bg-green-600 text-sm" > Register Success </button> <button onClick={() => machine.failure("Email already taken")} className="flex-1 px-3 py-2 bg-red-500 text-white rounded hover:bg-red-600 text-sm" > Register Failed </button> </div> </div> </div> ),
RequestingPasswordReset: (data) => ( <div className="text-center"> <h2 className="text-2xl font-bold mb-4">Sending Reset Link...</h2> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div> <p className="mb-6 opacity-70">Sending to {data.email}</p>
{/* Manual Controls */} <div className="space-y-2"> <p className="text-sm opacity-50 mb-3">Manual Controls:</p> <div className="flex space-x-2"> <button onClick={() => machine.success({ email: data.email })} className="flex-1 px-3 py-2 bg-green-500 text-white rounded hover:bg-green-600 text-sm" > Reset Success </button> <button onClick={() => machine.failure("Email not found")} className="flex-1 px-3 py-2 bg-red-500 text-white rounded hover:bg-red-600 text-sm" > Reset Failed </button> </div> </div> </div> ),
PasswordResetSent: (data) => ( <div className="text-center"> <h2 className="text-2xl font-bold mb-4 text-blue-600"> Reset Link Sent! </h2> <p className="mb-6 opacity-70"> We've sent a password reset link to {data.email}. </p> <button onClick={() => machine.goToLogin()} className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" > Back to Log In </button> </div> ),
LoggedIn: (data) => ( <div className="text-center"> <div className="mb-4"> {data.user.avatar && ( <img src={data.user.avatar} alt={data.user.name} className="w-16 h-16 rounded-full mx-auto mb-4" /> )} <h2 className="text-2xl font-bold text-green-600"> Welcome, {data.user.name}! </h2> <p className="opacity-70">{data.user.email}</p> </div> <div className="bg-green-500/10 border border-green-500/20 rounded p-4 mb-4"> <p className="text-green-600">You are successfully logged in!</p> </div> <button onClick={() => machine.logout()} className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600" > Log Out </button> </div> ), })} </div> );};
import { useState } from "react";import { AuthFlowView } from "./AuthFlowView";import { createAuthMachine } from "./machine";
export function AuthFlowDemo() { const [machine] = useState(createAuthMachine); return <AuthFlowView machine={machine} />;}
Key Patterns
Section titled “Key Patterns”- State machine for flow, not form state: The machine tracks which step the user is in (login, register, reset, etc.), but the view manages the actual form fields. This keeps the machine focused on transitions and error handling, not UI details.
- Async effects in dedicated states: Async logic (API calls) is triggered by entering states like
LoggingIn
,Registering
, orRequestingPasswordReset
. The machine handles success/failure and transitions accordingly. - Error propagation: Errors from async operations are passed back to the relevant form state, so the view can display them.
- Minimal transitions: Only transitions that represent real user intent or async results are modeled. There are no events for every keystroke or field update.
- Composable, testable logic: Each state and transition is explicit and can be tested in isolation.
How to Use This Pattern
Section titled “How to Use This Pattern”- Put UI state in the view: Use React state/hooks for form fields. Only call the machine when the user submits a form or triggers a navigation.
- Let the machine handle flow and validation: The machine decides what state comes next, and how to handle errors or async results.
- Trigger side effects on state entry: Use lifecycle hooks or effects to run async logic when entering a state, not in response to UI events.
- Propagate errors through state: Pass error messages as part of the state data, so the view can render them.
Why This Works
Section titled “Why This Works”- Predictable transitions: The machine enforces valid flows—users can’t skip steps or get into impossible states.
- Separation of concerns: UI code is simple and focused on rendering; the machine handles business logic and error cases.
- Easy to extend: Add new states (e.g., email verification) or transitions without rewriting the whole flow.
When to Use This Pattern
Section titled “When to Use This Pattern”- Multi-step forms where the flow depends on user actions or async results
- Flows with error handling, retries, or conditional navigation
- Anywhere you want to keep UI logic and business logic clearly separated
For more on effect management and advanced patterns, see the State Effects Guide.