Skip to content
This repository has been archived by the owner on Jan 12, 2023. It is now read-only.

useFormState custom hook with Yup validation and helpers to reduce form boilerplate #53

Merged
merged 14 commits into from
Sep 18, 2020

Conversation

mturley
Copy link
Collaborator

@mturley mturley commented Sep 17, 2020

Closes #40 (if we decide to go with this approach).

Adds custom hooks useFormField to track a field's state and useFormState to combine those fields into a validated form. Also adds helpers getFormGroupProps and getTextInputProps, as well as helper component ValidatedTextInput, taking advantage of this structure to vastly reduce form boilerplate. Depends on yup for validation, and builds the yup form schema on-the-fly from the individual field schema passed in useFormField calls.

Full TypeScript support; all generic types from the useFormField calls are retained through to the return value of useFormState, and it even enforces that your yup schema is compatible with the field's type.

The motivation for this comes from our experience with Formik in mig-ui. We were running into weird state behavior bugs that were unclear how to fix, and having to slap hacky solutions on them because we didn't have control over how our form state was managed. Form state seems like such a simple problem, at least in our use case, so I think it is worthwhile to have a much simpler, more lightweight and straightforward implementation that we have full control over. We can add features over time as we need them, or we can easily replace it with something else later.

Check out how short the syntax is to declare form types, state variables, and validation, even with conditional sets of fields:

  const providerTypeField = useFormField<ProviderType | null>(
    null,
    yup.mixed().label('Provider type').oneOf(Object.values(ProviderType)).required()
  );

  const vmwareForm = useFormState({
    providerType: providerTypeField,
    name: useFormField('', yup.string().label('Name').min(2).max(20).required()),
    hostname: useFormField('', yup.string().label('Hostname').max(40).required()),
    username: useFormField('', yup.string().label('Username').max(20).required()),
    password: useFormField('', yup.string().label('Password').max(20).required()),
  });

  const cnvForm = useFormState({
    providerType: providerTypeField,
    clusterName: useFormField('', yup.string().label('Cluster name').max(40).required()),
    url: useFormField('', yup.string().label('URL').max(40).required()),
    saToken: useFormField('', yup.string().label('Service account token').max(20).required()),
  });

  const providerType = providerTypeField.value;
  const formValues = providerType === ProviderType.vsphere ? vmwareForm.values : cnvForm.values;
  const isFormValid = providerType === ProviderType.vsphere ? vmwareForm.isValid : cnvForm.isValid;

The structure of the resulting cnvForm (for example) looks like:

{
  fields: {
    providerType: { value, setValue, isTouched, setIsTouched, reset, schema, error, isValid },
    clusterName: { value, setValue, isTouched, setIsTouched, reset, schema, error, isValid },
    url: { value, setValue, isTouched, setIsTouched, reset, schema, error, isValid },
    saToken: { value, setValue, isTouched, setIsTouched, reset, schema, error, isValid },
  },
  values: { providerType, clusterName, url, saToken },
  isValid,
  reset,
  schema,
}

Who needs Formik?

WIP. I want to add async validation support, debouncing and memoization, since it currently does a lot of redundant validation on each render. There will be slight lag on input and blur until I address that.

If we merge this, we should open an issue to move it into lib-ui and write docs. 🎉

@mturley
Copy link
Collaborator Author

mturley commented Sep 17, 2020

@gildub I cherry-picked your commit from master...gildub:yup to get this started 😄 I may have gone insane after all, but I think the result is pretty good!

@konveyor-preview-bot
Copy link

🚀 Deployed Preview: http://konveyor-virt-ui-pr-53-preview.surge.sh

@mturley
Copy link
Collaborator Author

mturley commented Sep 17, 2020

it even enforces that your yup schema is compatible with the field's type.

To elaborate on this, here are some examples.

By passing '' as a default value, TypeScript can infer that you're creating an IFormField<string>, and if you give it a yup.string() schema it's happy:

Screen Shot 2020-09-16 at 10 21 37 PM


But what if we remove the .required() in the yup schema? That means the value could be undefined, right? Yup, it infers IFormField<string | undefined> instead:

Screen Shot 2020-09-16 at 10 24 04 PM


If we wanted to be more explicit with the types, we could pass the field type in with useFormField<string>. But if we do that without a required() on the schema, it knows something's not right:

Screen Shot 2020-09-16 at 10 25 41 PM


Mark it .required() and it's happy:

Screen Shot 2020-09-16 at 10 25 52 PM


or, give it an explicit string | undefined type and it's also happy:

Screen Shot 2020-09-16 at 10 26 44 PM


Let's try declaring a field with an empty string as the initial value, but use a yup.number() schema:

Screen Shot 2020-09-16 at 10 17 24 PM


Change the initial value to a type that matches the schema and it's happy:

Screen Shot 2020-09-16 at 10 19 09 PM


And of course, the returned form state value itself is fully stocked with all the inferred field types:

Screen Shot 2020-09-16 at 10 31 37 PM


Clearly I'm a little too excited. Turns out inferred types and mapped types are really cool.

@ibolton336
Copy link
Contributor

I am beyond stoked on this! Feels good to be able to read the source code and understand it so we can trace any funky behavior back. Inferred types are awesome! I also love how you abstracted away all of the typical form behavior we would expect into your own helper "getTextInputProps" & packaged that all nicely up inside a custom component. This is really cool to see come together. Who needs formik indeed.

@mturley mturley changed the title WIP: useFormState custom hook with Yup validation and helpers to reduce form boilerplate useFormState custom hook with Yup validation and helpers to reduce form boilerplate Sep 17, 2020
@mturley
Copy link
Collaborator Author

mturley commented Sep 17, 2020

@ibolton336 I added memoized schemas and memoized async validation, so this performs a lot better now (no more lag when typing).

I started messing with debouncing the validation (let the user finish typing before running validation), but it was causing a weird effect where if you select a provider type, it sets isTouched on that field immediately but doesn't consider it valid for 50ms, so you'd get a quick flash of an error there that disappears. If you want to see that attempt, it's in mturley@dec9889.

I don't think that's worth investing more time in right now though. It performs fine, that would only become an issue if we added some expensive long-running async validation, we wouldn't want that triggering on every keystroke. There are other solutions we can consider if necessary when that comes up.

I think this is ready to go!

@mturley mturley marked this pull request as ready for review September 17, 2020 22:53
@mturley mturley requested a review from a team September 17, 2020 22:53
gildub
gildub previously approved these changes Sep 18, 2020
Copy link
Contributor

@gildub gildub left a comment

Choose a reason for hiding this comment

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

This looks very good to me. Well done!
Looking forward to use it.
I won't be surprised if it used by others quickly.

@mturley
Copy link
Collaborator Author

mturley commented Sep 18, 2020

I hope so! There may be a possibility of people reacting with "really? another form library?" but we'll see. I think the real value is having full control of it for addressing any future issues.

Copy link
Contributor

@ibolton336 ibolton336 left a comment

Choose a reason for hiding this comment

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

LGTM. Huge win here! Well done.

@ibolton336 ibolton336 merged commit 70690d3 into kubev2v:master Sep 18, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Evaluate form state libraries e.g. Formik, react-final-form, react-hook-form and add validation to our forms
4 participants