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

Project Lead

Tasks

    No tasks yet

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 or getControlProps.

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.