Eine häufige Kritik an TanStack Form ist seine Standard-Ausführlichkeit. Während dies für Bildungszwecke nützlich sein kann - und das Verständnis unserer APIs fördert -, ist es für Produktionsanwendungsfälle nicht ideal.
Daher ermöglicht form.Field zwar die leistungsfähigste und flexibelste Nutzung von TanStack Form, aber wir stellen APIs bereit, die es umschließen und Ihren Anwendungscode weniger ausführlich machen.
Der leistungsfähigste Weg, Formulare zu komponieren, ist die Erstellung benutzerdefinierter Formular-Hooks. Dies ermöglicht es Ihnen, einen Formular-Hook zu erstellen, der auf die Bedürfnisse Ihrer Anwendung zugeschnitten ist, einschließlich vorgebundener benutzerdefinierter UI-Komponenten und mehr.
Im Grunde ist createFormHook eine Funktion, die einen fieldContext und formContext entgegennimmt und einen useAppForm Hook zurückgibt.
Dieser un-angepasste useAppForm Hook ist identisch mit useForm, aber das wird sich schnell ändern, wenn wir weitere Optionen zu createFormHook hinzufügen.
import { createFormHookContexts, createFormHook } from '@tanstack/solid-form'
// export useFieldContext for use in your custom components
export const { fieldContext, formContext, useFieldContext } =
createFormHookContexts()
const { useAppForm } = createFormHook({
fieldContext,
formContext,
// We'll learn more about these options later
fieldComponents: {},
formComponents: {},
})
function App() {
const form = useAppForm(() => ({
// Supports all useForm options
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
}))
return <form.Field /> // ...
}
import { createFormHookContexts, createFormHook } from '@tanstack/solid-form'
// export useFieldContext for use in your custom components
export const { fieldContext, formContext, useFieldContext } =
createFormHookContexts()
const { useAppForm } = createFormHook({
fieldContext,
formContext,
// We'll learn more about these options later
fieldComponents: {},
formComponents: {},
})
function App() {
const form = useAppForm(() => ({
// Supports all useForm options
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
}))
return <form.Field /> // ...
}
Sobald dieses Gerüst steht, können Sie damit beginnen, benutzerdefinierte Feld- und Formular-Komponenten zu Ihrem Formular-Hook hinzuzufügen.
Hinweis: Der useFieldContext muss derselbe sein, der aus Ihrem benutzerdefinierten Formular-Kontext exportiert wird
import { useFieldContext } from './form-context.tsx'
export function TextField(props: { label: string }) {
// The `Field` infers that it should have a `value` type of `string`
const field = useFieldContext<string>()
return (
<label>
<div>{props.label}</div>
<input
value={field().state.value}
onChange={(e) => field().handleChange(e.target.value)}
/>
</label>
)
}
import { useFieldContext } from './form-context.tsx'
export function TextField(props: { label: string }) {
// The `Field` infers that it should have a `value` type of `string`
const field = useFieldContext<string>()
return (
<label>
<div>{props.label}</div>
<input
value={field().state.value}
onChange={(e) => field().handleChange(e.target.value)}
/>
</label>
)
}
Sie können diese Komponente dann mit Ihrem Formular-Hook registrieren.
import { TextField } from './text-field.tsx'
const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
TextField,
},
formComponents: {},
})
import { TextField } from './text-field.tsx'
const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
TextField,
},
formComponents: {},
})
Und sie in Ihrem Formular verwenden
function App() {
const form = useAppForm(() => ({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
}))
return (
// Notice the `AppField` instead of `Field`; `AppField` provides the required context
<form.AppField
name="firstName"
children={(field) => <field.TextField label="First Name" />}
/>
)
}
function App() {
const form = useAppForm(() => ({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
}))
return (
// Notice the `AppField` instead of `Field`; `AppField` provides the required context
<form.AppField
name="firstName"
children={(field) => <field.TextField label="First Name" />}
/>
)
}
Dies ermöglicht Ihnen nicht nur die Wiederverwendung der UI Ihrer gemeinsam genutzten Komponente, sondern behält auch die Typsicherheit bei, die Sie von TanStack Form erwarten: Tippfehler bei name und Sie erhalten einen TypeScript-Fehler.
Während form.AppField viele der Probleme mit Field-Boilerplate und Wiederverwendbarkeit löst, löst es nicht das Problem der Formular-Boilerplate und Wiederverwendbarkeit.
Insbesondere die Möglichkeit, Instanzen von form.Subscribe für beispielsweise einen reaktiven Formularübermittlungsbutton zu teilen, ist ein gängiger Anwendungsfall.
function SubscribeButton(props: { label: string }) {
const form = useFormContext()
return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (
<button type="submit" disabled={isSubmitting()}>
{props.label}
</button>
)}
</form.Subscribe>
)
}
const { useAppForm, withForm } = createFormHook({
fieldComponents: {},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})
function App() {
const form = useAppForm(() => ({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
}))
return (
<form.AppForm>
// Notice the `AppForm` component wrapper; `AppForm` provides the required
context
<form.SubscribeButton label="Submit" />
</form.AppForm>
)
}
function SubscribeButton(props: { label: string }) {
const form = useFormContext()
return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (
<button type="submit" disabled={isSubmitting()}>
{props.label}
</button>
)}
</form.Subscribe>
)
}
const { useAppForm, withForm } = createFormHook({
fieldComponents: {},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})
function App() {
const form = useAppForm(() => ({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
}))
return (
<form.AppForm>
// Notice the `AppForm` component wrapper; `AppForm` provides the required
context
<form.SubscribeButton label="Submit" />
</form.AppForm>
)
}
Manchmal werden Formulare sehr groß; so ist es eben manchmal. Während TanStack Form große Formulare gut unterstützt, ist es nie schön, mit Hunderten oder Tausenden von Zeilen Code langen Dateien zu arbeiten.
Um dies zu lösen, unterstützen wir das Zerlegen von Formularen in kleinere Teile mithilfe der Higher-Order-Komponente withForm.
const { useAppForm, withForm } = createFormHook({
fieldComponents: {
TextField,
},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})
const ChildForm = withForm({
// These values are only used for type-checking, and are not used at runtime
// This allows you to `...formOpts` from `formOptions` without needing to redeclare the options
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
// Optional, but adds props to the `render` function in addition to `form`
props: {
// These props are also set as default values for the `render` function
title: 'Child Form',
},
render: function Render(props) {
return (
<div>
<p>{props.title}</p>
<props.form.AppField
name="firstName"
children={(field) => <field.TextField label="First Name" />}
/>
<props.form.AppForm>
<props.form.SubscribeButton label="Submit" />
</props.form.AppForm>
</div>
)
},
})
function App() {
const form = useAppForm(() => ({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
}))
return <ChildForm form={form} title={'Testing'} />
}
const { useAppForm, withForm } = createFormHook({
fieldComponents: {
TextField,
},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})
const ChildForm = withForm({
// These values are only used for type-checking, and are not used at runtime
// This allows you to `...formOpts` from `formOptions` without needing to redeclare the options
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
// Optional, but adds props to the `render` function in addition to `form`
props: {
// These props are also set as default values for the `render` function
title: 'Child Form',
},
render: function Render(props) {
return (
<div>
<p>{props.title}</p>
<props.form.AppField
name="firstName"
children={(field) => <field.TextField label="First Name" />}
/>
<props.form.AppForm>
<props.form.SubscribeButton label="Submit" />
</props.form.AppForm>
</div>
)
},
})
function App() {
const form = useAppForm(() => ({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
}))
return <ChildForm form={form} title={'Testing'} />
}
Warum eine Higher-Order-Komponente anstelle eines Hooks?
Während Hooks die Zukunft von Solid sind, sind Higher-Order-Komponenten immer noch ein mächtiges Werkzeug für Komposition. Insbesondere ermöglicht die API von withForm, dass wir eine starke Typsicherheit haben, ohne dass Benutzer Generics übergeben müssen.
Während die obigen Beispiele großartig für den Einstieg sind, sind sie für bestimmte Anwendungsfälle, bei denen Sie Hunderte von Formular- und Feldkomponenten haben könnten, nicht ideal. Insbesondere möchten Sie möglicherweise nicht alle Ihre Formular- und Feldkomponenten in den Bundle jeder Datei einbeziehen, die Ihren Formular-Hook verwendet.
Um dies zu lösen, können Sie die createFormHook TanStack API mit den Solid lazy und Suspense Komponenten mischen
// src/hooks/form-context.ts
import { createFormHookContexts } from '@tanstack/solid-form'
export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
// src/hooks/form-context.ts
import { createFormHookContexts } from '@tanstack/solid-form'
export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
// src/components/text-field.tsx
import { useFieldContext } from '../hooks/form-context.tsx'
export default function TextField(props: { label: string }) {
const field = useFieldContext<string>()
return (
<label>
<div>{props.label}</div>
<input
value={field().state.value}
onChange={(e) => field().handleChange(e.target.value)}
/>
</label>
)
}
// src/components/text-field.tsx
import { useFieldContext } from '../hooks/form-context.tsx'
export default function TextField(props: { label: string }) {
const field = useFieldContext<string>()
return (
<label>
<div>{props.label}</div>
<input
value={field().state.value}
onChange={(e) => field().handleChange(e.target.value)}
/>
</label>
)
}
// src/hooks/form.ts
import { lazy } from 'solid-js'
import { createFormHook } from '@tanstack/solid-form'
const TextField = lazy(() => import('../components/text-fields.tsx'))
const { useAppForm, withForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
TextField,
},
formComponents: {},
})
// src/hooks/form.ts
import { lazy } from 'solid-js'
import { createFormHook } from '@tanstack/solid-form'
const TextField = lazy(() => import('../components/text-fields.tsx'))
const { useAppForm, withForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
TextField,
},
formComponents: {},
})
// src/App.tsx
import { Suspense } from 'solid-js'
import { PeoplePage } from './features/people/form.tsx'
export default function App() {
return (
<Suspense fallback={<p>Loading...</p>}>
<PeoplePage />
</Suspense>
)
}
// src/App.tsx
import { Suspense } from 'solid-js'
import { PeoplePage } from './features/people/form.tsx'
export default function App() {
return (
<Suspense fallback={<p>Loading...</p>}>
<PeoplePage />
</Suspense>
)
}
Dies zeigt den Suspense-Fallback, während die TextField Komponente geladen wird, und rendert dann das Formular, sobald es geladen ist.
Nachdem wir nun die Grundlagen der Erstellung benutzerdefinierter Formular-Hooks behandelt haben, fügen wir alles in einem einzigen Beispiel zusammen.
// /src/hooks/form.ts, to be used across the entire app
const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
function TextField(props: { label: string }) {
const field = useFieldContext<string>()
return (
<label>
<div>{props.label}</div>
<input
value={field().state.value}
onChange={(e) => field().handleChange(e.target.value)}
/>
</label>
)
}
function SubscribeButton(props: { label: string }) {
const form = useFormContext()
return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (
<button disabled={isSubmitting()}>{props.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: (props) => {
return (
<div>
<p>{title}</p>
<props.form.AppField
name="firstName"
children={(field) => <field.TextField label="First Name" />}
/>
<props.form.AppForm>
<props.form.SubscribeButton label="Submit" />
</props.form.AppForm>
</div>
)
},
})
// /src/features/people/page.ts
const Parent = () => {
const form = useAppForm({
...formOpts,
})
return <ChildForm form={form} title={'Testing'} />
}
// /src/hooks/form.ts, to be used across the entire app
const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
function TextField(props: { label: string }) {
const field = useFieldContext<string>()
return (
<label>
<div>{props.label}</div>
<input
value={field().state.value}
onChange={(e) => field().handleChange(e.target.value)}
/>
</label>
)
}
function SubscribeButton(props: { label: string }) {
const form = useFormContext()
return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (
<button disabled={isSubmitting()}>{props.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: (props) => {
return (
<div>
<p>{title}</p>
<props.form.AppField
name="firstName"
children={(field) => <field.TextField label="First Name" />}
/>
<props.form.AppForm>
<props.form.SubscribeButton label="Submit" />
</props.form.AppForm>
</div>
)
},
})
// /src/features/people/page.ts
const Parent = () => {
const form = useAppForm({
...formOpts,
})
return <ChildForm form={form} title={'Testing'} />
}
Hier ist eine Tabelle, die Ihnen bei der Entscheidung hilft, welche APIs Sie verwenden sollten
Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.