Skip to content

Reactive validation: useValidationState + validationStyles + ajvAdapter (#357)#359

Merged
CarlosNZ merged 2 commits into
v2.0-devfrom
357-use-validation-state
Jun 15, 2026
Merged

Reactive validation: useValidationState + validationStyles + ajvAdapter (#357)#359
CarlosNZ merged 2 commits into
v2.0-devfrom
357-use-validation-state

Conversation

@CarlosNZ

Copy link
Copy Markdown
Owner

Summary

Adds reactive, whole-document validation to @json-edit-react/utils: useValidationState, validationStyles, ajvAdapter, and the useStableValue primitive they build on. Closes the reactive angle of #357 (origin #197). No core changes — the hooks ride existing public-API channels.

The problem

Validating inside a render-time function of NodeData that reads fullData (a style function, an allow* filter, a custom-node condition) goes stale under fine-grained re-rendering. Document validity is a whole-document property — an edit at node A can flip the validity of node B on a different branch (JSON Schema if/then, dependentRequired, discriminated unions) — but B never re-renders when A changes, so its inline validation never re-runs. This is intentional: the React.memo comparator in memoNode.ts deliberately ignores fullData identity. The bug is demonstrated by the existing devOnly "Validation staleness" demo scratchpad.

The approach — identity keyed on the error set

  • useValidationState(data, validate) runs the validator once per data change (O(N), not the O(N²) of validating inside every node), normalizes the issues, and exposes an O(1) query surface: isValid, errors, hasErrorAt, errorsAt, hasErrorWithin.
  • The returned object is referentially stable while the error set is unchanged. Memoize a theme / customNodeDefinitions / allow* value on it and valid→valid commits keep the §16 node-memo boundary intact; when validity actually changes, the new identity pierces React.memo through that channel and the tree re-renders once — correctly restyling cross-branch nodes.
  • useStableValue(compute, deps, isEqual?) is the general identity-stabilizer underneath, exported for any other cross-branch derived value (duplicate detection, doc-wide totals).

What's included

  • stable-value/useStableValue (public) + an internal structural deepEqual.
  • validation/useValidationState, validationStyles (theme sugar; leaf slots consult hasErrorAt, opt-in within for collection ancestors), ajvAdapter, and the ValidationIssue / ValidationState / Validate types.
  • READMEs for both groups + a package-README section; changeset (@json-edit-react/utils minor).
  • A devOnly demo example, Validation flagging, mirroring the staleness scenario but fixed — a direct before/after.

Zero-dependency posture

The package adds no runtime dependency, not even on AJV: ajvAdapter is typed against AJV's error shape structurally, so consumers bring their own validator (or pass any (data) => ValidationIssue[]). The hook also keeps core a type-only dependency — the path-key helper is inlined locally rather than importing core's toPathString at runtime.

Testing

  • 18 new tests across test/stable-value.test.tsx and test/validation-state.test.tsx; full suite green (480 passed / 2 todo).
  • Coverage: the O(1) query surface (incl. root path, numeric indices, keys containing .), the §16 identity-stability invariant, ajvAdapter normalization (JSON-Pointer parsing/escapes, required→parent path), and validationStyles slot behaviour.
  • End-to-end cross-branch regression (renders a real JsonEditor): editing one node flips a different-branch node's validity; the far node bails on the commit via the memo boundary, so only the identity-pierce can restyle it. Verified sharp — temporarily freezing the identity check makes both cases fail with the genuine staleness signature (the cross-branch node keeps its stale colour).
  • Package tsc --noEmit clean; build clean (1.52 kB gzip ESM); no ajv in any dependency field or the bundle. Core lint + compile unaffected.

Out of scope (follow-ups)

Zod/Yup adapters; validation.filter() convenience; the createUpdateValidator commit-gate (shares this kernel); the custom-node glyph component (#358).

🤖 Generated with Claude Code

@CarlosNZ CarlosNZ merged commit 6bef51b into v2.0-dev Jun 15, 2026
2 checks passed
@CarlosNZ CarlosNZ deleted the 357-use-validation-state branch June 15, 2026 05:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant