Authentication Flow
A robust authentication flow using a state machine to coordinate login, registration, and password reset. Demonstrates how to separate UI form state from business logic, and how to handle async effects and error propagation in a predictable way.
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.
Async effects in dedicated states — async logic (API calls) is triggered by entering states like LoggingIn, Registering, or RequestingPasswordReset. The machine handles success/failure and transitions accordingly.
Error propagation — errors from async operations are passed back to the relevant form state as state data, so the view can display them without any extra wiring.
import { matchina, defineStates, createStoreMachine, setup, effect,} from "matchina";
interface User { id: string; name: string; email: string; avatar?: string;}
interface AuthFormState { email: string; password: string; name: string; error: string | null; user: User | null;}
const states = defineStates({ LoggedOut: undefined, LoginForm: undefined, RegisterForm: undefined, PasswordResetForm: undefined, PasswordResetSent: undefined, LoggingIn: undefined, Registering: undefined, RequestingPasswordReset: undefined, LoggedIn: undefined,});
export const createAuthMachine = () => { const initialState: AuthFormState = { email: "demo@example.com", password: "password123", name: "Demo User", error: null, user: null, };
const store = createStoreMachine<AuthFormState>(initialState, { setEmail: (email: string) => (change) => ({ ...change.from, email }), setPassword: (password: string) => (change) => ({ ...change.from, password, }), setName: (name: string) => (change) => ({ ...change.from, name }), setError: (error: string | null) => (change) => ({ ...change.from, error }), setUser: (user: User) => (change) => ({ ...change.from, user, error: null, }), clearError: () => (change) => ({ ...change.from, error: null }), reset: () => () => initialState, });
const machine = 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: "LoginForm", }, Registering: { success: "LoggedIn", failure: "RegisterForm", }, RequestingPasswordReset: { success: "PasswordResetSent", failure: "PasswordResetForm", }, PasswordResetSent: { goToLogin: "LoginForm", }, LoggedIn: { logout: "LoggedOut", }, }, "LoggedOut" );
setup(machine)( effect((ev) => { if (ev.type === "failure" && ev.params[0]) { store.dispatch("setError", ev.params[0] as string); } if (ev.type === "success" && ev.from.is("LoggingIn")) { store.dispatch("setUser", ev.params[0] as User); } if (ev.type === "success" && ev.from.is("Registering")) { store.dispatch("setUser", ev.params[0] as User); } if (ev.type === "success" && ev.from.is("RequestingPasswordReset")) { // Password reset success doesn't set user, just confirms email sent } if (ev.type === "showLogin" || ev.type === "showRegister") { store.dispatch("clearError"); } if (ev.type === "logout") { store.dispatch("reset"); } }) );
// Add ergonomic methods that handle store updates const enhancedMachine = Object.assign(machine, { store,
success: (data: { user?: User; email?: string }) => { if (data.user) { store.dispatch("setUser", data.user); } machine.send("success"); },
failure: (error: string) => { store.dispatch("setError", error); machine.send("failure"); }, });
return enhancedMachine;};
export type AuthMachine = ReturnType<typeof createAuthMachine>;How to use this pattern
Section titled “How to use this pattern”- Put UI state in the view: use React state for form fields. Only call the machine when the user submits or triggers navigation.
- Let the machine handle flow and validation: the machine decides what state comes next and how to handle errors.
- Trigger side effects on state entry, not in response to UI events.
- Pass error messages through state data so the view can render them.
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 UI logic and business logic clearly separated.
For more on effect management, see the State Effects Guide.