# Architecture

You don't need to read this guide to use Formisch. It's here for the curious — for people who want to understand why the bundle stays small, why updates are fine-grained, and how the same library can support Vue, SolidJS, React, Svelte, Preact and Qwik without forking the codebase. Each of those properties falls out of a few deliberate architectural choices.

## Three packages, three responsibilities

Formisch is structured as three packages, each with a clear role:

- [`@formisch/core`](https://github.com/open-circle/formisch/tree/main/packages/core) is the framework-agnostic foundation. It contains the form and field store types, the recursive store builder that mirrors your Valibot schema, and the validation orchestration.
- [`@formisch/methods`](https://github.com/open-circle/formisch/tree/main/packages/methods) exposes the form operations as standalone, tree-shakeable functions: <Link href="/methods/api/setInput/">`setInput`</Link>, <Link href="/methods/api/validate/">`validate`</Link>, <Link href="/methods/api/reset/">`reset`</Link>, <Link href="/methods/api/insert/">`insert`</Link>, <Link href="/methods/api/move/">`move`</Link>, <Link href="/methods/api/focus/">`focus`</Link> and so on. Each method lives in its own file and only imports the core helpers it actually needs.
- [`@formisch/vue`](https://github.com/open-circle/formisch/tree/main/frameworks/vue) is the thin user-facing layer. It provides the <Link href="/vue/api/useForm/">`useForm`</Link>, <Link href="/vue/api/useField/">`useField`</Link> and <Link href="/vue/api/useFieldArray/">`useFieldArray`</Link> composables, the <Link href="/vue/api/Form/">`<Form />`</Link>, <Link href="/vue/api/Field/">`<Field />`</Link> and <Link href="/vue/api/FieldArray/">`<FieldArray />`</Link> components, and re-exports every method from `@formisch/methods` so you only import from a single package.

```
@formisch/vue
  ├─ useForm, <Form />, <Field />, …             (Vue-specific reactive layer)
  └─ re-exports @formisch/methods/vue            (focus, reset, validate, …)
         └─ @formisch/core/vue                   (createFormStore, types, signals)
                  └─ framework adapter           (createSignal, batch, untrack, createId)
```

The modular architecture is what makes Formisch tree-shakeable. The core stays small, and every additional capability — `setInput`, `validate`, `reset`, `insert`, `move`, `focus` and the rest — is exported as its own function. A form that only imports `useForm`, `<Form />` and `<Field />` ships nothing else, even though `@formisch/vue` re-exports every method.

## One core, one adapter per framework

The core package is framework-agnostic, but it still needs _some_ reactivity primitive to build on. Formisch handles this by providing a subpath export for every supported framework (`@formisch/core/vue`, `@formisch/core/solid`, `@formisch/core/react`, …), all built from the same source. The only file that differs between them [is a small adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.ts) that exports four functions: `createSignal`, `batch`, `untrack` and `createId`.

The swap happens at build time, not at runtime. A small [rolldown plugin](https://github.com/open-circle/formisch/blob/main/packages/core/tsdown.config.ts) sits in front of import resolution: whenever the bundler resolves an import of `./framework/index.ts`, the plugin looks for a sibling `./framework/index.vue.ts` (or `.solid.ts`, `.react.ts`, …) and, if it exists, redirects the import there. The build runs once per framework, and each run produces a self-contained bundle with the framework-specific adapter inlined as if it had always been the only option.

Vue's own `shallowRef` already implements the `.value` getter/setter shape Formisch's core relies on, so [the Vue adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.vue.ts) is essentially a direct re-export:

```ts
export { shallowRef as createSignal } from 'vue';
```

For frameworks that don't have a native signal primitive (like React), the adapter implements a small pub/sub system from scratch that exposes the same `Signal<T>` shape. Every other file in `@formisch/core` only ever imports `createSignal`, `batch`, `untrack` and `createId`, never a framework directly. That's why the same [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts) code path can power every framework binding without a single conditional.

This is what makes Formisch different from most framework-agnostic libraries. The usual approach is to ship a runtime abstraction — a custom store, an observer protocol, a subscription layer — that lives in the bundle and has to be bridged into each framework. Formisch ships nothing like that. Where the framework has native signals, Formisch uses them directly; where it doesn't, the adapter is just enough pub/sub to drive re-renders. Either way, there is no extra layer between your form state and the framework's reactivity, and your bundle pays no overhead for portability. Form state lives in real Vue `shallowRef` instances, fully integrated with Vue's reactivity system and dependency tracking.

That Formisch is framework-agnostic is not a trade-off you make — it's a benefit you get. A bug surfaced by a Svelte user is a bug fixed for Vue, Solid, React and everyone else. An improvement to the recursive store builder, the validation orchestration or any individual method lands across every binding at once. There is one library to maintain, not one per framework, and every community pulls the rest forward.

## State lives in signals

Every piece of reactive state in Formisch is a [`Signal<T>`](https://github.com/open-circle/formisch/blob/main/packages/core/src/types/signal/signal.ts) with the same minimal interface:

```ts
interface Signal<T> {
  get value(): T;
  set value(nextValue: T): void;
}
```

Reads track, writes notify. There is no central store object that gets diffed and no equality check on the consumer side. Each field carries its own signals for `errors`, `isTouched`, `isDirty`, plus an `input` signal for value fields and a `children` collection for arrays and objects. When you call `setInput(form, { path: ['email'], input: 'a@b.c' })`, only the `input` signal of that one field is written, so only the components that read that one signal — typically a single `<input />` — re-render.

This is the source of the fine-grained reactivity. The shape of the store is pre-allocated at form creation, matching the shape of your Valibot schema, so methods never have to look up paths in a generic object — they walk a typed tree of stores and update individual signals.

## What happens when you call `useForm`

When you call <Link href="/vue/api/useForm/">`useForm`</Link>, three layers cooperate to produce the form store you receive.

```vue
<script setup lang="ts">
import { useForm } from '@formisch/vue';
import * as v from 'valibot';

const LoginSchema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

const loginForm = useForm({ schema: LoginSchema });
</script>
```

First, the [Vue wrapper](https://github.com/open-circle/formisch/blob/main/frameworks/vue/src/composables/useForm/useForm.ts) hands the configuration to the core function [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts), together with a parse closure that captures your schema:

```ts
const internalFormStore = createFormStore(config, (input) =>
  v.safeParseAsync(config.schema, input)
);
```

Next, `createFormStore` calls [`initializeFieldStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/field/initializeFieldStore/initializeFieldStore.ts), which walks your Valibot schema recursively and builds the internal store hierarchy:

- A `v.object(...)` becomes an `InternalObjectStore` with a `children` record keyed by field name.
- A `v.array(...)` or `v.tuple(...)` becomes an `InternalArrayStore` with a `children` list and its own `initialInput`, `startInput` and `input` signals so it can track structural changes.
- A leaf like `v.string()` or `v.number()` becomes an `InternalValueStore` with an `input` signal that holds the current field value.
- Wrapper schemas such as `v.optional`, `v.nullable`, `v.nonNullable` and `v.lazy` are unwrapped and recursed into.
- `v.union`, `v.variant` and `v.intersect` initialize every option into the same store, which is what lets you address discriminated fields with a single field path.

At every level, the same three signals are created: `errors`, `isTouched` and `isDirty`. Once the field tree is built, three more signals are added to the form root: `isSubmitting`, `isSubmitted` and `isValidating`.

Finally, the Vue wrapper builds the public form store you actually receive. It's a small reactive object that sits on top of the internal store: form-level signals are exposed through getters that read `internalFormStore.isSubmitting.value` and friends, while derived state like `isTouched`, `isDirty` and `isValid` is wrapped in `computed` so it recomputes lazily as the underlying field signals change:

```ts
const isTouched = computed(() =>
  getFieldBool(internalFormStore, 'isTouched')
);
const isDirty = computed(() => getFieldBool(internalFormStore, 'isDirty'));
const isValid = computed(() => !getFieldBool(internalFormStore, 'errors'));

return {
  [INTERNAL]: internalFormStore,
  get isSubmitting() {
    return internalFormStore.isSubmitting.value;
  },
  get isTouched() {
    return isTouched.value;
  },
  // …
};
```

The internal store is tucked away behind the [`INTERNAL`](https://github.com/open-circle/formisch/blob/main/packages/core/src/values.ts) symbol. Methods like `setInput(form, …)` reach in via `form[INTERNAL]` to mutate signals, while your code only sees the public form store. This separation is what keeps the public API small and stable even as the internal shape evolves.

## Why this design pays off

The same four properties keep coming back, and now you can see where they come from:

- **Small bundle size.** Because methods are individual modules re-exported from [`@formisch/vue`](https://github.com/open-circle/formisch/tree/main/frameworks/vue), a form that uses only <Link href="/vue/api/useForm/">`useForm`</Link> and <Link href="/vue/api/Field/">`<Field />`</Link> never pulls in <Link href="/methods/api/validate/">`validate`</Link>, <Link href="/methods/api/reset/">`reset`</Link>, <Link href="/methods/api/insert/">`insert`</Link> or the other operations. Tree-shaking gets to do real work.
- **Fine-grained reactivity.** The store shape is pre-allocated at form creation, with one signal per piece of state. Methods write `.value` directly and Vue's reactivity system handles propagation. Updating one field does not invalidate the rest.
- **Framework portability.** The entire library is parameterized over a four-function adapter. Adding a new framework means writing a small adapter that maps `createSignal`, `batch`, `untrack` and `createId` onto its primitives.
- **Type safety.** Your Valibot schema is the single source of truth. It drives both runtime parsing and the inferred TypeScript types for paths, inputs and outputs, so there is no second declaration to keep in sync.

## Further reading

To see how these design choices stack up against alternatives, head to the <Link href="/vue/guides/comparison/">comparison</Link> guide. For a closer look at how Valibot's inference flows into your editor, see <Link href="/vue/guides/typescript/">TypeScript</Link>. And if you want to read the recursive walker, the adapter implementations, or how individual methods are written, the full source is on [GitHub](https://github.com/open-circle/formisch).
