Scoping for powerful abstractions
A core feature (and one of the most powerful features) of RVF is the ability to scope your form down to just one part of your data. This gives us the ability to create flexible abstractions for whole subforms or even just a single field.
Subform example
To demonstrate this, let's create a simple form for entering people's names and emails. If you want to skip to the final code, go ahead and check out the code in this demo.
Demo form
Using scope
When you call useForm
, it returns a FormApi
object.
The type of this object takes a single generic parameter, which is the type of the form's default values.
// `form` has the type `FormApi<{ foo: string }>`
const form = useForm({
defaultValues: { foo: "bar" },
// ...etc
});
One of the methods on FormApi
is scope
.
When you call scope
and pass it the name of a field, it returns a FormScope
that's been scoped to that field.
You can even continue chaining scope
calls to get even deeper, which is useful for deeply nested or recursive data.
Once you have a FormScope
, you can pass it around to any component that needs it.
But in order to actually use it, you need to pass it to a useFormScope
, useField
, or useFieldArray
hook.
const form = useForm({
defaultValues: {
foo: "foo",
bar: { baz: "baz" }
},
// ...etc
});
const fooScope = form.scope("foo");
// `fooScope` has the type `FormScope<string>`
const barScope = form.scope("bar");
// `barScope` has the type `FormScope<{ baz: string }>`
const bazScope = barScope.scope("baz");
// `barScope` has the type `FormScope<string>`
Typesafe text input
Scoping allows us to encapsulate input logic in a typesafe way. For this example, let's create an text input component that:
- Is guranteed by the types to have a
string
value. - Automatically shows the validation error message if there is one.
- Can be used without having to think about whether to use
getInputProps
orgetControlProps
.
We can do this by accepting a FormScope<string>
as a prop
and passing it to useField
.
type MyInputProps = {
scope: FormScope<string>;
label: string;
};
const MyInput = ({ scope, label }: MyInputProps) => {
const field = useField(scope);
return (
<div>
<label>
{label}
<input {...field.getInputProps()} />
</label>
{/* There's more work to be done here for accesibility,
but we're keeping it simple for now. */}
{field.error() && <span>{field.error()}</span>}
</div>
);
}
This particular input component can only handle text inputs,
but it's also possible to create a typesafe input component that can handle any
input
type. Checkout the typesafe input component recipe for more details.