Skip to content

Commit 6879f57

Browse files
MAST1999autofix-ci[bot]crutchcorn
authored
feat(solid-form): add createFormHook for solid-js. (#1597)
* feat(solid-form): add `createFormHook` for solid-js. * fix(solid-form): make the field in useFieldContext an accessor. * fix(solid-form): directly assign values to form object. This fixes and issue where some parts of the form, like submitting didn't work. * feat(solid-form): add docs for form composition for solid. * ci: apply automated fixes and generate docs * fix: move to PoeplePage to align with the large form example. * feat(solid-form): add large form example. * ci: apply automated fixes and generate docs * chore: use tanstack store adapter * chore: fix CI * ci: apply automated fixes and generate docs --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Corbin Crutchley <git@crutchcorn.dev>
1 parent 1f69b68 commit 6879f57

24 files changed

+1547
-40
lines changed

docs/framework/react/guides/form-composition.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,12 +302,12 @@ const { useAppForm, withForm } = createFormHook({
302302
```tsx
303303
// src/App.tsx
304304
import { Suspense } from 'react'
305-
import { PeoplePage } from './features/people/page.tsx'
305+
import { PeoplePage } from './features/people/form.tsx'
306306

307307
export default function App() {
308308
return (
309309
<Suspense fallback={<p>Loading...</p>}>
310-
<PeopleForm />
310+
<PeoplePage />
311311
</Suspense>
312312
)
313313
}
Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
---
2+
id: form-composition
3+
title: Form Composition
4+
---
5+
6+
A common criticism of TanStack Form is its verbosity out-of-the-box. While this _can_ be useful for educational purposes - helping enforce understanding our APIs - it's not ideal in production use cases.
7+
8+
As a result, while `form.Field` enables the most powerful and flexible usage of TanStack Form, we provide APIs that wrap it and make your application code less verbose.
9+
10+
## Custom Form Hooks
11+
12+
The most powerful way to compose forms is to create custom form hooks. This allows you to create a form hook that is tailored to your application's needs, including pre-bound custom UI components and more.
13+
14+
At it's most basic, `createFormHook` is a function that takes a `fieldContext` and `formContext` and returns a `useAppForm` hook.
15+
16+
> This un-customized `useAppForm` hook is identical to `useForm`, but that will quickly change as we add more options to `createFormHook`.
17+
18+
```tsx
19+
import { createFormHookContexts, createFormHook } from '@tanstack/react-form'
20+
21+
// export useFieldContext for use in your custom components
22+
export const { fieldContext, formContext, useFieldContext } =
23+
createFormHookContexts()
24+
25+
const { useAppForm } = createFormHook({
26+
fieldContext,
27+
formContext,
28+
// We'll learn more about these options later
29+
fieldComponents: {},
30+
formComponents: {},
31+
})
32+
33+
function App() {
34+
const form = useAppForm({
35+
// Supports all useForm options
36+
defaultValues: {
37+
firstName: 'John',
38+
lastName: 'Doe',
39+
},
40+
})
41+
42+
return <form.Field /> // ...
43+
}
44+
```
45+
46+
### Pre-bound Field Components
47+
48+
Once this scaffolding is in place, you can start adding custom field and form components to your form hook.
49+
50+
> Note: the `useFieldContext` must be the same one exported from your custom form context
51+
52+
```tsx
53+
import { useFieldContext } from './form-context.tsx'
54+
55+
export function TextField(props: { label: string }) {
56+
// The `Field` infers that it should have a `value` type of `string`
57+
const field = useFieldContext<string>()
58+
return (
59+
<label>
60+
<div>{props.label}</div>
61+
<input
62+
value={field().state.value}
63+
onChange={(e) => field().handleChange(e.target.value)}
64+
/>
65+
</label>
66+
)
67+
}
68+
```
69+
70+
You're then able to register this component with your form hook.
71+
72+
```tsx
73+
import { TextField } from './text-field.tsx'
74+
75+
const { useAppForm } = createFormHook({
76+
fieldContext,
77+
formContext,
78+
fieldComponents: {
79+
TextField,
80+
},
81+
formComponents: {},
82+
})
83+
```
84+
85+
And use it in your form:
86+
87+
```tsx
88+
function App() {
89+
const form = useAppForm({
90+
defaultValues: {
91+
firstName: 'John',
92+
lastName: 'Doe',
93+
},
94+
})
95+
96+
return (
97+
// Notice the `AppField` instead of `Field`; `AppField` provides the required context
98+
<form.AppField
99+
name="firstName"
100+
children={(field) => <field.TextField label="First Name" />}
101+
/>
102+
)
103+
}
104+
```
105+
106+
This not only allows you to reuse the UI of your shared component, but retains the type-safety you'd expect from TanStack Form: Typo `name` and get a TypeScript error.
107+
108+
### Pre-bound Form Components
109+
110+
While `form.AppField` solves many of the problems with Field boilerplate and reusability, it doesn't solve the problem of _form_ boilerplate and reusability.
111+
112+
In particular, being able to share instances of `form.Subscribe` for, say, a reactive form submission button is a common usecase.
113+
114+
```tsx
115+
function SubscribeButton(props: { label: string }) {
116+
const form = useFormContext()
117+
return (
118+
<form.Subscribe selector={(state) => state.isSubmitting}>
119+
{(isSubmitting) => (
120+
<button type="submit" disabled={isSubmitting()}>
121+
{props.label}
122+
</button>
123+
)}
124+
</form.Subscribe>
125+
)
126+
}
127+
128+
const { useAppForm, withForm } = createFormHook({
129+
fieldComponents: {},
130+
formComponents: {
131+
SubscribeButton,
132+
},
133+
fieldContext,
134+
formContext,
135+
})
136+
137+
function App() {
138+
const form = useAppForm({
139+
defaultValues: {
140+
firstName: 'John',
141+
lastName: 'Doe',
142+
},
143+
})
144+
145+
return (
146+
<form.AppForm>
147+
// Notice the `AppForm` component wrapper; `AppForm` provides the required
148+
context
149+
<form.SubscribeButton label="Submit" />
150+
</form.AppForm>
151+
)
152+
}
153+
```
154+
155+
## Breaking big forms into smaller pieces
156+
157+
Sometimes forms get very large; it's just how it goes sometimes. While TanStack Form supports large forms well, it's never fun to work with hundreds or thousands of lines of code long files.
158+
159+
To solve this, we support breaking forms into smaller pieces using the `withForm` higher-order component.
160+
161+
```tsx
162+
const { useAppForm, withForm } = createFormHook({
163+
fieldComponents: {
164+
TextField,
165+
},
166+
formComponents: {
167+
SubscribeButton,
168+
},
169+
fieldContext,
170+
formContext,
171+
})
172+
173+
const ChildForm = withForm({
174+
// These values are only used for type-checking, and are not used at runtime
175+
// This allows you to `...formOpts` from `formOptions` without needing to redeclare the options
176+
defaultValues: {
177+
firstName: 'John',
178+
lastName: 'Doe',
179+
},
180+
// Optional, but adds props to the `render` function in addition to `form`
181+
props: {
182+
// These props are also set as default values for the `render` function
183+
title: 'Child Form',
184+
},
185+
render: function Render(props) {
186+
return (
187+
<div>
188+
<p>{props.title}</p>
189+
<props.form.AppField
190+
name="firstName"
191+
children={(field) => <field.TextField label="First Name" />}
192+
/>
193+
<props.form.AppForm>
194+
<props.form.SubscribeButton label="Submit" />
195+
</props.form.AppForm>
196+
</div>
197+
)
198+
},
199+
})
200+
201+
function App() {
202+
const form = useAppForm({
203+
defaultValues: {
204+
firstName: 'John',
205+
lastName: 'Doe',
206+
},
207+
})
208+
209+
return <ChildForm form={form} title={'Testing'} />
210+
}
211+
```
212+
213+
### `withForm` FAQ
214+
215+
> Why a higher-order component instead of a hook?
216+
217+
While hooks are the future of React, higher-order components are still a powerful tool for composition. In particular, the API of `withForm` enables us to have strong type-safety without requiring users to pass generics.
218+
219+
## Tree-shaking form and field components
220+
221+
While the above examples are great for getting started, they're not ideal for certain use-cases where you might have hundreds of form and field components.
222+
In particular, you may not want to include all of your form and field components in the bundle of every file that uses your form hook.
223+
224+
To solve this, you can mix the `createFormHook` TanStack API with the Solid `lazy` and `Suspense` components:
225+
226+
```typescript
227+
// src/hooks/form-context.ts
228+
import { createFormHookContexts } from '@tanstack/solid-form'
229+
230+
export const { fieldContext, useFieldContext, formContext, useFormContext } =
231+
createFormHookContexts()
232+
```
233+
234+
```tsx
235+
// src/components/text-field.tsx
236+
import { useFieldContext } from '../hooks/form-context.tsx'
237+
238+
export default function TextField(props: { label: string }) {
239+
const field = useFieldContext<string>()
240+
241+
return (
242+
<label>
243+
<div>{props.label}</div>
244+
<input
245+
value={field().state.value}
246+
onChange={(e) => field().handleChange(e.target.value)}
247+
/>
248+
</label>
249+
)
250+
}
251+
```
252+
253+
```tsx
254+
// src/hooks/form.ts
255+
import { lazy } from 'solid-js'
256+
import { createFormHook } from '@tanstack/react-form'
257+
258+
const TextField = lazy(() => import('../components/text-fields.tsx'))
259+
260+
const { useAppForm, withForm } = createFormHook({
261+
fieldContext,
262+
formContext,
263+
fieldComponents: {
264+
TextField,
265+
},
266+
formComponents: {},
267+
})
268+
```
269+
270+
```tsx
271+
// src/App.tsx
272+
import { Suspense } from 'solid-js'
273+
import { PeoplePage } from './features/people/form.tsx'
274+
275+
export default function App() {
276+
return (
277+
<Suspense fallback={<p>Loading...</p>}>
278+
<PeoplePage />
279+
</Suspense>
280+
)
281+
}
282+
```
283+
284+
This will show the Suspense fallback while the `TextField` component is being loaded, and then render the form once it's loaded.
285+
286+
## Putting it all together
287+
288+
Now that we've covered the basics of creating custom form hooks, let's put it all together in a single example.
289+
290+
```tsx
291+
// /src/hooks/form.ts, to be used across the entire app
292+
const { fieldContext, useFieldContext, formContext, useFormContext } =
293+
createFormHookContexts()
294+
295+
function TextField(props: { label: string }) {
296+
const field = useFieldContext<string>()
297+
return (
298+
<label>
299+
<div>{props.label}</div>
300+
<input
301+
value={field().state.value}
302+
onChange={(e) => field().handleChange(e.target.value)}
303+
/>
304+
</label>
305+
)
306+
}
307+
308+
function SubscribeButton(props: { label: string }) {
309+
const form = useFormContext()
310+
return (
311+
<form.Subscribe selector={(state) => state.isSubmitting}>
312+
{(isSubmitting) => (
313+
<button disabled={isSubmitting()}>{props.label}</button>
314+
)}
315+
</form.Subscribe>
316+
)
317+
}
318+
319+
const { useAppForm, withForm } = createFormHook({
320+
fieldComponents: {
321+
TextField,
322+
},
323+
formComponents: {
324+
SubscribeButton,
325+
},
326+
fieldContext,
327+
formContext,
328+
})
329+
330+
// /src/features/people/shared-form.ts, to be used across `people` features
331+
const formOpts = formOptions({
332+
defaultValues: {
333+
firstName: 'John',
334+
lastName: 'Doe',
335+
},
336+
})
337+
338+
// /src/features/people/nested-form.ts, to be used in the `people` page
339+
const ChildForm = withForm({
340+
...formOpts,
341+
// Optional, but adds props to the `render` function outside of `form`
342+
props: {
343+
title: 'Child Form',
344+
},
345+
render: (props) => {
346+
return (
347+
<div>
348+
<p>{title}</p>
349+
<props.form.AppField
350+
name="firstName"
351+
children={(field) => <field.TextField label="First Name" />}
352+
/>
353+
<props.form.AppForm>
354+
<props.form.SubscribeButton label="Submit" />
355+
</props.form.AppForm>
356+
</div>
357+
)
358+
},
359+
})
360+
361+
// /src/features/people/page.ts
362+
const Parent = () => {
363+
const form = useAppForm({
364+
...formOpts,
365+
})
366+
367+
return <ChildForm form={form} title={'Testing'} />
368+
}
369+
```
370+
371+
## API Usage Guidance
372+
373+
Here's a chart to help you decide what APIs you should be using:
374+
375+
![](https://raw.githubusercontent.com/TanStack/form/main/docs/assets/react_form_composability.svg)

0 commit comments

Comments
 (0)