React

Wave A React adapters for Sometic. Thin wrappers over shared engines (@sometic/dom, @sometic/forms, @sometic/store, @sometic/auth, @sometic/http). Behavior stays framework-independent; React owns rendering and lifecycle.

Overview

When to use

  • You ship a React 18 or 19 app and want native components with shared Sometic behavior.
  • You need controlled / uncontrolled inputs, forms, overlays, store subscriptions, auth session hooks, or HTTP client context.

When not to use

  • No React runtime → use Vanilla / elements or another adapter.
  • You only need a store bind in Preact → prefer @sometic/preact (Experimental) or React if you already use React.
  • Data tables and date picker UI are not in this package yet. See What’s included.

Installation

Peer: react ^18 || ^19.

pnpm
pnpm add @sometic/react
npm
npm install @sometic/react
yarn
yarn add @sometic/react
bun
bun add @sometic/react

Add foundation packages as needed (@sometic/theme, @sometic/store, @sometic/auth, …). Workspace dependencies of @sometic/react install transitively when using a package manager that hoists correctly; pin peers explicitly in apps.

Import map

Prefer subpath imports so unused families stay out of your bundle.

ImportExports
@sometic/react/buttonButton, IconButton, ToggleButton, ButtonGroup, AsyncButton
@sometic/react/fieldField
@sometic/react/inputInput, PasswordInput, OtpInput, NumberInput, FileInput, MaskedInput, CurrencyInput, DateInput
@sometic/react/formForm, FormProvider, useForm, useFormContext, useFormField, useFormState, useFieldArray
@sometic/react/selectionCheckbox, Radio, Select, Switch, Combobox
@sometic/react/overlayAlert, Dialog, Drawer, Menu, MenuItem, ContextMenu, Popover, Tooltip, ToastRegion
@sometic/react/structureTabs, TabTrigger, TabPanel, Accordion, AccordionItem, Breadcrumb, BreadcrumbItem, CommandPalette, Tree, Badge, Progress, Spinner, Skeleton
@sometic/react/storeuseStore
@sometic/react/authAuthProvider, useAuth, useSession, useCan
@sometic/react/httpHttpProvider, useHttp
@sometic/reactRoot barrel (prefer subpaths)

Usage

Button

tsx
import { Button, AsyncButton } from "@sometic/react/button";

export function SaveActions() {
    return (
        <>
            <Button type="button" variant="primary">
                Save
            </Button>
            <AsyncButton
                action={async (signal) => {
                    const response = await fetch("/api/save", { signal });
                    if (!response.ok) {
                        throw new Error("Save failed");
                    }
                    return response.json();
                }}
            >
                Save async
            </AsyncButton>
        </>
    );
}

Input and field

tsx
import { Field } from "@sometic/react/field";
import { Input, PasswordInput } from "@sometic/react/input";

export function LoginFields() {
    return (
        <>
            <Field label="Email" htmlFor="email">
                <Input id="email" name="email" type="email" autoComplete="email" />
            </Field>
            <Field label="Password" htmlFor="password">
                <PasswordInput id="password" name="password" autoComplete="current-password" />
            </Field>
        </>
    );
}

Form

tsx
import { Form, useForm } from "@sometic/react/form";
import { Input } from "@sometic/react/input";
import { Button } from "@sometic/react/button";

type Values = { email: string };

export function SignupForm() {
    const form = useForm<Values>({
        defaultValues: { email: "" },
    });

    return (
        <Form
            form={form}
            onValid={async (values) => {
                await fetch("/api/signup", {
                    method: "POST",
                    body: JSON.stringify(values),
                });
            }}
        >
            <Input name="email" type="email" />
            <Button type="submit">Create account</Button>
        </Form>
    );
}

Overlay

tsx
import { Dialog, Alert } from "@sometic/react/overlay";
import { Button } from "@sometic/react/button";
import { useState } from "react";

export function ConfirmDialog() {
    const [open, setOpen] = useState(false);
    return (
        <>
            <Button type="button" onClick={() => setOpen(true)}>
                Open
            </Button>
            <Dialog open={open} onOpenChange={setOpen} titleId="confirm-title">
                <h2 id="confirm-title">Confirm</h2>
                <Alert>This action cannot be undone from the client alone.</Alert>
                <Button type="button" onClick={() => setOpen(false)}>
                    Close
                </Button>
            </Dialog>
        </>
    );
}

Dialog uses the shared modal overlay controller (focus trap, body scroll lock, Escape dismiss). Outside press does not dismiss in the current beta. Pass titleId / descriptionId or an accessible name. See Beta maturity.

Structure

tsx
import { useState } from "react";
import {
    Tabs,
    TabTrigger,
    TabPanel,
    Accordion,
    AccordionItem,
    Breadcrumb,
    BreadcrumbItem,
    CommandPalette,
    Tree,
} from "@sometic/react/structure";

const commands = [
    { id: "docs", label: "Open docs", keywords: ["guide"], group: "Navigation" },
    { id: "theme", label: "Toggle theme", group: "Theme" },
];
const treeItems = [
    {
        id: "docs",
        label: "Docs",
        children: [{ id: "intro", label: "Introduction" }],
    },
];

export function StructureExample() {
    const [open, setOpen] = useState(false);
    return (
        <>
            <Tabs defaultValue="overview">
                <TabTrigger value="overview">Overview</TabTrigger>
                <TabPanel value="overview">Portable tab selection with ARIA resolve.</TabPanel>
            </Tabs>
            <Accordion type="single" defaultValue="a">
                <AccordionItem value="a" title="Accessibility">
                    Focus, dismiss, and ARIA live in the core engines.
                </AccordionItem>
            </Accordion>
            <Breadcrumb>
                <BreadcrumbItem>
                    <a href="/">Docs</a>
                </BreadcrumbItem>
                <BreadcrumbItem current>Structure</BreadcrumbItem>
            </Breadcrumb>
            <button type="button" onClick={() => setOpen(true)}>
                Open command palette
            </button>
            <CommandPalette
                open={open}
                onOpenChange={setOpen}
                commands={commands}
                onSelect={(command) => console.log(command.id)}
            />
            <Tree items={treeItems} defaultValue="docs" defaultExpanded={["docs"]} />
        </>
    );
}

Import from @sometic/react/structure. Same engines as Vue and Vanilla.

Store

tsx
import { createStore } from "@sometic/store";
import { useStore } from "@sometic/react/store";
import { Button } from "@sometic/react/button";

const counterStore = createStore({ count: 0 });

export function Counter() {
    const count = useStore(counterStore, (state) => state.count);
    return (
        <Button
            type="button"
            onClick={() => counterStore.update((state) => ({ count: state.count + 1 }))}
        >
            {count}
        </Button>
    );
}

useStore integrates with useSyncExternalStore. Pass a selector (and optional equality function) to limit re-renders. Dispose long-lived stores when the app tears down.

Auth

tsx
import { AuthProvider, useAuth, useSession, useCan } from "@sometic/react/auth";
import { createAuth, requirePermission } from "@sometic/auth";
import type { ReactNode } from "react";

const auth = createAuth({/* provider + session options */});

export function AppAuth({ children }: { children: ReactNode }) {
    return <AuthProvider auth={auth}>{children}</AuthProvider>;
}

export function SessionBadge() {
    const controller = useAuth();
    const session = useSession();
    const canEdit = useCan(requirePermission("post:edit"));
    return (
        <span>
            {session.status} · canEdit={String(canEdit)} ·{" "}
            <button type="button" onClick={() => void controller.signOut()}>
                Sign out
            </button>
        </span>
    );
}

AuthProvider accepts either auth (an AuthController) or options (CreateAuthOptions). useAuth() returns the controller from context. useSession / useCan can take an optional controller override.

Wire a concrete provider package (@sometic/auth-local, Firebase, Supabase, OIDC) at the auth core boundary. Client auth is UX orchestration; enforce authorization on the server.

HTTP

tsx
import { HttpProvider, useHttp } from "@sometic/react/http";
import { createHttp } from "@sometic/http";
import type { ReactNode } from "react";

const http = createHttp({
    baseUrl: "/api",
});

export function AppHttp({ children }: { children: ReactNode }) {
    return <HttpProvider client={http}>{children}</HttpProvider>;
}

export function LoadProfile() {
    const client = useHttp();
    return (
        <button
            type="button"
            onClick={() => {
                void client.get("/me");
            }}
        >
            Load
        </button>
    );
}

HttpProvider accepts client or options. useHttp() returns the HttpClient from context.

How it works

  • Components call shared controllers from feature packages; they do not reimplement validation, focus, or auth refresh.
  • Styling stays unstyled by default: pass className, classes, styles, or theme CSS variables.
  • Controllable props follow Sometic conventions: value / defaultValue / onValueChange where applicable.
  • No browser globals at import time. Safe to import in SSR modules; attach DOM-only work in effects or event handlers.

SSR notes

  • Importing @sometic/react/* does not touch window or document.
  • Create auth / HTTP / store instances in module scope only when they are SSR-safe (no localStorage at construction without an injected storage). Prefer per-request or client-only creation when using browser storage.
  • Custom elements are a separate path (@sometic/elements). Do not register CEs during SSR; use React components on the server and hydrate with the same props.
  • Overlays that lock scroll or move focus should open only after mount.

See also SSR guide.

Recipes

Controlled input with store

tsx
const formStore = createStore({ email: "" });

function EmailField() {
    const email = useStore(formStore, (s) => s.email);
    return (
        <Input
            name="email"
            value={email}
            onValueChange={(next) => formStore.set({ email: next })}
        />
    );
}

Tree-shake one family

ts
import { Button } from "@sometic/react/button";
// avoid: import { Button } from "@sometic/react";

Theme + React

Install @sometic/theme, apply CSS variables on a root element, and style components with your design system. See Theming.

Edge cases

  • Multiple React roots sharing one store: fine if you dispose() once when nothing else needs the store.
  • Strict Mode double-mount: controllers used inside effects must clean up with the effect return.
  • exactOptionalPropertyTypes: omit optional props instead of passing undefined unless the prop type allows it.
  • AsyncButton: abort via the provided AbortSignal; do not ignore cancellation.

FAQ

Do I need @sometic/elements in a React app?

No. Use @sometic/react components. Elements are for Vanilla / multi-framework HTML hosts.

Can I use React with Preact compat?

Not a claimed support path. Use @sometic/react with React, or Experimental @sometic/preact for store bind only.

Where is Menu / Combobox / Tabs?

Shipped. Menu / MenuItem / ContextMenu / Drawer from @sometic/react/overlay. Combobox from @sometic/react/selection. Tabs, Accordion, Breadcrumb, CommandPalette, and Tree from @sometic/react/structure.

Why subpath imports?

Keeps button-only apps from pulling form / overlay / auth code. Matches package exports in @sometic/react.

How does useStore relate to Zustand?

Same external-store idea, Sometic-owned core, shared with Vue and Vanilla. See Store.

Are peers bundled?

No. react is a peer. Do not expect the adapter to ship a React runtime.

What about React Server Components?

Client components only for interactive adapters. Keep engines and config in shared modules that avoid browser APIs at import time.

TypeScript version?

Packages target modern TypeScript with strict flags. Use the declarations shipped next to dist.