DEV Community

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

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

Corbin Crutchley on February 18, 2023

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 i...
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.