Skip to content
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

Reusable app hooks #1179

Merged
merged 23 commits into from
Mar 1, 2025
Merged

Reusable app hooks #1179

merged 23 commits into from
Mar 1, 2025

Conversation

crutchcorn
Copy link
Member

@crutchcorn crutchcorn commented Feb 25, 2025

This has been cooking in the oven for a long time - I'm extremely sorry it took me so long to figure out the direction we wanted to go in.

This PR introduces three new APIs to help with:

  • Form composability
  • UI component binding
  • form.Field boilerplate

All without having to type cast any generic by hand.

The API, for React, is as such:

// /src/hooks/form.ts, to be used across the entire app
const { fieldContext, useFieldContext, formContext, useFormContext } =
  createFormHookContexts()

function TextField({ label }: { label: string }) {
  const field = useFieldContext<string>()
  return (
    <label>
      <div>{label}</div>
      <input
        value={field.state.value}
        onChange={(e) => field.handleChange(e.target.value)}
      />
    </label>
  )
}

function SubscribeButton({ label }: { label: string }) {
  const form = useFormContext()
  return (
    <form.Subscribe selector={(state) => state.isSubmitting}>
      {(isSubmitting) => <button disabled={isSubmitting}>{label}</button>}
    </form.Subscribe>
  )
}

const { useAppForm, withForm } = createFormHook({
  fieldComponents: {
    TextField,
  },
  formComponents: {
    SubscribeButton,
  },
  fieldContext,
  formContext,
})

// /src/features/people/shared-form.ts, to be used across `people` features
const formOpts = formOptions({
  defaultValues: {
    firstName: 'John',
    lastName: 'Doe',
  },
})

// /src/features/people/nested-form.ts, to be used in the `people` page
const ChildForm = withForm({
  ...formOpts,
  // Optional, but adds props to the `render` function outside of `form`
  props: {
    title: 'Child Form',
  },
  render: ({ form, title }) => {
    return (
      <div>
        <p>{title}</p>
        <form.AppField
          name="firstName"
          children={(field) => <field.TextField label="First Name" />}
        />
        <form.AppForm>
            <form.SubscribeButton label="Submit" />
        </form.AppForm>
      </div>
    )
  },
})

// /src/features/people/page.ts
const Parent = () => {
  const form = useAppForm({
    ...formOpts,
  })

  return <ChildForm form={form} title={'Testing'} />
}

TODO

  • Confirm this doesn't break React Compiler usage
  • Gather feedback from other maintainers on withField and useAppForm API
  • Port this API (or similar) to other frameworks
    • May be delayed until after v1
  • Write docs

Notes

This PR supersedes #583, #825, and #1084

Copy link

nx-cloud bot commented Feb 25, 2025

View your CI Pipeline Execution ↗ for commit 7d9300e.

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 54s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2025-03-01 06:04:33 UTC

Copy link

pkg-pr-new bot commented Feb 25, 2025

Open in Stackblitz

More templates

@tanstack/angular-form

npm i https://pkg.pr.new/@tanstack/angular-form@1179

@tanstack/form-core

npm i https://pkg.pr.new/@tanstack/form-core@1179

@tanstack/react-form

npm i https://pkg.pr.new/@tanstack/react-form@1179

@tanstack/lit-form

npm i https://pkg.pr.new/@tanstack/lit-form@1179

@tanstack/vue-form

npm i https://pkg.pr.new/@tanstack/vue-form@1179

@tanstack/solid-form

npm i https://pkg.pr.new/@tanstack/solid-form@1179

commit: 7d9300e

@crutchcorn crutchcorn mentioned this pull request Feb 25, 2025
11 tasks
Copy link

codecov bot commented Feb 25, 2025

Codecov Report

Attention: Patch coverage is 93.54839% with 2 lines in your changes missing coverage. Please review.

Project coverage is 88.62%. Comparing base (bea5803) to head (7d9300e).
Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
packages/react-form/src/createFormHook.tsx 93.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1179      +/-   ##
==========================================
+ Coverage   88.50%   88.62%   +0.11%     
==========================================
  Files          27       28       +1     
  Lines        1218     1248      +30     
  Branches      322      325       +3     
==========================================
+ Hits         1078     1106      +28     
- Misses        125      127       +2     
  Partials       15       15              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@LeCarbonator

This comment was marked as resolved.

@crutchcorn
Copy link
Member Author

crutchcorn commented Feb 25, 2025

Agreed @LeCarbonator. The idea will be that if you need to use form.Subscribe then you can use formComponents, but if you're using form.Field you can use fieldComponents. Something like this:

---
config:
    look: handDrawn
---
flowchart TD
    A["Do you need to reuse state (like defaultValues)?"]
    A -- Yes --> B["Use 'formOptions()'"]
    A -- No --> C["Do you need to reuse custom validation functions?"]
    C -- Yes --> D["Wrap 'useForm' hook into a custom app hook"]
    C -- No --> E["Do you need to reuse custom UI components?"]
    E -- Yes --> F["Do you need access to the 'field'?"]
    F -- Yes --> G["Use 'createFormHook''s 'fieldComponents'<br/>(EG: 'TextInput' and 'NumberInput')"]
    F -- No --> H["Use 'createFormHook''s 'formComponents'<br/>(EG: 'SubmitButton')"]
    E -- No --> I["Do you need to reuse whole subsections of your form?"]
    I -- Yes --> J["Use 'withForm' from 'createFormHook'"]
    I -- No --> K["Use 'form.Subscribe' and 'form.Field'"]
Loading

@gapurov

This comment was marked as resolved.

@crutchcorn

This comment was marked as resolved.

@LeCarbonator

This comment was marked as resolved.

crutchcorn and others added 5 commits February 28, 2025 12:51
* fix: type unions should now work properly

Co-authored-by: irwinarruda <[email protected]>

* ci: apply automated fixes and generate docs

* chore: reintroduce TDepth for infinate depth issues

Co-authored-by: irwinarruda <[email protected]>

* ci: apply automated fixes and generate docs

* test: add large schema test

---------

Co-authored-by: irwinarruda <[email protected]>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Copy link
Contributor

@LeCarbonator LeCarbonator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just minor suggestions and some typos. Looks good to me otherwise!

Note: This review only addresses the docs. I have not reviewed the code changes.

@crutchcorn crutchcorn changed the title [WIP] Reusable app hook Reusable app hooks Mar 1, 2025
@crutchcorn crutchcorn marked this pull request as ready for review March 1, 2025 06:06
@crutchcorn crutchcorn merged commit 49fea74 into main Mar 1, 2025
8 checks passed
@crutchcorn crutchcorn deleted the reusable-app-hook branch March 1, 2025 06:06
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.

3 participants