Skip to content

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.

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>;
  • 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, or RequestingPasswordReset. 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.
  • 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.
  • 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.
  • 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.