DEV Community

Cover image for Formik Works Great; Here's Why I Wrote My Own
Corbin Crutchley
Corbin Crutchley

Posted on

Formik Works Great; Here's Why I Wrote My Own

TL;DR? I made a library to compete with Formik and React Hook Form called "HouseForm". It would mean a lot if you looked at it, gave feedback on it, and maybe gave it a star on GitHub.

If you've looked into form validation with React, you'll likely have heard of Formik. My first run-in with Formik was at a large company I worked; it was already established as the go-to form library for our projects, and I immediately fell in love with it.

My time at this company was in 2019, right before Formik surpassed one million weekly downloads. Thanks to my experience using Formik at this company, I was left with a strong recommendation in favor of the tool for all future React forms usage.

Fast forward to today. I'm leading a front-end team in charge of many React and React Native applications. One such application we inherited was very heavily form-focused. Formik is still the wildly popular, broadly adopted intuitive forms API I used all those years ago.

So, if we loved Formik, why did we not only remove it from our projects but replace it with a form library of our own?

I think this question is answered by taking a look at the whole story:

  • Why is Formik great?
  • Why don't we want to use Formik?
  • What can be improved about Formik?
  • What alternatives are there?
  • Why did we make our own form library?
  • How does our own form library differ?
  • How did we write it?
  • What's next?

Why is Formik great?

Let's take a step back from Formik for a second. I started web development in 2016 with the advent of Angular 2. While it has its ups and downs, one of its strengths is in its built-in abilities to do form validation - made only stronger when recent versions of Angular (namely, 14) introduced fully typed forms.

React doesn't have this capability baked in, so during my early explorations into the framework I was dearly missing the ability to do validated forms for more complex operations.

While an Angular form might look something like this:

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, FormsModule, ReactiveFormsModule],
  template: `
  <form [formGroup]="form" (ngSubmit)="onSubmit()" > 
    <label>
      <div>Name</div>
      <input type="text" required minlength="4" formControlName="name">
    </label>
    <button>Submit</button>

    <div *ngIf="form.controls.name.invalid && (form.controls.name.dirty || form.controls.name.touched)">
      <div *ngIf="form.controls.name.errors?.['required']">
        Name is required.
      </div>
      <div *ngIf="form.controls.name.errors?.['minlength']">
        Name must be at least 4 characters long.
      </div>
    </div>
  </form>
  `,
})
export class App {
  form = new FormGroup({
    name: new FormControl('', [Validators.required, Validators.minLength(4)]),
  });

  onSubmit() {
    if (!this.form.valid) return;
    alert(JSON.stringify(this.form.value));
  }
}
Enter fullscreen mode Exit fullscreen mode

The React version (without any libraries) might look something like this:

function runValidationRulesGetErrors(rules, val) {
  return rules.map((fn) => fn(val)).filter(Boolean);
}

export default function App() {
  const [form, setForm] = useState({
    name: { value: '', isTouched: false, isDirty: false },
  });

  const validationRules = {
    name: [
      (val) => (!!val ? null : 'Name is required.'),
      (val) =>
        !!val && val.length >= 4
          ? null
          : 'Name must be at least 4 characters long.',
    ],
  };

  const [errors, setErrors] = useState({ name: [] });

  const runValidation = (name, val) => {
    const errors = runValidationRulesGetErrors(validationRules[name], val);
    setErrors((v) => {
      return {
        ...v,
        [name]: errors,
      };
    });
  };

  const onFieldChange = (name, val) => {
    setForm((v) => {
      return {
        ...v,
        [name]: {
          ...v[name],
          isDirty: true,
          value: val,
        },
      };
    });

    runValidation(name, val);
  };

  const onFieldBlur = (name) => {
    setForm((v) => {
      return {
        ...v,
        [name]: {
          ...v[name],
          isTouched: true,
        },
      };
    });

    runValidation(name, form[name].value);
  };

  const onSubmit = (e) => {
    e.preventDefault();
    alert(JSON.stringify(form));
  };

  return (
    <form onSubmit={onSubmit}>
      <label>
        <div>Name</div>
        <input
          value={form.name.value}
          onChange={(e) => onFieldChange('name', e.target.value)}
          onBlur={() => onFieldBlur('name')}
          type="text"
        />
      </label>
      <button>Submit</button>

      {errors.name.length !== 0 && (form.name.isDirty || form.name.isTouched) && (
        <div>
          {errors.name.map((error) => (
            <div key={error}>{error}</div>
          ))}
        </div>
      )}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

That's a difference of ~50 LOC for the Angular version vs. 90 LOC for the React version.

Clearly something needed changing in the React ecosystem.

How Formik saved the day

Here's the previous React code sample, but this time using Formik:

import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';

const schema = Yup.object().shape({
  name: Yup.string()
    .min(4, 'Name must be at least 4 characters long.')
    .required('Name is required.'),
});

export default function App() {
  return (
    <Formik
      initialValues={{
        name: '',
      }}
      validationSchema={schema}
      onSubmit={(values) => {
        alert(JSON.stringify(values));
      }}
    >
      {({ errors, touched, dirty }) => (
        <Form>
          <label>
            <div>Name</div>
            <Field name="name" />
          </label>
          <button>Submit</button>

          {errors.name && (touched.name || dirty) && <div>{errors.name}</div>}
        </Form>
      )}
    </Formik>
  );
}
Enter fullscreen mode Exit fullscreen mode

'Nough said?

Not only is this example shorter than even the Angular example, but it's significantly easier to follow the flow of what's happening and when. On top of this, we're able to use existing validation logic from the exceedingly popular Yup library to make sure our form follows a consistent schema.

Is it any wonder I fell in love with Formik the first time I used it?

Why don't we want to use Formik?

We've talked a lot about my past with Formik in this article; Fast forward to today. Nowadays, I'm leading a small frontend team in charge of a plethora of applications. One such application we inherited is very heavily form-focused:

An example screenshot showing "Add New Customer" screen on a mobile app. This screen has multiple fields in it for customer data.

This is not a real screenshot from the app, but is a mockup used to reflect how heavily form-heavy it is. We have multiple pages like this in our app; all of which with more fields than are displayed here.

While this kind of application may seem simple at first glance, there's a lot of moving parts to it. Ignoring the other functionality within the app, this type of form page might contain:

  • On blur field formatting
  • Per-field validation type (Some fields validate on field blur, some validate on value change)
  • Detection of if a field is touched or dirty
  • Internationalized error messages

As a result, our hand-written field validation code was quickly getting out-of-hand. Because of the difficulty in maintaining that much complexity by hand, bugs, regressions, and otherwise unexpected behavior started occurring. What's worse; One form page would differ wildly in implementation from another, leading to inconsistent user experience.

While this was okay for a short while; while we were under crunch time and this project was not a high priority - it quickly became a thorn in the side.

As such, I asked one of the engineers on my team to look into React form libraries; pointing them towards Formik as a reference of what I knew existed in the ecosystem.

After a day or two of research that engineer came back: They liked Formik but had some concerns over its maintenance.

See, when they went to the Formik GitHub repository, they noticed the high number of issues and pull requests.

Formik has over 600 issues and nearly 150 open pull requests

When they then realized that its last release date was in 2021 - nearly 2 years ago - they wanted to look into it more:

The latest Formik release was 2.2.9 in 2021

After looking into it more, there were no fewer than three separate issues asking if the project was still under maintenance.

Three different GitHub issues asking if the project is maintained with an answer from the community saying "Short answer: no" and suggesting another library called "React Hook Form"

"No problem," we thought, "surely there must be a community fork of the project."

After some time looking into it, we found a single option: An individual contributor by the name of johnrom hacking away at a version 3.

It's sincerely impressive! While the main v3 PR we linked has 97 commits, John also started working on documentation for this potential v3 release with an additional 76 commits.

Unfortunately, he's made it clear that he's not a maintainer of Formik and admits:

[...] whether my changes are ever merged into Formik itself isn't up to me [...]

- John Rom on May 3, 2021

It was clear to use that it was time to find an alternative to Formik.


I want to be very explicit here; neither Jared nor John owe us anything. Their contributions to the ecosystem are not assured, nor should they be.

Almost all open-source maintainers are unpaid for their work, and it's an immense responsibility to bear the load. You constantly have to keep up with the outside ecosystem changes, manage others' contributions, answer questions, and more. It's exceedingly easy to burn out from a project with such immense loads and little personal return.

I'm very grateful for their work on Formik and admire their engineering capabilities and even their abilities to maintain and upkeep Formik while they did. Their work on Formik should be celebrated, not chastised - even while dormant.


What alternatives are there?

After looking through GitHub issues, forums, and chats, there appears to be two primary alternatives to Formik available today:

While React Final Form initially looked promising - it's only seen 5 commits to the main branch since 2021, and has over 300 issues.

Let's check on the React Hook Form GitHub and see if things are more lively:

A repository with 33.1K stars, commits 2 days ago, and a release 2 days ago.

WOW! Now that's an actively maintained repository!

Looking further into React Hook Form, we found ourselves enjoying the basic examples:

import { useForm } from "react-hook-form";

export default function App() {
  const { register, handleSubmit, watch, formState: { errors } } = useForm();
  const onSubmit = data => console.log(data);

  console.log(watch("example")); // watch input value by passing the name of it

  return (
    /* "handleSubmit" will validate your inputs before invoking "onSubmit" */
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* register your input into the hook by invoking the "register" function */}
      <input defaultValue="test" {...register("example")} />

      {/* include validation with required or other standard HTML validation rules */}
      <input {...register("exampleRequired", { required: true })} />
      {/* errors will return when field validation fails  */}
      {errors.exampleRequired && <span>This field is required</span>}

      <input type="submit" />
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

In particular, we really liked the ability to do per-field validation right inline with the input itself:

import { useForm } from "react-hook-form";

export default function App() {
  const { register, handleSubmit } = useForm();
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName", { required: true, maxLength: 20 })} />
      <input {...register("lastName", { pattern: /^[A-Za-z]+$/i })} />
      <input type="number" {...register("age", { min: 18, max: 99 })} />
      <input type="submit" />
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

This allows us to keep our UI and our validation logic collocated in the same part of the code without having to cross-reference multiple locations of code to see how a field looks and acts.

Unfortunately, as we read deeper into this functionality, we found that it doesn't support Yup or Zod validation. To use either of these tools to validate your fields, you must use a schema object validator to validate the whole form:

import { useForm } from "react-hook-form";
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from "yup";

const schema = yup.object({
  firstName: yup.string().required(),
  age: yup.number().positive().integer().required(),
}).required();

export default function App() {
  const { register, handleSubmit, formState:{ errors } } = useForm({
    resolver: yupResolver(schema)
  });
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName")} />
      <p>{errors.firstName?.message}</p>

      <input {...register("age")} />
      <p>{errors.age?.message}</p>

      <input type="submit" />
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

What's more; we noticed that most of the form examples used HTML and a register function. We were curious how this worked, so we did a bit of a deeper dive into their docs page and found that React Hook Form is, by default, uncontrolled and leaves the state persistence up to the DOM.

While this works okay for web applications, React Native doesn't really support this functionality.

To sidestep this problem, RHF introduces a Controller API that allows you to treat your Fields as render functions:

import { useForm, Controller } from "react-hook-form";
import { TextField, Checkbox } from "@material-ui/core";

function App() {
  const { handleSubmit, control, reset } = useForm({
    defaultValues: {
      checkbox: false,
    }
  });
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="checkbox"
        control={control}
        rules={{ required: true }}
        render={({ field }) => <Checkbox {...field} />}
      />
      <input type="submit" />
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

This works out well and enables you to even use custom field components, but introduces a new set of headaches; There's now multiple ways of building out a field in React Hook Form.

You have to establish social rules within your team about which ways to do things, and potentially introduce abstractions to enforce these rules.

Surely, we can make some improvements overall.

What can be improved about Formik?

Let's take a more focused look at what a large form component with 15 input fields might look like when using Formik. We'll take a look at what one field being rendered might look like:

import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';

const schema = Yup.object().shape({
    firstName: Yup.string()
    .required('First name is required.'),
    middleInitials: Yup.string(),
    lastName: Yup.string()
    .required('First name is required.'),
    email: Yup.string().email('Invalid email address').required('Email is required.'),
    // Imagine there are 15 more fields here
});

export default function App() {
  return (
    <Formik
      initialValues={{
        name: '',
      }}
      validationSchema={schema}
      onSubmit={(values) => {
        alert(JSON.stringify(values));
      }}
    >
      {({ errors, touched, dirty }) => (
        <Form>
          {/* Imagine there are 15 more fields here */}
          <Field name="firstName">
            {({
              field,
              meta,
            }) => (
              <div>
                <label>
                    <div>First Name</div>
                    <input type="text" placeholder="Email" {...field} />
                </label>
                {meta.touched && meta.error && (
                  <div className="error">{meta.error}</div>
                )}
              </div>
            )}
          </Field>
        </Form>
      )}
    </Formik>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here's the challenge though; your schema variable is defined at the top of the component file while your Field's input is towards the bottom of the file; meaning that you have as many lines of code in your component separating out your field's validation logic from the UI rendering behavior. That's two places to look for one concern.

And, if that's ~15 lines of code per field, that's at least 225 lines of code to sift through between the validation logic and the UI rendering, ignoring any additional logic, styling, or layout code.

What's more, Formik appears to have many ways to define a Field and Form. There's:

Some of these handle rendering the UI for you, some don't - meaning some work with React Native while others don't - and some of these components have multiple ways of doing the same thing.

This ability to do the same thing in multiple ways is a problem because it:

  • Introduces required training of what method to use and when internally.
  • Bloats the bundle size by requiring more code to be shipped.
  • Increases maintenance costs of the library.

How can React Hook Form be improved?

As we mentioned in the React Hook Form section, we really liked the ability to do per-field validation using the Controller's rules field. However, the lack of ability to use custom validation logic - either through Zod/Yup usage or with custom functions - make this a non-starter for our use-cases.

Because of this, we run into the same issues we did with Formik's shortcomings; they don't allow you to do custom validation on the Field (or Controller) components themselves.

So, we know the shortcomings of RHF and Formik... What do we do now?

Why did we make our own form library?

Alright, you've read the title; I wrote an alternative library to Formik and React Hook Form called "HouseForm".

The HouseForm website with a docs sidebar and a logo front-and-center

Why?

Well, I took a look at our business' needs, saw that Formik was a better choice for us than React Hook Form, but acknowledged that the maintenance of the library was a blocker for us moving forward with it.

While I'm comfortable with open-source maintenance - I maintain NPM packages that account for 7 million downloads a month - I wanted to see what the level of effort was in creating a library akin to Formik that solved our non-maintenance complaints with the tool.

After a week's worth of work, we found ourselves with a tool that we enjoyed using and solved our production needs.

How does HouseForm differ?

If you looked closely at the previous image of the HouseForm website, you'll see our single-sentence explanation of what makes HouseForm unique:

HouseForm is a field-first, Zod-powered, headless, runtime agnostic form validation library for React.

Let's walk through what that means and what a basic usage of HouseForm looks like.

First, HouseForm is "field-first". You can see this when we demonstrate an example HouseForm form:

import { Field, Form } from "houseform";
import { z } from "zod";

export default function App() {
  return (
    <Form
      onSubmit={(values) => {
        alert("Form was submitted with: " + JSON.stringify(values));
      }}
    >
      {({ isValid, submit }) => (
        <>
          <Field
            name="email"
            onChangeValidate={z.string().email("This must be an email")}
          >
            {({ value, setValue, onBlur, errors }) => {
              return (
                <div>
                  <input
                    value={value}
                    onBlur={onBlur}
                    onChange={(e) => setValue(e.target.value)}
                    placeholder={"Email"}
                  />
                  {errors.map((error) => (
                    <p key={error}>{error}</p>
                  ))}
                </div>
              );
            }}
          </Field>
          <button onClick={submit}>Submit</button>
        </>
      )}
    </Form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, we're using <Field onChangeValidate={}/> to validate the field's value when the user has changed their input. This is a stark difference from Formik's single-object validation schema, because it places the validation logic right inline with the UI rendering.

Second: HouseForm is Zod-powered. Unlike Formik, which uses Yup to do validation, or RHF which requires an external dependency to use Zod; HouseForm relies more heavily on Zod to do its validation. Zod is well-loved by many React developers and seemed like a good choice for validation needs.

Third: HouseForm is headless and runtime agnostic. This means that it runs just as well in React Native, Next.js, or even Ink as it does React for the web. No differing APIs; just use the same components for each of these.

Fourth: HouseForm is flexible. Let's take the previous code sample and add validation to the email field that should run during the form submission.

// ...

<Field
    name="email"
    onChangeValidate={z.string().email("This must be an email")}
    onSubmitValidate={isEmailUnique}
>
    {({ value, setValue, onBlur, errors }) => {
        // ...
    }}
</Field>

// ...

// This is simulating a check against a database
function isEmailUnique(val: string) {
  return new Promise<boolean>((resolve, reject) => {
    setTimeout(() => {
      const isUnique = !val.startsWith("crutchcorn");
      if (isUnique) {
        resolve(true);
      } else {
        reject("That email is already taken");
      }
    }, 20);
  });
}
Enter fullscreen mode Exit fullscreen mode

Notice that, outside of the isEmailUnique logic, the only thing we had to do to add submission validation was add a onSubmitValidate property on the Field component.

With HouseForm, we can even extend this per-field validation logic by adding a onBlurValidate function:

<Field
    name="email"
    onChangeValidate={z.string().email("This must be an email")}
    onSubmitValidate={isEmailUnique}
    onBlurValidate={z.string().min(1, "Your email must have at least one character")}
>
    {({ value, setValue, onBlur, errors }) => {
        // ...
    }}
</Field>
Enter fullscreen mode Exit fullscreen mode

The rules passed to each onXValidate property can differ from one another, even within the same field. This is a super powerful API that allows you to decide:

  • What fields you want validated or not
  • How you want to validate each field
  • At which user interaction you want to validate
  • Which rules you want to validate on a per-interaction basis

Neither RHF or Formik has this capability and flexibility today.

What's next for HouseForm?

HouseForm has a lot going for it today:

Even with all of these goodies, HouseForm isn't perfect and it never will be; No project is ever fully finished in the programming world.

Looking forward, we plan on:

But we need your help! If any of this sounds interesting to you, please help us by:

That's all for now. In the next article I write, I'll be talking about how I built HouseForm using Vite and how you can build your own React library using a similar setup (it's pretty rad).

Happy form building!

Top comments (36)

Collapse
 
ivankleshnin profile image
Ivan Kleshnin • Edited

We definitely need a good library for form management. Not Formik, nor R-H-F fit the bill, in my subjective view. You touch some great points in the article but (aside from code verbosity) I'm concerned about the following. For text inputs onChange validation is very expensive (1 validation per character). onBlur validation requires to switch focuses back and worth, providing a somewhat clunky UX. I typically validate text inputs on debounce, so I'm surprised this approach is not even mentioned in the docs or repo. Though maybe a debounced function can be provided to onChangeValidate – I need to give it a try :)

Collapse
 
crutchcorn profile image
Corbin Crutchley

I believe you should be able to add debounce to onChange pretty trivially, but would have to build a POC for it.

Could you add a GH Discussion or GH issue for this problem so that I can remember to investigate?

github.com/houseform/houseform

Collapse
 
ronnewcomb profile image
Ron Newcomb

Kudos to anyone who wants to make form validation nicer.

I don't have your headless restriction, and am unsure what that entails since I've never used React Native, so take what I say with a bag of salt.

I couldn't use your Field component because I'm on react-bootstrap so I'm already using someone else's Field component, and all the css etc that goes with. That's one thing I notice a lot with form helper libraries is "who gets to use their Field component and who has to dance around that? Is it css-first or js-first?"

I also don't like the function-as-children syntax. I understand it fine but it just looks really busy. I understand this isn't really a concern of yours though.

Related to the above, I would definitely say make a helper for functions of the form (e) => setValue(e.target.value) since your users will be writing that a lot. Make a version of setValue that accepts the whole event. User should only write onChange={setValue} and similar. It would also help cut down on punctuation.

I'm interested in how HouseForm deals with multiple validation errors at once from the same onBlur or whatever. How to write onBlurValidate that checks both email valid, required, belongs to the current domain, and has a non-tomato fruit in it, and shows all four validation errors at once should it need to? Currently it looks set up so only one *validate event can have one error.

Anyway, very nice work.

Collapse
 
crutchcorn profile image
Corbin Crutchley

First; sincerely thank you for providing this feedback. Any feedback provided in a sincere way is worth more than gold.

You mentioned that you don't have my headless restriction, but in the very next line you presented a key reason why we went forward with the headless concept; It enables you to use any CSS or UI library you want with HouseForm.

stackblitz.com/edit/houseform-v1-e...

The above link is HouseForm integrated with React Bootstrap - I'll be adding it to our docs pages shortly - it was a seamless process that took me 10 minutes and I've never used React Bootstrap before. 😄 I didn't have to fiddle with an as key or do some strange for trickery, nothing like that.

As for the function-as-children syntax - I hear you. It's a mixed bag overall for sure, but I'm not convinced yet that it's better than a render={() => {}} API, especially when you can do something exceedingly similar with the children={() => {}} explicit syntax. If you have suggestions of how you'd improve this, let's chat more in a GH issue or GH discussion.

As for the e => setValue trick - I think making a helper function will be detrimental rather than a boon. I do a fair bit of mentorship in programming, and have often come into questions of "how do I get X to work with Y?", and official recommendations throw people off hard when they're not accurate for their specific use-case. While the helper e => setValue() function might work well for React for web or some UI libraries, it won't be universal. This type of non-universal APIs often end up bringing in support tickets and headaches for all involved.

Finally, to talk about your multiple validation errors - we support this right out of the box! This is why we do:

{errors.map(error => <p key={error}>{error}</p>)}
Enter fullscreen mode Exit fullscreen mode

Instead of:

{error && <p>{error}</p>}
Enter fullscreen mode Exit fullscreen mode

If you do any testing and see that this isn't the case, that'd be a bug, which we'd love to learn more about with a GH issue.

Once again, thank you so much for taking the time to write this. If you have any additional thoughts (or replies to the above), let me know.

Collapse
 
crutchcorn profile image
Corbin Crutchley

Super fast update on this: We've launched a docs page outlining how to use HouseForm with external UI libraries. Even used the React Bootstrap example :)

houseform.dev/guides/ui-libraries....

Collapse
 
joshuaamaju profile image
Joshua Amaju

Related to the above, I would definitely say make a helper for functions of the form (e) => setValue(e.target.value) since your users will be writing that a lot. Make a version of setValue that accepts the whole event. User should only write onChange={setValue} and similar. It would also help cut down on punctuation.

They specifically mentioned being environment agnostic, so it wouldn't make sense to have a helper that is fully aware of the event of the environment it's running in.

Collapse
 
husseinkizz profile image
Hussein Kizz

I like zod and that's why I would use houseform and even it being agnostic means I can even use it for server side validation, however I find these validation libraries's syntax always hard to grasp, but in all, nice work though!

Collapse
 
jackmellis profile image
Jack

Really interesting article and next time I'm building a greenfield react app I'll definitely look into this!
I had a very similar jourrney to you in the Vue ecosystem where the form libraries are either unmaintained, too tightly coupled to validation libraries, or have awkward/ugly syntax. The only difference was that I didn't publish my library because I couldn't face the public maintenance commitments of yet another OS project 😅

Collapse
 
joshuaamaju profile image
Joshua Amaju

I never seem to get Formik to work for me, which lead me to roll my own form handling implementation for every project. I've gotten deeply uninterested in other form libraries given I know how to write one myself.

It always irks me when a library unrelated to another library ties its implementation to that library. Great work but I think it's a recipe for disaster/irrelevance.

Collapse
 
crutchcorn profile image
Corbin Crutchley

I'm not sure I follow 😅 HouseForm has zero direct dependencies and only two peer deps: React and Zod (Zod is optional). It doesn't build on top of Formik, and doesn't even use the same API.

I'm unclear how HouseForm would fall under:

when a library unrelated to another library ties its implementation to that library

Collapse
 
joshuaamaju profile image
Joshua Amaju

Isn't the "onChangeValidate" for "Field"s tied to zod. It's particularly expecting a zod validator

Thread Thread
 
crutchcorn profile image
Corbin Crutchley

Not really. It's expecting either:

  1. An object with the shape of {parseAsync} that returns {errors: string[]}
  2. An async function with a rejection of the error

If Zod isn't installed, either will still work. We have docs on this here:

houseform.dev/guides/basic-usage.h...

At the bottom. We could (and will) do better to highlight non-Zod usage in our docs

Thread Thread
 
joshuaamaju profile image
Joshua Amaju

I get that one can write their own custom validator that mimics the zod API. But that just highlights my point, your validation step is explicitly/implicitly tied to zod. Which is one problem I always run into when trying to use form libraries out there.

Hence why I roll my own which doesn't tie me to any particular validation library. I understand that it's hard to create a form library that handles validation without somehow owning the validation step. You don't have to take my opinion seriously, just highlighting my frustration with form libraries.

Thread Thread
 
joshuaamaju profile image
Joshua Amaju • Edited

But looking through the codebase I saw this

((val: T, form: FormInstance<T>) => Promise<boolean>)

in

export function validate<T>(
  val: T,
  form: FormInstance<T>,
  validator: ZodTypeAny | ((val: T, form: FormInstance<T>) => Promise<boolean>)
) {
  const isZodValidator = (validator: any): validator is ZodTypeAny => {
    return validator instanceof Object && validator.parseAsync;
  };
  if (isZodValidator(validator)) {
    return validator.parseAsync(val);
  }
  return validator(val, form);
}
export function getValidationError(error: ZodError | string) {
  const isZodError = (error: any): error is ZodError => {
    return error instanceof Object && error.errors;
  };
  if (isZodError(error)) {
    return error.errors.map((error) => error.message);
  } else {
    return [error];
  }
}
Enter fullscreen mode Exit fullscreen mode

which is more like what I was expecting for form validation not tied to any validation library. So I guess I was wrong

Thread Thread
 
crutchcorn profile image
Corbin Crutchley

Right, this is what I was trying to point out 😅 You can pass a custom function with validation that returns a promise. Helpful when wanting to avoid Zod and/or do async ops

Collapse
 
perkinsjr profile image
James Perkins

Congratulations on such a great library, i have been using it in my own prod apps and really enjoying it.

Collapse
 
crutchcorn profile image
Corbin Crutchley

Thanks so much for the kind words and the support! It was such an awesome surprise to wake up one day to the introduction video of HouseForm on your channel! 🎉

Collapse
 
geovannygil profile image
Geovanny Gil

What an amazing library, I was looking for alternatives to Formik and React Hook Form, I just found this for my personal project I will be using it!

Collapse
 
crutchcorn profile image
Corbin Crutchley

Thanks so much for the kind words! :D

Let me know if you have any feedback as you use it - I'm eager to improve the library as much as possible.

Collapse
 
calag4n profile image
calag4n

Haven't you been through lack of performance with big forms in Formik ?

You didn't mention it and it's the biggest downside I struggle with.
Especially with a lot of codependent fields and even when restricting validation on blur instead of on change events.

Does your lib handle this better ?

Collapse
 
crutchcorn profile image
Corbin Crutchley

You know, you're not the first person I've heard this from. The challenge is that I haven't experienced this, have numbers to the contrary (more on that soon), and - until now - never had anyone provide me specific details about specifically when performance falls on it's face.

That's the bad news. Here's the good news: Performance is a key value for HouseForm. Remember, I'm using it in production for huge forms in our most business critical apps.

We actually have a set of benchmarks that we're developing to make sure we're on-par or beating common alternatives like Formik and React Hook Form (we're adding React Final Forms soon). While we only have three benchmarks currently, it shows that Formik actually outperforms React Hook Form when using RHF's Controller for 1,000 form fields and that HouseForm is within spiting distance of Formik:

Benchmarks for Formik and React Hook Form showing Formik being faster by 1.2x than HouseForm and React Hook Form in most operations with the exception of submitting 1,000 fields, where React Hook Form is 3x slower than Formik and HouseForm

We know 3 benchmarks is a paltry number, we're working on growing it + improving docs as quickly as we can!

That's not all - we're actually still working on HouseForm's performance! We believe we may have a couple of methods we can still use to optimize the performance of the codebase. Some of the other performance improvements we've made previously is to conditionally recalculate the <Form>'s isValid style helper variables if and only if the user is actually using the variables.


That all said, now that you've been able to specifically highlight onBlur and codependant fields as areas of performance concerns with large-scale Formik forms, I will write benchmarks for those functionalities ASAP. I'll send a reply when we have some solid numbers for you.

Collapse
 
calag4n profile image
calag4n

Thanks for the answer. I definitely try Houseform out.

Thread Thread
 
crutchcorn profile image
Corbin Crutchley

Of course! Since my last message, I've added 3 more benchmarks, still looking generally in favor for HouseForm.

That said, I think I've found a possible major pitfall in terms of performance for all of the major libraries.

I've figured out a way to solve a potential performance issue, but the API is a bit wonky and I'm unclear if this is actually a problem or not.

If you're up for it and willing to share some insights as to what your performance problems are with large Formik forms, please let me know. I'd be happy to sit down and diagnose the root causes and solve them in HouseForm (or even in your app without a HouseForm migration!)

My DMs are open: twitter.com/crutchcorn or via Discord on this server: discord.gg/FMcvc6T (@crutchcorn on Discord)

Thread Thread
 
crutchcorn profile image
Corbin Crutchley

@calag4n I just released version 1.3.0 of HouseForm, which includes a quick and easy way to drastically increase the performance of a form's re-render:

houseform.dev/guides/performance-o...

I'm not joking when I say that I've seen 70x performance increase in some specific edgecases in production.

I'd love to hear if this problem helps solve the performance issues you were running into with Formik.

Collapse
 
romeerez profile image
Roman K • Edited

I really like how react-hook-form handles when to validate: initially, it's on blur, then on submit, and if you submitted with mistakes it becomes on change (saying this approximately, it may be a more complex logic). This is the best for UX, so, before a form submit, user is not bothered with error messages, and after non successful submit they are clearly messaged what to change and validation message goes away right after being corrected.

I don't know, maybe your use cases are very different from typical ones, and this way of UX doesn't work for you well, but for me it's hard to imagine why would someone want to set up three different validations on the same input for on change, on blur, and on submit.

Also it's not clear what's bad about defining a whole form schema with Zod, it even gives you properly typed result afterward in the submit handler. Setting validation on each field separately seems to be more cumbersome.

Collapse
 
crutchcorn profile image
Corbin Crutchley

Truth be told, it's just a different philosophy. Our buisness has pretty stringent requirements of what kind of requirements to do and when.

There's nothing wrong with having less control over that flow in exchange for simple to use relatively straightforward UX - just a different set of priorities.

More cumbersome? Maybe? I think most will only use onChange and call it done.

More flexible though? 100%.

What's more, you can still use an object schema and use:

schema.shape.fieldName
Enter fullscreen mode Exit fullscreen mode

In the onXValidate functions in HouseForm.

This is all to say; HouseForm fits our needs well but may not fit everyone's needs. That's OK - we admire and respect the other library's efforts! 😊

Collapse
 
romeerez profile image
Roman K

I think most will only use onChange and call it done.

Don't you think it's a problem with UX? So you just start entering your name or email, and it immediately tells you that name is not long enough or email is not valid.

Developer may call it done, user may be fine with it, but what if your client will see it and will ask to change, but you can't do it as gracefully as with react-hook-form because this library wasn't designed for it.

So I think using onChange as a default option for validation is a mistake, and if client cares about UX they will ask to change this anyway, if client doesn't care of it much than it's just a not as good as it could be.

Of course, UX is subjective and for your case maybe it's better to display error messages right after entering first letter.

Thread Thread
 
crutchcorn profile image
Corbin Crutchley • Edited

because this library wasn't designed for it

I think that's unfair.

This library has the ability to do pretty custom validation schemas - it absolutely was designed to do things like RHF's validation method, but be able to customize beyond that point.

Further, take a look at the example I was thinking of when I said onChange:

frontendscript.com/wp-content/uplo...

This is a pretty common pattern to show the requirements of a password that can be done easily with HouseForm.

HouseForm is absolutely capable of pretty polished form UX without a ton of extra lift. Remember, isTouched and isDirty are absolutely tools of the trade, as is conditional validation logic and more.

Thread Thread
 
crutchcorn profile image
Corbin Crutchley • Edited

Just to showcase that HouseForm can absolutely follow RHF's validation strategy...

Their docs claim that validation works like this:

  1. Wait until onSubmit
  2. After form is submitted, revalidate only onChange

How is this functionally different than:

import * as React from 'react';
import { Field, Form } from 'houseform';
import { z } from 'zod';

export default function App() {
  return (
    <Form
      onSubmit={(values) => {
        alert('Form was submitted with: ' + JSON.stringify(values));
      }}
    >
      {({ isValid, submit, isSubmitted }) => (
        <form
          onSubmit={(e) => {
            e.preventDefault();
            submit();
          }}
        >
          <Field
            name="email"
            onChangeValidate={z.string().min(5, 'Must be 5 characters')}
            onSubmitValidate={isEmailUnique}
          >
            {({ value, setValue, onBlur, errors }) => {
              return (
                <div>
                  <input
                    value={value}
                    onBlur={onBlur}
                    onChange={(e) => setValue(e.target.value)}
                    placeholder={'Email'}
                  />
                  {isSubmitted &&
                    errors.map((error) => <p key={error}>{error}</p>)}
                </div>
              );
            }}
          </Field>
          <button type="submit">Submit</button>
        </form>
      )}
    </Form>
  );
}
Enter fullscreen mode Exit fullscreen mode

stackblitz.com/edit/houseform-v1-e...

?

Thread Thread
 
romeerez profile image
Roman K

Truth is yours, I though it would be a problem, but in your example this looks simple to do.

In case when using RHF, usually no need to customize, but when there is a need I just add the onChange callbacks directly to the input, call onChange from the control of controlled input, and use method of the lib setError to set or remove the error.

You know, it's a must have comments section "why you did it when X exists", but anyway that's great that you've made it, it has a different philosophy and syntax, having an alternative is always for good!

 
crutchcorn profile image
Corbin Crutchley

I mean, I need React support though. Our applications are written in React Native (for various buisness reasons). I'm not sure that "Replace React" is a solution here.

Collapse
 
crutchcorn profile image
Corbin Crutchley

I'm unclear what that would look like in React/JSX. What would you imagine this API to look like?

Collapse
 
skyjur profile image
Ski • Edited

I'm feeling dumbfounded by the examples and their complexity of the syntax that's involved. Barely able to get what is what. Surely there must be a way to write same application logic without giving so much stress on my brains while I'm trying to parse these examples bracket by bracket and understand what it does?

Collapse
 
crutchcorn profile image
Corbin Crutchley

I hear you - the "children as a function" syntax is unfamiliar at first.

The challenge is that our primary goal is to remain headless - no UI elements rendered as part of HouseForm's API.

Moreover, we want to avoid making our API larger to add helper components (see Formik) to make that easier to read - with convos with other devs they're rarely used in production.

One way that you could chose to make the codebase a bit easier to read (in terms of brackets) is:

const EmailComponent = ({setValue, value, onBlur}) => (
    return (<input {/* ... */} />)
)

// ...
<Field children={FieldComponent}/>
Enter fullscreen mode Exit fullscreen mode

We chose to avoid recommending this for the official syntax because:

  • Using children as a direct property is uncommon
    • We could've renamed it to render, but then it suggests we want to add properties like as or component, which we do not for API surface reduction reasons.
  • Your UI and validation logic are split again now.

I know the brackets can be confusing at first glance, but with IDE extensions that change the colors of each bracket pair (or, built in, like VSCode), it helps a lot. Practice with it, and it might even make you more familiar with React's internals and why the syntax is the way it is. 😊

If you have a concrete example for how you think the syntax can be improved (while remaining fully headless), let us know; happy to discuss this further in a GitHub issue.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
skyjur profile image
Ski • Edited

Just saying how it is. You'd be telling lies to your self if you think the syntax involved here is not extremely complicated.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.