alepha@docs:~/docs/guides/frontend$
cat 2-state-management.md
3 min read
Last commit:

#State Management

You know the drill. You fetch data on the server, pass it through props, and pray it survives hydration. Or you set up Redux with 47 files just to share a user object.

Alepha has a simpler approach: atoms for defining state, useStore for consuming it in React, and automatic SSR hydration. No providers, no reducers, no boilerplate.

Note: useStore is part of @alepha/react (core) and works with or without the router.

#Defining State with $atom

An atom is a piece of state with a name and a schema. The schema gives you type safety and validation.

typescript
 1// atoms/currentProjectAtom.ts 2import { $atom, t } from "alepha"; 3  4export const currentProjectAtom = $atom({ 5  name: "current_project", 6  schema: t.optional(t.object({ 7    id: t.integer(), 8    title: t.text(), 9    description: t.optional(t.text()),10  })),11});

That's your entire state definition. No store setup. No provider wrapping your app.

#Reusing Entity Schemas

If you have database entities, you can reuse their schemas directly:

typescript
1import { $atom, t } from "alepha";2import { projects } from "../api/entities/projects.ts";3 4// The atom schema matches your database entity5export const currentProjectAtom = $atom({6  name: "current_project",7  schema: t.optional(projects.schema),8});

One schema definition, used everywhere: database, API validation, and frontend state.

#Arrays of Items

For lists, wrap the schema in t.array():

typescript
1import { $atom, t } from "alepha";2import { projects } from "../api/entities/projects.ts";3 4export const userProjectsAtom = $atom({5  name: "user_projects",6  schema: t.optional(t.array(projects.schema)),7});

#Reading State with useStore

In React components, use the useStore hook. It returns a tuple like useState: the current value and a setter function.

tsx
 1import { useStore } from "@alepha/react"; 2import { userProjectsAtom } from "../atoms/userProjectsAtom.ts"; 3  4const ProjectList = () => { 5  const [projects = []] = useStore(userProjectsAtom); 6  7  return ( 8    <ul> 9      {projects.map(project => (10        <li key={project.id}>{project.title}</li>11      ))}12    </ul>13  );14};

The component re-renders automatically when the atom value changes. No subscriptions to manage.

#Default Values

If the atom might be undefined, provide a fallback:

tsx
1const [projects = []] = useStore(userProjectsAtom);2// projects is never undefined, defaults to []

#Updating State from Components

The second element of the tuple is a setter:

tsx
1const ThemeToggle = () => {2  const [theme, setTheme] = useStore(themeAtom);3 4  return (5    <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>6      Current: {theme}7    </button>8  );9};

#Setting State from Page Loading

The real power comes when you combine atoms with $page loading. Load data on the server, put it in an atom, and it's instantly available to all components.

typescript
 1import { $page } from "@alepha/react/router"; 2import { $inject, Alepha } from "alepha"; 3import { $client } from "alepha/server/links"; 4import { currentProjectAtom } from "./atoms/currentProjectAtom.ts"; 5import type { ProjectController } from "./api/ProjectController.ts"; 6  7class AppRouter { 8  alepha = $inject(Alepha); 9  projectApi = $client<ProjectController>();10 11  project = $page({12    path: "/p/:projectId",13    lazy: () => import("./components/ProjectView.tsx"),14    loader: async ({ params }) => {15      // Fetch on server (or client on navigation)16      const project = await this.projectApi.getProjectById({17        params: { id: params.projectId },18      });19 20      // Put it in the atom21      this.alepha.store.set(currentProjectAtom, project);22 23      // Also return for props if needed24      return { project };25    },26    onLeave: () => {27      // Clean up when leaving the page28      this.alepha.store.set(currentProjectAtom, undefined);29    },30  });31}

Now any component under this route can access the project without prop drilling:

tsx
1// Deep in the component tree - no props needed2const ProjectHeader = () => {3  const [project] = useStore(currentProjectAtom);4 5  return <h1>{project?.title}</h1>;6};

#Updating State After Actions

When the user creates, updates, or deletes something, update the relevant atoms:

tsx
 1import { useAlepha, useClient, useStore } from "@alepha/react"; 2import { useRouter } from "@alepha/react/router"; 3import { useForm } from "@alepha/react/form"; 4import { userProjectsAtom } from "../atoms/userProjectsAtom.ts"; 5import type { ProjectController } from "../api/ProjectController.ts"; 6  7const ProjectCreate = () => { 8  const alepha = useAlepha(); 9  const client = useClient<ProjectController>();10  const router = useRouter();11 12  const form = useForm({13    schema: t.object({14      title: t.text(),15      description: t.optional(t.text()),16    }),17    handler: async (body) => {18      // Create the project19      const project = await client.createProject({ body });20 21      // Update the atom with the new project22      alepha.store.set(userProjectsAtom, [23        ...(alepha.store.get(userProjectsAtom) || []),24        project,25      ]);26 27      // Navigate to the new project28      await router.go("project", {29        params: { projectId: String(project.id) },30      });31    },32  });33 34  return (35    <form onSubmit={form.onSubmit}>36      {/* form fields */}37    </form>38  );39};

The project list component will re-render automatically with the new project included.

#Accessing State Outside React

Sometimes you need to read or write atoms outside of React components—in services, event handlers, or page resolution.

#Using alepha.store

typescript
1// Read2const project = alepha.store.get(currentProjectAtom);3 4// Write5alepha.store.set(currentProjectAtom, newProject);6 7// Read raw key (without atom)8const user = alepha.store.get("alepha.server.request.user");

#In Services with $use

For class-based services, use the $use primitive for reactive access:

typescript
1import { $use } from "alepha";2 3class ThemeService {4  prefs = $use(userPreferencesAtom);5 6  isDarkMode() {7    return this.prefs.theme === "dark";8  }9}

The prefs property updates automatically when the atom changes.

#SSR Hydration

Here's the magic: atoms serialize on the server and hydrate on the client automatically.

bash
1. Server: resolve() fetches data, sets atom
2. Server: React renders with atom value
3. Server: Atom state serialized into HTML
4. Client: Page loads, atom hydrates from HTML
5. Client: React renders with same value - no flash!

No hydration mismatches. No useEffect hacks. No loading spinners for data you already have.

#Organizing Atoms

Keep atoms in a dedicated folder, one file per atom:

bash
src/
├── atoms/
│   ├── currentProjectAtom.ts
│   ├── currentTaskAtom.ts
│   ├── userProjectsAtom.ts
│   └── userPreferencesAtom.ts
├── components/
└── api/

Each atom file is simple:

typescript
1// atoms/currentTaskAtom.ts2import { $atom, t } from "alepha";3import { tasks } from "../api/entities/tasks.ts";4 5export const currentTaskAtom = $atom({6  name: "current_task",7  schema: t.optional(tasks.schema),8});

#When to Use What

Scenario Solution
Data shared across many components $atom + useStore
Current page/route data $atom set in loader()
User preferences, theme $atom with default value
Authentication state $atom (Alepha handles this internally)
Form input useForm hook
Local UI state (modal open, tab index) useState
Derived/computed values Just compute in render or use a getter

#Quick Reference

typescript
 1// Define an atom 2const myAtom = $atom({ 3  name: "unique_name", 4  schema: t.object({ ... }), 5  default: { ... }, // optional 6}); 7  8// React component - read & write 9const [value, setValue] = useStore(myAtom);10 11// React component - read only with default12const [value = defaultValue] = useStore(myAtom);13 14// Outside React - read15const value = alepha.store.get(myAtom);16 17// Outside React - write18alepha.store.set(myAtom, newValue);19 20// In services - reactive access21class MyService {22  data = $use(myAtom);23}

State management without the ceremony. Define your shape, use it everywhere, let Alepha handle the plumbing.

On This Page
No headings found...
ready
mainTypeScript
UTF-8guides_frontend_state_management.md