Skip to content

Forms State And Data

Chris Michael edited this page Jul 23, 2026 · 3 revisions

Effuse

Forms, State, And Data

Status: Implemented across @effuse/use, @effuse/store, and @effuse/query. Compatibility remains experimental.

Forms

useForm creates reactive field values, errors, validity, submission state, and validation behavior.

const form = useForm({
  initialValues: { email: '' },
  validate: {
    email: [required(), email()],
  },
  onSubmit: async (values) => {
    await register(values);
  },
});

Use labels, native controls, and explicit error output in templates. Server validation should be mapped into the same field/error model rather than stored in unrelated component state.

Stores

@effuse/store provides reactive state, selectors, slices, actions, async actions, cancellation, persistence-related hooks, and Effect-based execution.

const cart = createStore('cart', {
  items: [] as CartItem[],
  add(item: CartItem) {
    this.items.value = [...this.items.value, item];
  },
});

For capability-owned state, expose the store through a layer service or derived props. Components should bind to the layer rather than rely on ambient string lookup as the primary architecture.

Queries

@effuse/query provides query clients, caches, observers, retries, mutations, infinite queries, query options, key factories, and reactive cache metadata.

const users = useQuery({
  queryKey: ['users'],
  queryFn: () => fetch('/api/users').then((r) => r.json()),
});

Server state belongs in query caches; durable application commands and capabilities belong in layers. A query can call a manifest client instead of hard-coding a URL.

Internationalization

@effuse/i18n supports typed translations, locale state, nested keys, interpolation, and pluralization. Install its runtime once and consume translations through the package hooks or an application layer boundary.

For server-side rendering, prefer request-scoped instances over the global singleton so concurrent requests never share locale state. createI18nInstance builds an instance without registering it globally (locale detection and persistence are off by default, since the locale must come from the request), resolveLocale(header, available, fallback) negotiates an Accept-Language header against the locales you ship, and withI18n(instance, () => ...) binds the instance for one render so getI18n, t, and useTranslation resolve against it:

const locale = resolveLocale(request.headers['accept-language'], ['en', 'es'], 'en');
const i18n = createI18nInstance({ defaultLocale: locale, translations });
const html = withI18n(i18n, () => renderApp());

Markdown

@effuse/ink renders reactive Markdown and supports mapped Effuse components. Treat Markdown input as content, and apply an application security policy when content is not trusted.

Related

Clone this wiki locally