alepha@docs:~/docs/guides/frontend$
cat 1-react-integration.md
4 min read
Last commit:

#React Integration

Alepha isn't just a backend framework. It's a full-stack framework that treats your frontend as a first-class citizen of your application graph. No more "backend team vs frontend team" drama. Just code.

#Installation

The @alepha/react package ships separately from the main alepha package. Why? Because not everyone needs React. Some people are still writing jQuery. We don't judge.

Recommended: Use the CLI to scaffold a React-ready project:

npx alepha init --react

This sets up everything:

  • Installs @alepha/react and peer dependencies
  • Creates index.html entry point
  • Configures main.browser.ts for client hydration
  • Sets up Vite for SSR builds

Manual installation:

npm install @alepha/react

Note: You'll need to manually create index.html and configure the browser entry point. But you knew that.


#Part 1: Core Module

The core module (@alepha/react) provides essential React utilities that work anywhere. Next.js, Expo, your weird custom setup - doesn't matter. No router required.

#What's in Core?

tsx
 1import { 2  // Hooks 3  useAlepha,       // Access the Alepha instance 4  useInject,       // Dependency injection in React 5  useClient,       // Type-safe HTTP client 6  useStore,        // Global state management 7  useAction,       // Async action handler with loading/error/cancel 8  useEvents,       // Subscribe to Alepha events 9 10  // Components11  ClientOnly,      // Render only on client (skip SSR)12  ErrorBoundary,   // Catch and display errors gracefully13 14  // Module15  AlephaReact,     // Core module registration16} from "@alepha/react";

#useAlepha - Access the Framework

Need the Alepha instance? Here you go:

tsx
1const MyComponent = () => {2  const alepha = useAlepha();3 4  // Access store, events, services...5  const user = alepha.store.get("current_user");6 7  return <div>Hello, {user?.name}</div>;8};

#useInject - Dependency Injection in React

Your services shouldn't live outside React. Inject them:

tsx
1const Dashboard = () => {2  const analytics = useInject(AnalyticsService);3 4  useEffect(() => {5    analytics.trackPageView("dashboard");6  }, []);7 8  return <div>Dashboard</div>;9};

Same DI container, same services, works in your components.

#useClient - Type-Safe API Calls

Call your backend with full type safety:

tsx
 1import { useClient } from "@alepha/react"; 2import type { UserController } from "../api/UserController"; 3  4const UserProfile = () => { 5  const client = useClient<UserController>(); 6  const [user, setUser] = useState(null); 7  8  useEffect(() => { 9    client.getUser({ params: { id: "123" } }).then(setUser);10  }, []);11 12  return <div>{user?.name}</div>;13};

No more guessing endpoint URLs or request shapes. TypeScript knows.

#useStore - Global State

Read and write global state without Redux boilerplate:

tsx
 1import { useStore } from "@alepha/react"; 2import { themeAtom } from "./atoms/theme"; 3  4const ThemeToggle = () => { 5  const [theme, setTheme] = useStore(themeAtom); 6  7  return ( 8    <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}> 9      Current: {theme}10    </button>11  );12};

See the State Management guide for the full story.

#useAction - Async Operations Done Right

Handle async operations with loading states, error handling, and cancellation:

tsx
 1import { useAction } from "@alepha/react"; 2  3const SaveButton = () => { 4  const [save, { loading, error }] = useAction(async () => { 5    await api.saveDocument(document); 6  }); 7  8  return ( 9    <button onClick={save} disabled={loading}>10      {loading ? "Saving..." : "Save"}11    </button>12  );13};

Features:

  • Single execution: Prevents double-clicks
  • Cancellation: Abort in-flight requests
  • Error capture: Catches and exposes errors
  • Loading state: Know when it's working

#ClientOnly - Skip SSR

Some code should only run in the browser. LocalStorage, window dimensions, that sort of thing:

tsx
 1import { ClientOnly } from "@alepha/react"; 2  3const LiveClock = () => { 4  const [time, setTime] = useState(new Date()); 5  6  useEffect(() => { 7    const interval = setInterval(() => setTime(new Date()), 1000); 8    return () => clearInterval(interval); 9  }, []);10 11  return (12    <ClientOnly>13      <span>{time.toLocaleTimeString()}</span>14    </ClientOnly>15  );16};

No hydration mismatches. No cryptic warnings. Just works.

#ErrorBoundary - Graceful Failures

Catch errors before they crash your app:

tsx
1import { ErrorBoundary } from "@alepha/react";2 3const App = () => (4  <ErrorBoundary fallback={<div>Something went wrong</div>}>5    <Dashboard />6  </ErrorBoundary>7);

#Module Registration

If you're using Alepha's DI system without the router, register the core module:

typescript
1import { Alepha } from "alepha";2import { AlephaReact } from "@alepha/react";3 4const alepha = Alepha.create().with(AlephaReact);

#Part 2: Router Module

The router module (@alepha/react/router) is where the SSR magic happens. It gives you file-system-style routing with the power of TypeScript.

Is it required? No. But if you want SSR, you need it. If you're building a SPA, you still probably want it. It's good.

#What's in Router?

tsx
 1import { 2  // Primitives 3  $page,           // Define routes as page primitives 4  5  // Hooks 6  useRouter,       // Navigation and path generation 7  useRouterState,  // Current route state 8  useActive,       // Active link detection 9  useQueryParams,  // Query string access10 11  // Components12  Link,            // Client-side navigation link13  NotFound,        // 404 handling14  NestedView,      // Nested route rendering15 16  // Module17  AlephaReactRouter, // Router module (auto-loads with $page)18} from "@alepha/react/router";

#The $page Primitive

In frameworks like Next.js, you create files in a pages/ directory. In Alepha, you define pages as class properties. Why? Type-safe linking between your backend and frontend.

tsx
 1import { $page } from "@alepha/react/router"; 2import { t } from "alepha"; 3  4export class AppRouter { 5  home = $page({ 6    path: "/", 7    component: () => <div>Welcome!</div> 8  }); 9 10  dashboard = $page({11    path: "/dashboard",12    schema: {13      query: t.object({14        filter: t.optional(t.text())15      })16    },17    // Server-Side Data Fetching18    loader: async ({ query }) => {19      const stats = await db.stats.get(query.filter);20      return { stats };21    },22    // Props typed automatically from resolve23    component: ({ stats }) => {24      return <div>Stats: {stats.count}</div>25    }26  });27 28  userProfile = $page({29    path: "/users/:id",30    schema: {31      params: t.object({ id: t.text() })32    },33    loader: async ({ params }) => {34      return { user: await db.users.findById(params.id) };35    },36    component: ({ user }) => <UserCard user={user} />37  });38}

#Schema = Type Safety

If your path has :id, declare it in schema.params. Query params go in schema.query. This isn't optional - it's how you get type safety.

tsx
 1postDetail = $page({ 2  path: "/posts/:id/:slug", 3  schema: { 4    params: t.object({ 5      id: t.uuid(), 6      slug: t.text(), 7    }), 8    query: t.object({ 9      tab: t.optional(t.enum(["comments", "related"])),10    }),11  },12  loader: async ({ params, query }) => {13    // params.id is string (validated as UUID)14    // params.slug is string15    // query.tab is "comments" | "related" | undefined16  },17  component: ({ /* ... */ }) => { /* ... */ }18});

Without the schema, params and query are unknown. Nobody wants that.

#The useRouter Hook

Type-safe navigation:

tsx
 1import { useRouter } from "@alepha/react/router"; 2  3const Navigation = () => { 4  const router = useRouter<AppRouter>(); 5  6  return ( 7    <div> 8      {/* Navigate by page name */} 9      <button onClick={() => router.go("home")}>Home</button>10      <button onClick={() => router.go("dashboard")}>Dashboard</button>11 12      {/* With params */}13      <button onClick={() => router.go("userProfile", { params: { id: "123" } })}>14        View User15      </button>16 17      {/* History */}18      <button onClick={() => router.back()}>Back</button>19    </div>20  );21};

#Generating Paths

Use router.path() for URLs:

tsx
 1const UserNav = () => { 2  const router = useRouter<AppRouter>(); 3  4  return ( 5    <nav> 6      <a href={router.path("home")}>Home</a> 7      <a href={router.path("userProfile", { params: { id: "123" } })}> 8        View User 9      </a>10      <a href={router.path("dashboard", { query: { filter: "active" } })}>11        Active Users12      </a>13    </nav>14  );15};

#Anchor Props with router.anchor()

Get both href and onClick for client-side navigation:

tsx
1const NavLink = ({ page, children }) => {2  const router = useRouter<AppRouter>();3 4  return (5    <a {...router.anchor(page)}>6      {children}7    </a>8  );9};

#Active State with useActive

Build navigation that knows where you are:

tsx
 1import { useActive } from "@alepha/react/router"; 2  3const NavLink = ({ href, children }) => { 4  const { isActive, isPending, anchorProps } = useActive(href); 5  6  return ( 7    <a 8      {...anchorProps} 9      className={isActive ? "active" : isPending ? "loading" : ""}10    >11      {children}12    </a>13  );14};15 16// With startWith for nested routes17const SidebarLink = ({ href, children }) => {18  const { isActive, anchorProps } = useActive({ href, startWith: true });19 20  // isActive is true for /users, /users/123, /users/settings...21  return (22    <a {...anchorProps} className={isActive ? "active" : ""}>23      {children}24    </a>25  );26};

#Query Parameters

Access and modify query params:

tsx
 1const Filters = () => { 2  const router = useRouter<AppRouter>(); 3  4  const { sort, filter } = router.query; 5  6  const setSort = (value: string) => { 7    router.setQueryParams({ ...router.query, sort: value }); 8  }; 9 10  return (11    <select value={sort} onChange={(e) => setSort(e.target.value)}>12      <option value="name">Name</option>13      <option value="date">Date</option>14    </select>15  );16};

#SSR - It Just Works

Alepha handles Server-Side Rendering:

  1. Server matches URL to $page
  2. Runs loader function for data
  3. Renders React to HTML
  4. Sends HTML to browser
  5. Hydrates the React app

No Babel config. No Webpack wrestling. alepha dev and alepha build handle it.

#Auto-Loading

When you use $page in your module's primitives, AlephaReactRouter loads automatically:

typescript
1import { $module } from "alepha";2import { $page } from "@alepha/react/router";3 4// No manual .with(AlephaReactRouter) needed5export const MyAppModule = $module({6  name: "my-app",7  primitives: [$page], // Router auto-loads8});

#When to Use What

You want... Use...
Just React utilities (hooks, components) @alepha/react
SSR, routing, pages @alepha/react/router
Works with Next.js/Expo @alepha/react (core only)
Full Alepha experience @alepha/react/router

#Import Cheatsheet

typescript
 1// Core - works everywhere 2import { useAlepha, useClient, useStore, useAction, ClientOnly } from "@alepha/react"; 3  4// Router - full SSR experience 5import { $page, useRouter, useActive, Link } from "@alepha/react/router"; 6  7// Form - type-safe forms 8import { useForm } from "@alepha/react/form"; 9 10// Head - document head management (requires router)11import { $head, useHead } from "@alepha/react/head";12 13// i18n - internationalization14import { $dictionary, useI18n } from "@alepha/react/i18n";15 16// Auth - authentication (requires router)17import { useAuth } from "@alepha/react/auth";

Next up: State Management | Head Management | Forms | i18n

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