Repository navigation
chore(lint): enforce useHeadlessForm over direct createHeadlessForm imports #1439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
8e4776f
fix(onboarding): submit computed money forced values in the right unit
cammellos ccf0275
refactor(forms): default transformMoneyFields per key
gabrielseco 77136fc
refactor(invoice-schedules): drop redundant money flag
gabrielseco c28068d
test(onboarding): cover money forced values on the legacy contract de…
gabrielseco 6896273
refactor(form): drop getForcedValue
gabrielseco 2d2809d
test(form): pin that forced values are submitted as their const
gabrielseco a5b4d35
Merge PR #1426 (refactor/create-headless-form-money-default)
gabrielseco 8ac5bcb
refactor(onboarding): drop explicit transformMoneyFields
gabrielseco 39b2adc
test(form): drop parseSubmitValues forced value test
gabrielseco 8e1421d
refactor(form): add useHeadlessForm with rebuild and buildOnce strate…
gabrielseco c3cc3b1
refactor(onboarding): build legacy contract details through useHeadle…
gabrielseco fbdf5a6
fix(form): stop buildOnce from rebuilding on every render with inline…
gabrielseco f1e9034
refactor(onboarding): build jsf v1 contract details through useHeadle…
gabrielseco 60b4ecb
fix(onboarding): refetch the contract details schema per employment
gabrielseco e5fc865
test(form): cover options shapes in the jsf engine contract
gabrielseco 5575dbc
docs(form): add useHeadlessForm rollout plan
gabrielseco 956e0f2
Merge branch 'main' into refactor/use-headless-form
gabrielseco 1b51631
chore(lint): enforce useHeadlessForm over direct createHeadlessForm i…
gabrielseco 0a89632
chore(lint): mark legacy createHeadlessForm imports inline instead of…
gabrielseco d7a65d3
docs(bugbot): drop unclear useHeadlessForm flag line
gabrielseco 7af4ea8
remove no effect
gabrielseco 120c985
chore(lint): fail example lint on unused disable directives
gabrielseco ede6120
fix(lint): catch relative createHeadlessForm imports
gabrielseco b718cf6
refactor(form): seed buildOnce forms from initialValues and move basi…
gabrielseco 11dc6f5
test(form): fold useHeadlessForm lifecycle cases into the jsf engine …
gabrielseco d3e0a35
docs(form): mark basic information as migrated to buildOnce in the ro…
gabrielseco 53da62f
test(onboarding): drop the contract details employment_id query key test
gabrielseco e3e68c0
Merge remote-tracking branch 'origin/refactor/use-headless-form' into…
gabrielseco 1571e23
docs(form): pass saved initialValues to buildOnce in the useHeadlessF…
gabrielseco a4acf1a
chore(lint): exempt __tests__ and colocated test files from the creat…
gabrielseco 759774c
refactor(onboarding): build benefits and engagement agreement details…
gabrielseco f27a6b0
Merge branch 'main' into chore/enforce-use-headless-form
gabrielseco File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,57 +1,59 @@ | ||
| --- | ||
| alwaysApply: true | ||
| description: Call createHeadlessForm once and recompute via handleValidation, not by re-calling createHeadlessForm on every value change | ||
| description: Build JSON Schema forms with useHeadlessForm, never by calling createHeadlessForm directly | ||
| glob: '{src,example/e2e}/**/*.{ts,tsx}' | ||
| --- | ||
|
|
||
| # createHeadlessForm / handleValidation Rule | ||
| # useHeadlessForm Rule | ||
|
|
||
| ## Philosophy: Recompute Incrementally, Don't Re-Derive From Scratch | ||
| ## Philosophy: One Hook Owns the Form Engine | ||
|
|
||
| `createHeadlessForm` (from `@remoteoss/remote-json-schema-form-kit`, wrapping either | ||
| `@remoteoss/json-schema-form` or the deprecated v0 package) is designed to be called **once** | ||
| per schema. When values change and conditional fields (`if`/`then`, visibility, available | ||
| `options`) need to be re-evaluated, call the `handleValidation` function it returns instead of | ||
| calling `createHeadlessForm` again. | ||
| `useHeadlessForm` (`src/common/useHeadlessForm.ts`) is the only place app code builds a form from a JSON Schema. It builds the form once per schema and options, then recomputes conditionals through `handleValidation`, which mutates the same `fields` array in place. Calling `createHeadlessForm` again on every value change throws that array away and rebuilds the whole form from the raw schema. | ||
|
|
||
| **Both jsf versions implement `handleValidation` by mutating the same `fields` array `createHeadlessForm` | ||
| returned, in place** — it does not return a new one. Re-invoking `createHeadlessForm` from scratch every | ||
| time values change throws away that array and re-derives the entire form (types, options, conditionals) | ||
| from the raw JSON Schema for no reason. | ||
| Importing `createHeadlessForm` is an oxlint error (`no-restricted-imports` in `.oxlintrc.json`). | ||
|
|
||
| ## Pattern to Follow | ||
|
|
||
| ### ❌ DON'T: Re-call createHeadlessForm on every value change | ||
| ### ❌ DON'T: Call createHeadlessForm yourself | ||
|
|
||
| ```typescript | ||
| function recompute(values: Record<string, unknown>) { | ||
| // Wasteful: re-parses the schema and rebuilds every field from scratch each time. | ||
| const { fields } = createHeadlessForm(schema, { initialValues: values }); | ||
| return fields; | ||
| } | ||
| const form = useMemo( | ||
| () => (schema ? createHeadlessForm(schema, fieldValues, options) : null), | ||
| [schema, fieldValues, options], | ||
| ); | ||
| ``` | ||
|
|
||
| ### ✅ DO: Call createHeadlessForm once, then drive updates through handleValidation | ||
| ### ✅ DO: Fetch the schema, hand it to useHeadlessForm | ||
|
|
||
| ```typescript | ||
| const { fields, handleValidation } = createHeadlessForm(schema, { | ||
| initialValues: values, | ||
| const { data: schema, isLoading } = useQuery({ | ||
| queryKey: ['my-form-schema', countryCode], | ||
| queryFn: fetchMySchema, | ||
| select: ({ data }) => data?.data, | ||
| }); | ||
|
|
||
| function recompute(values: Record<string, unknown>) { | ||
| handleValidation(values); // mutates `fields` in place — re-read it, don't re-derive it | ||
| return fields; | ||
| } | ||
| const { form, handleValidation, parseFormValues, onValuesChange } = | ||
| useHeadlessForm({ | ||
| schema, | ||
| initialValues: savedValues, | ||
| options: { jsfModify: options?.jsfModify }, | ||
| strategy: 'buildOnce', | ||
| }); | ||
| ``` | ||
|
|
||
| **Reference:** `example/e2e/helpers/benefits.ts` (`fillOnboardingBenefitsStepDynamically`) calls | ||
| `createHeadlessForm` once and loops on `handleValidation(values)` to discover fields that only | ||
| become visible once a sibling field (e.g. a benefit's "value" after its "filter") has a value. | ||
| `initialValues` are the saved values in API units (partner initial values merged with the employment's saved step data), not the live form values. They seed the first build so conditional fields are visible before the step mounts; after that, visibility follows `handleValidation`, and a rebuild caused by a `jsfModify` change keeps the last validated values. | ||
|
|
||
| Expose `handleValidation` and `parseFormValues` from the hook on the flow bag rather than calling `parseJSFToValidate` by hand, and call `onValuesChange` when the form values change. | ||
|
|
||
| **Reference:** `useBasicInformationSchema` and `useContractDetailsSchema` in `src/flows/Onboarding/api.ts`. | ||
|
|
||
| ## Strategies | ||
|
|
||
| - **`buildOnce`**: use this for all new code. | ||
| - **`rebuild`**: only for moving a legacy call site onto the hook without changing behaviour. It will be removed (see `docs/USE_HEADLESS_FORM_ROLLOUT.md`). | ||
|
|
||
| ## Known Exception: Legacy Call Sites | ||
|
|
||
| Some existing code (e.g. `useBenefitOffersSchema` and similar hooks in `src/flows/*/api.ts`) calls | ||
| `createHeadlessForm` fresh inside a React Query `select` that re-runs whenever `fieldValues` | ||
| changes, instead of using `handleValidation`. This is accepted technical debt, not the pattern to | ||
| copy — new code and refactors of these call sites should move to the `handleValidation` pattern | ||
| above. | ||
| Files that still call `createHeadlessForm` directly carry a `// oxlint-disable-next-line no-restricted-imports -- TODO` comment on the import and are tracked in `docs/USE_HEADLESS_FORM_ROLLOUT.md`. That's debt, not a pattern to copy. Moving a file onto the hook removes its comment (lint fails on unused ones). New code never adds one. | ||
|
|
||
| Tests, `scripts/`, and `example/e2e` helpers (e.g. `fillOnboardingBenefitsStepDynamically` in `example/e2e/helpers/benefits.ts`) may still use the engine directly. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.