npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dccs/teff-rfc

v1.1.0-alpha.1

Published

Reactive form engine for deep, controlled enterprise forms. A plain-JavaScript signal graph owns the state; React only paints. Deep type-safety, auto-wired cross-field validation, fine-grained performance.

Readme

@dccs/teff-rfc

A reactive form engine for deep, controlled enterprise forms. A plain-JavaScript signal graph owns the state; React only paints. The result: deep type-safety that never explodes, cross-field validation that wires itself, and fine-grained performance at hundreds of fields — behind a clean object-access API with no render props and no path strings.

Status: alpha. Published for early feedback. The core engine (state graph, validation, arrays, React bindings) is stable and fully tested, but a built-in submit lifecycle (handleSubmit, isSubmitting, submitCount) does not exist yet and the API may still change before the first stable release. Install with npm install @dccs/teff-rfc@alpha.

f.company.departments.at(0)!.field.lines.at(3)!.field.taxRate.set(19); // fully typed, one level per access

Why

React's state model is pull-based and top-down. Deep enterprise forms want the opposite: a keystroke on one field, a rule firing on the other side of the tree, hundreds of fields on screen, and full type-safety on paths like company.departments[0].budget.lines[3].taxRate.

teff-rfc keeps the truth outside React in a small reactive graph, and lets React subscribe to exactly the leaves it renders.

| Goal | How teff-rfc delivers it | | --- | --- | | Deep type-safety | Field<T> resolves one level per property access — never a flat union of all paths, so it cannot hit "Type instantiation is excessively deep" at any depth. | | Cross-field validation | Rules are plain functions. Reading another field inside a rule wires the dependency automatically — no dependsOn, and conditional edges are correct by construction. | | Performance | Every value is a signal. A keystroke notifies only that field's subscribers; derived values recompute only when a real dependency changes. | | Controlled state | Values live in the graph, never in uncontrolled DOM refs. | | Clean API | Object access (f.a.b.set(x)), one hook per input (useField), stable-id arrays. |


Install

pnpm add @dccs/teff-rfc
# peer dependency:  react >= 18
# runtime dependency: @dccs/teff-log (zero-dep; carries the diagnostics)

Quick start

import { createForm, useForm, useField, required, email, type Leaf } from '@dccs/teff-rfc';

interface Signup {
  name: string;
  email: string;
}

function build() {
  return createForm<Signup>({ name: '', email: '' }, (f) => {
    f.name.rule(required('Name is required'));
    f.email.rule(required(), email());
  });
}

function TextInput({ field }: { field: Leaf<string> }) {
  const { value, setValue, error, touch } = useField(field);
  return (
    <label>
      <input value={value} onChange={(e) => setValue(e.target.value)} onBlur={touch} />
      {error && <span className="error">{error}</span>}
    </label>
  );
}

function SignupForm() {
  const form = useForm(build);
  return (
    <form onSubmit={() => console.log(form.validate(), form.values())}>
      <TextInput field={form.field.name} />
      <TextInput field={form.field.email} />
    </form>
  );
}

The one idea

Fields, validation results, and derived values are all the same primitive: a cell in a reactive graph.

  • A leaf field is a writable signal.
  • A validation rule or a derived value is a derivation — a pure function of other cells.
  • While a derivation runs, every cell it reads becomes a dependency edge, discovered automatically. Edges are rebuilt every run, so conditional dependencies are always correct.
  • Change a cell → only its transitive dependents recompute, and only their leaf components repaint. Nothing in between re-renders.
┌───────────────────────────────────────────────┐
│ React components (paint only)                  │
│   useField / useArray / useWatch               │  ← one subscription each
└───────────────┬───────────────────────────────┘
                │  useSyncExternalStore
┌───────────────▼───────────────────────────────┐
│ teff-rfc engine — PLAIN JAVASCRIPT             │
│   Signal (writable)   Derivation (auto-tracked)│
│   + the typed proxy mirror                     │  ← all state & logic live here
└────────────────────────────────────────────────┘

Cross-field rules wire themselves

No dependsOn. Reading a field inside a rule is the dependency.

createForm<Company>(defaults, (f) => {
  // taxRate now depends on fiscalCountry — discovered from the read.
  f.fiscal.taxRate.rule((v) =>
    f.fiscal.fiscalCountry.get() === 'AT' ? (v ?? 0) <= 20 || 'Max 20% in AT' : true,
  );

  // The deepest dependency of all: a top-level cap vs the sum of the whole tree.
  f.grandTotal.derive(() => sum(f.departments.items().map((d) => d.field.total.get())));
  f.fiscal.maxBudget.rule((v) => v == null || f.grandTotal.get() <= v || 'Exceeds cap');
});

Because edges are rebuilt on every run, when the AT branch isn't taken the edge to fiscalCountry disappears — the rule can't be triggered by a field it no longer reads.


Arrays with stable identity

Items are stored by a stable id with a separate order list, so reorder / insert / remove never tears the subscriptions or dependency edges of the surviving items.

f.departments.each((dept) => {
  dept.projects.each((project) => {
    project.total.derive(() => sum(project.lines.items().map((l) => l.field.total.get())));
  });
});

const id = f.departments.append(emptyDepartment()); // grows infinitely
f.departments.move(0, 2);
f.departments.remove(id);

Validation

Rules are plain functions returning true (valid) or a message. The toolkit provides the common ones; compose them by passing several to rule.

import { required, minLength, email, matches, when } from '@dccs/teff-rfc';

f.username.rule(required(), minLength(3));
f.email.rule(required(), email());
f.confirm.rule(matches(() => f.password.get(), 'Passwords do not match'));
f.vat.rule(when(() => isEU(f.country.get()), required('VAT required in the EU')));

Async validation (e.g. server-side uniqueness) runs on change and is merged after the synchronous rules pass:

f.username.asyncRule(async (v) => ((await isTaken(v)) ? 'Already taken' : true), { debounce: 300 });
await form.validateAsync();

Auto-tracking cannot follow reads across await, so read any cross-field dependencies synchronously before the first await inside an async rule.


Localized messages (i18n)

Rules never bake in a language. A validator can emit a deferred message — a catalog key plus optional params — that is resolved to text at render time, so switching language re-localizes every error with no form rebuild. The engine stays i18n-agnostic; it stores the key, never the string.

1. Register your catalog once (type-only, via declaration merging). A hook can't infer what you pass to a provider at runtime, so this is how teff-rfc learns your keys:

import type { AppMessages } from './i18n'; // e.g. export type AppMessages = typeof en;

declare module '@dccs/teff-rfc' {
  interface Register {
    messages: AppMessages;
  }
}

Now MessageKey is keyof AppMessages everywhere — msg('…') and custom codes are key-checked against your catalog. (Params stay an untyped Record<string, string>, matching a useMsgsWithParam-style API.) Skip registration and keys fall back to string.

Why a declare module? A hook can't see the runtime value you pass to a provider, so the catalog is supplied at the type level via TypeScript declaration merging — the same Register pattern TanStack Router and i18next use. Notes:

  • It is type-only and zero-runtime — it compiles away entirely.
  • Put it in any one .ts/.d.ts file that is part of your compilation (e.g. next to your i18n catalog, or a global teff-rfc.d.ts). It is global; you write it once per app, never per form.
  • The module specifier must match the package name exactly: '@dccs/teff-rfc'.
  • AppMessages is just keyof-able — export type AppMessages = typeof en over your generated JSON catalog is the intended source.

2. Emit keys from rules:

import { required, min, custom, msg } from '@dccs/teff-rfc';

f.name.rule(required(msg('formValidationRequiredField')));
f.qty.rule(min(1, msg('formValidationMin', { min: '1' })));
f.name.rule(custom((v) => (v ? true : msg('formValidationNameTaken'))));
f.name.rule(required()); // no key → English default (escape hatch: plain strings pass through)

3. Wrap the app once and feed your resolver — reusing the same provider that tracks dirty state:

import { TeffFormProvider } from '@dccs/teff-rfc';

function Root() {
  const resolve = useMsgsWithParam(); // (key, params?) => string; changes on locale switch
  return (
    <TeffFormProvider resolveMessage={resolve} guardUnload>
      <App />
    </TeffFormProvider>
  );
}

useField(field).error and useFieldError(field) now return localized strings and update live when the language changes. Imperative form.validate() returns { path, key, params } so non-React code can resolve however it likes.

Unsaved-changes guard

The same provider tracks the dirty state of every form created with useForm (they register automatically). Use it for save bars and navigation guards:

const dirty = useAnyFormDirty();     // true when any registered form is dirty
const resetAll = useResetAllForms(); // reset every dirty form

// <TeffFormProvider guardUnload> also arms a browser beforeunload prompt while dirty.

Value-like objects (Date, Moment, Decimal)

A field compares values with Object.is. For a string, number, boolean or bigint that is exactly right — identity and value are the same thing, so bigint needs nothing special here. For a File it is also right: a different File genuinely is a different value.

It is wrong for a value-like object, where two instances can mean the same thing:

form.field.when.set(new Date('2026-01-01')); // the value it already held
form.isDirty();                              // → true. Same instant, different object.

Re-picking the date the user already chose marks the form dirty and fires your unsaved-changes guard. Tell the field what equality means and both stop — the write is dropped, nothing is notified, and dirty-tracking compares the same way:

const form = createForm<Contract>(defaults, (f) => {
  f.validFrom.equals((a, b) => a?.valueOf() === b?.valueOf()); // Date, Moment, Dayjs
  f.price.equals((a, b) => a.eq(b));                           // Decimal.js, Money, …
});

It is per field: siblings keep Object.is. It works through each() for array items. And it does not rescue a value mutated in place —

moment.add(1, 'day');
form.field.validFrom.set(moment); // compares the object with itself

— because no comparator can see a change that happened behind the field's back. That write is half-applied: the new value is in values() and will submit, but the input never repaints and the form never looks dirty. Replace, never mutate — the same rule React state has always had.

The simplest option remains a primitive. An ISO YYYY-MM-DD string needs no comparator, survives JSON.stringify, and matches how dates usually cross the wire anyway.


Debugging

Forms fail quietly. A write that does not stick, a rule that fires on a value you did not expect, a dirty flag that will not clear — none of it throws, and from the outside it all looks like a React wiring problem. So the engine narrates itself through @dccs/teff-log.

Nothing is stripped from production. The instrumentation ships and stays silent until the log level is raised, so a form misbehaving in a deployed app can be inspected where it misbehaves — no rebuild, no local reproduction:

// browser console, takes effect on the next change
globalThis.__teff__.logger.level = 'DEBUG';
# or from the URL, for hash routers
https://app.example.com/#/invoices?teffLogLevel=DEBUG
// or from application code
import { Logger, LogLevel } from '@dccs/teff-log';
Logger.setLevel(LogLevel.DEBUG);

At DEBUG every change reports itself — how it was routed, which rule rejected what, and every array mutation with the ids it touched:

2026-08-18T09:12:04.881Z [DEBUG] teff-rfc invoice set lines.k2.qty {to: 'number', as: 'leaf'}
2026-08-18T09:12:04.883Z [DEBUG] teff-rfc invoice rule failed lines.k2.qty {value: 'number', message: 'max 2 per line'}
2026-08-18T09:12:07.114Z [DEBUG] teff-rfc invoice append lines {id: 'k7', at: 3, value: 'object{qty, label}', length: 4}

What is never logged

Values. Form values are user data — passwords, addresses, whatever someone typed — and every record is also retained in the buffer Logger.getMessages() hands to whoever writes a bug report. Since any end user can raise the level from the URL or the console, every record must be safe at every level. So a value appears only as its kind and size — string(8), array(3), object{city, zip}, Date instance — never as content. Paths, shapes, ids, rule messages and counts answer nearly every question the values would; for the rest, form.debug()'s return value hands the full data to the developer who explicitly asked, without logging it.

One consequence worth knowing as the author of an app: rule messages are logged verbatim. Write 'Max 20% in AT', not `${value} is too high` — interpolating the rejected value into the message would carry it into the records.

Writes that cannot be applied warn by themselves

A value whose shape disagrees with the field's is accepted by the graph but dropped by the projection — the write is real, values() never shows it. That is reported at WARN, which is above the default level, so nobody has to have opted in first:

[WARN] teff-rfc invoice set validFrom {expected: 'branch', received: 'leaf',
  reason: "the field's default makes \"validFrom\" a branch; a leaf written here is not visible in values()"}

Note that field.set(null) on a nullable section type-checks — Field<T> intersects Leaf<T> — so the compiler cannot catch this class of mistake. The warning can.

form.debug()

One call, for when a form does not do what the code says it should. It returns the full snapshot to the caller, and logs a values-described copy at INFO (the retained buffer is globally readable, so even an explicit report carries descriptions, not content):

const { dirty, errors, derived, reordered, arrays, values } = form.debug();

| Field | Answers | | --- | --- | | dirty | Why is this form dirty? — every drifted path with initial and current. | | reordered | The other reason it is dirty: arrays whose item order moved. | | errors | Failing rules with the value that failed, not just the message. | | derived | Paths backed by a derivation — the ones that reject writes. | | arrays | Array paths mapped to their current item ids, in order. | | validating / asyncRules | What is in flight, and what could be. | | values, epoch, fields, touched, name, valid | The rest of the picture. |

Several forms on one screen stay apart via createForm(defaults, setup, { name: 'invoice' }); unnamed ones become form#1, form#2, … The epoch counter lets you tie a burst of log lines to a single render.

For a bug report, Logger.getMessages() returns the retained trace — the last N records, ready to paste.


API

createForm<T>(defaults, setup?, options?)

Creates a form. setup(field, form) runs once and is where derived values and rules are declared. options.name labels the form in diagnostics (default form#1, form#2, …). Returns a Form<T>.

Field<T>

The typed, lazy handle to a value. Resolves one level per property access.

Leaf fields

| Member | Description | | --- | --- | | get() / set(v) / update(fn) | Read / write / functional update. | | reset() | Restore to the initial value. | | valid() | true or the error message. | | error() | Error message, or null when valid. | | validating() | Whether an async rule is in flight. | | isDirty() / isTouched() / touch() | Change- and interaction-state. | | rule(...validators) | Register synchronous rules (first failure wins). | | asyncRule(fn, opts?) | Register an async rule. | | derive(fn) | Mark the field derived from other fields. | | equals(fn) | Teach the field what unchanged means. See value-like objects. | | path | The dot-path. |

Array fields

items(), length(), ids(), at(i), append, prepend, insert, remove(id), removeAt(i), move, swap, clear, replace(values), reset, each(init), rule(...), valid(), error().

Form<T>

field, values(), setValues(next), reset(), validate(), validateAsync(), isValid(), isDirty(), errors(), stats(), subscribe(listener), debug().

Hooks

| Hook | Purpose | | --- | --- | | useForm(factory) | Create the form once; stable across renders. | | useField(field) | Everything an input needs: { value, setValue, update, error, isValid, isValidating, isDirty, isTouched, touch, reset, path }. | | useFieldValue(field) | Just the value (fine-grained). | | useFieldError(field) | Just the error. | | useArray(field) | { items, length, append, prepend, insert, remove, removeAt, move, swap, clear }. | | useWatch(form, selector) | Read across fields; re-renders only when the selected value changes. | | useFormValues(form) | The whole value object (debug panels, previews). | | useFormStats(form) | { fields, dirty, errors }. | | useAnyFormDirty() | true when any form under the provider is dirty. | | useResetAllForms() | Resets every dirty form under the provider. |

Provider & i18n

<TeffFormProvider resolveMessage? guardUnload?>, msg(key, params?), and the augmentable Register interface (MessageKey, MessageRef, MessageResolver).

Validators

required, minLength, maxLength, min, max, between, pattern, email, url, integer, oneOf, matches, when, all, custom.


Honest limits

  • "O(1)" is in affected nodes, not DOM operations. The one subscribed leaf still re-runs — that is React's floor. The win is that nothing else does.
  • Async rules need the explicit asyncRule path; auto-tracking can't follow await.
  • No path-keyed config object. Rules live as functions on the field, which is what keeps both the type-checker and the graph fast.
  • Model keys named exactly like a leaf method (get, set, rule, …) resolve to the child field, not the method. Field names like items, length, append are fully supported.
  • A plain object is always a section; anything else is one value. Date, Map, Set, File, Moment/Dayjs and your own domain entities are held as they are, never spread into child fields. The types are looser than the runtime here: because TypeScript cannot see a prototype, Field<Moment> still offers the instance's own properties as child fields. Reading or writing the field as a whole is correct; navigating into it is not. A class instance used as a section therefore does not work — its keys are not child fields.
  • Change detection is Object.is per field, not a deep compare. That is what keeps a keystroke from re-validating the form, and it is why a value-like object needs equals. Values mutated in place are invisible to it, by design.
  • Replacing a section with a single value does not stick. set(null) on a nullable section, or a class instance onto a path whose default is a plain object, lands in the graph but is dropped by the projection. It type-checks, so the compiler will not stop you — the engine warns at runtime instead.

Development

pnpm install
pnpm test          # vitest
pnpm run coverage  # vitest + v8 coverage
pnpm run check     # prettier + eslint + audit
pnpm run build     # clean + check + madge + tsup (ESM + CJS + d.ts)

Contributions are welcome — read CONTRIBUTING.md first: it explains the hard rules (one runtime dependency, React 18+), what kinds of MRs are accepted, and why most needs compose from the public API without touching the library at all.

License

MIT