> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Features

> Build a Single Page App as vertical slices — self-contained features that own their full front-end stack.

A **feature** is a vertical slice for one domain area of your Single Page App. It lives at `modules/<spa>/src/features/<feature>/` and owns its full front-end stack — components, hooks, services, state, styles, translations, and types. Each feature is self-contained: it never imports another feature's internals. When two features need the same code, that code is promoted to `src/shared/` and imported from there. This keeps every domain area independent and easy to reason about, and it lets you grow the app by adding slices rather than tangling existing ones.

See [SPA overview](/spa/overview) for the bigger picture and [SPA structure](/spa/structure) for how features sit inside a module.

## Why vertical slices

* **One domain per feature.** Each folder maps to a single area (settings, billing, profile) and holds everything that area needs — no shared "components" dumping ground spanning concerns.
* **No cross-feature coupling.** A feature never reaches into another feature's files; shared code lives in `src/shared/`, so dependencies are explicit and one-directional.
* **Clear layering.** Backend calls live only in `hooks/`; `services/` stay pure domain logic; `store/` holds client state. Each layer has one job.
* **Move-or-delete simplicity.** Because a slice is self-contained, you can rename, relocate, or remove a whole domain area by touching one folder.

## How it works

Scaffold a feature with `spa:feature:create` and you get a route file plus a feature folder. The route file wires TanStack Router to the feature's layouts; the feature folder is organized by layer, each with a single responsibility.

| Subfolder       | Purpose                                                                                          |
| --------------- | ------------------------------------------------------------------------------------------------ |
| `assets/`       | Images, fonts, and other static files used by the feature.                                       |
| `components/`   | Presentational React components for this domain area.                                            |
| `hooks/`        | React hooks — **the only layer that talks to the backend** (TanStack Query reads and mutations). |
| `layouts/`      | Page and boundary components (page, skeleton, error, not-found).                                 |
| `services/`     | **Pure domain logic** — no backend calls, no React; just functions over data.                    |
| `store/`        | **Client state** for the feature (UI/local state).                                               |
| `styles/`       | Feature-scoped styles.                                                                           |
| `translations/` | Feature-scoped i18n messages (see [internationalization](/spa/internationalization)).            |
| `types/`        | TypeScript types for the feature's data and props.                                               |
| `utils/`        | Small feature-local helpers.                                                                     |

The layering rule is what keeps slices clean: components and layouts render, `hooks/` fetch and mutate against the backend, `services/` transform data with no side effects, and `store/` holds client state. Nothing outside `hooks/` should call the backend, and nothing should reach across into a sibling feature.

## What `spa:feature:create` generates

`ooneex spa:feature:create --name=<Name> --module=<spa>` scaffolds a complete slice. PascalCase drives component names and kebab-case drives paths, so a feature named `Settings` produces files under `settings/` with `Settings*` components. It generates:

* A **route file** at `src/routes/<kebab>.tsx` — a TanStack Router file route at path `/<kebab>`.
* Four **layouts/boundaries** in `src/features/<kebab>/layouts/`: `<Name>Layout.tsx` (the page component), `<Name>SkeletonLayout.tsx` (pending/loading boundary), `<Name>ErrorLayout.tsx` (error boundary), and `<Name>NotFoundLayout.tsx` (not-found boundary).
* Two **example hooks** in `src/features/<kebab>/hooks/`: `useGet<Name>.ts` (a TanStack Query READ hook) and `useUpdate<Name>.ts` (a MUTATION hook).

It also installs `@tanstack/react-query` if it is missing.

The generated route wires the layouts as the route's component and its boundaries — one place that maps loading, error, and not-found states to the feature's own components:

```tsx theme={null}
import { createFileRoute } from "@tanstack/react-router";
import { SettingsErrorLayout } from "../features/settings/layouts/SettingsErrorLayout";
import { SettingsLayout } from "../features/settings/layouts/SettingsLayout";
import { SettingsNotFoundLayout } from "../features/settings/layouts/SettingsNotFoundLayout";
import { SettingsSkeletonLayout } from "../features/settings/layouts/SettingsSkeletonLayout";

export const Route = createFileRoute("/settings")({
  component: SettingsLayout,
  pendingComponent: SettingsSkeletonLayout,
  errorComponent: SettingsErrorLayout,
  notFoundComponent: SettingsNotFoundLayout,
});
```

The page layout renders the feature's content. It accepts optional children and falls back to the router `Outlet` so nested routes render in place:

```tsx theme={null}
import { Outlet } from "@tanstack/react-router";
import type { ReactNode } from "react";

type SettingsLayoutPropsType = { children?: ReactNode };

export const SettingsLayout = ({ children }: SettingsLayoutPropsType): ReactNode => {
  return <section>{children ?? <Outlet />}</section>;
};
```

The two generated hooks are example starting points — a read hook and a mutation hook — that you replace with your real queries. Data-fetching hooks are covered in depth on [data fetching](/spa/data-fetching), and the routing model behind the generated route file is covered on [routing](/spa/routing).

## Best practices

* **One domain per feature.** Keep each slice scoped to a single area; if a folder starts serving two concerns, split it.
* **Never import another feature's internals.** Cross-feature imports re-couple the slices you worked to separate — treat each feature as a closed box.
* **Promote shared code.** When two features need the same component, hook, or type, move it to `src/shared/` and import it from there.
* **Keep routes thin.** The route file should only wire layouts and boundaries; put real logic in the feature's layers, not in the route.
* **Backend calls only in hooks.** Read and mutate through `hooks/`; keep `services/` pure and `store/` for client state, so the data path stays predictable.

## CLI command

Scaffold a feature with the generator. It writes the route file, the four layouts, and the two example hooks into the target spa module, and installs `@tanstack/react-query` if it is missing.

```bash theme={null}
# Interactive: prompts for the feature name and target module
ooneex spa:feature:create

# Provide the name and module
ooneex spa:feature:create --name=Settings --module=dashboard

# Overwrite existing feature files
ooneex spa:feature:create --name=Settings --module=dashboard --override
```

| Option       | Description                                                                           | Default             |
| ------------ | ------------------------------------------------------------------------------------- | ------------------- |
| `--name`     | Feature name. Normalized to PascalCase; a trailing `Feature` or `Layout` is stripped. | Prompted if omitted |
| `--module`   | Target spa module the feature is generated into.                                      | Prompted if omitted |
| `--override` | Overwrite existing feature files without prompting.                                   | `false`             |

See [spa:feature:create](/cli/commands/spa-feature-create) for the full command reference.

## Use with Claude and Codex

The generator ships a matching `spa:feature:create` skill. It runs the scaffold and then guides your AI agent through filling in the slice — implementing the layouts, replacing the example hooks with real queries, and adding components, services, and types under the feature folder. Initialize the skills once for your agent:

<Tabs>
  <Tab title="Claude">
    ```bash theme={null}
    ooneex claude:init
    ```

    Then ask Claude in natural language — it maps the request to the generator, runs it, and fills in the slice:

    ```text Prompt icon="terminal" wrap theme={null}
    Create a settings feature in the dashboard spa.
    ```
  </Tab>

  <Tab title="Codex">
    ```bash theme={null}
    ooneex codex:init
    ```

    Then ask Codex in natural language — it maps the request to the generator, runs it, and fills in the slice:

    ```text Prompt icon="terminal" wrap theme={null}
    Create a settings feature in the dashboard spa.
    ```
  </Tab>
</Tabs>

For example, the prompt above maps to `spa:feature:create --name=Settings --module=dashboard`, then implements the layouts and turns the example hooks into the real reads and mutations the settings page needs.
