2.0.1 • Published 8 months ago

mobx-easy-form v2.0.1

Weekly downloads
-
License
MIT
Repository
github
Last release
8 months ago

MobX Easy Form

Simple and performant form library built with MobX.

Works with React, React Native and any other framework that supports MobX.

Motivation

This library is heavily inspired by Formik. I like Formik, but there are some issues with Formik that MobX Easy Form solves.

1. Performace

Forms can get very complicated. Formik can get slow as the form grows as it re-renders the whole form on every keystroke. There are available optimizations but it's complex to setup and can introduce bugs like a field not rendering when it should.

MobX Easy Form solves this issue with MobX. It makes sure to re-render only the components that need to re-render.

You can see the difference in the following GIF. The number of times each component renders is shown below.

2. Validation limitations

Formik forces you to define the validationScheme and initialValues for all your fields at the same time, which means you can't change the validation logic based on the value of some field.

With MobX Easy Form you define each field separately so you can use one field value to initialize another.

Installation

Installing the dependencies

MobX Easy Form depends on mobx, and if you're using React, you'll also need mobx-react-lite (or mobx-react if you support class components). To install these, run

npm install mobx mobx-react-lite

# if you use yarn
yarn add mobx mobx-react-lite

Installing MobX Easy Form

npm install mobx-easy-form

# if you use yarn
yarn add mobx-easy-form

Quick start with React

import { useForm, useField } from "mobx-easy-form";
import { Observer, observer } from "mobx-react";
import * as yup from "yup";

export default observer(function Form() {
  const form = useForm({
    onSubmit({ values }) {
      console.log("Values:", values);
    },
  });

  const firstName = useField({
    id: "firstName",
    form,
    initialValue: "",
  });

  const lastName = useField({
    id: "lastName",
    form,
    initialValue: "",
  });

  const initials = useField({
    id: "initials",
    form,
    initialValue: "",
    validate(initials) {
      if (
        initials.length === 2 &&
        initials[0] === firstName.state.value?.[0] &&
        initials[1] === lastName.state.value?.[0]
      ) {
        return { error: undefined, parsed: initials };
      }

      return { error: "Wrong initials", parsed: undefined };
    },
  });

  const age = useField<string, number>({
    id: "age",
    form,
    initialValue: "",
    validationSchema: yup.number(),
  });

  return (
    <div>
      <h1>User info</h1>

      <Observer>
        {() => {
          return (
            <div>
              <div>First name</div>
              <input
                value={firstName.state.value}
                onChange={(e) => firstName.actions.onChange(e.target.value)}
                onFocus={() => firstName.actions.onFocus()}
                onBlur={() => firstName.actions.onBlur()}
              ></input>
              <div>{firstName.computed.ifWasEverBlurredThenError}</div>
            </div>
          );
        }}
      </Observer>

      <Observer>
        {() => {
          return (
            <div>
              <div>Last name</div>
              <input
                value={lastName.state.value}
                onChange={(e) => lastName.actions.onChange(e.target.value)}
                onFocus={() => lastName.actions.onFocus()}
                onBlur={() => lastName.actions.onBlur()}
              ></input>
              <div>{lastName.computed.ifWasEverBlurredThenError}</div>
            </div>
          );
        }}
      </Observer>

      <Observer>
        {() => {
          return (
            <div>
              <div>Initials</div>
              <input
                value={initials.state.value}
                onChange={(e) => initials.actions.onChange(e.target.value)}
                onFocus={() => initials.actions.onFocus()}
                onBlur={() => initials.actions.onBlur()}
              ></input>
              <div>{initials.computed.ifWasEverBlurredThenError}</div>
            </div>
          );
        }}
      </Observer>

      <Observer>
        {() => {
          return (
            <div>
              <div>Age</div>
              <input
                value={age.state.value}
                onChange={(e) => age.actions.onChange(e.target.value)}
                onFocus={() => age.actions.onFocus()}
                onBlur={() => age.actions.onBlur()}
              ></input>
              <div>{age.computed.ifWasEverBlurredThenError}</div>
            </div>
          );
        }}
      </Observer>

      <Observer>
        {() => {
          return (
            <button
              onClick={form.actions.submit}
              disabled={form.computed.isError && form.state.submitCount > 0}
            >
              SUBMIT ({form.computed.isError ? "invalid" : "valid"})
            </button>
          );
        }}
      </Observer>
    </div>
  );
});

React Example on CodeSandbox

https://codesandbox.io/s/mobx-easy-form-example-xn3ss

React Native Example on Expo Snack

https://snack.expo.dev/@hrastnik/react-native---mobx-easy-form

Getting started

Create a form and define the onSubmit function.

const from = useForm({
  async onSubmit({ values }) {
    console.log(values);
  },
});

Create some fields, and add them to the form by passing the created form to the useField parameter form. We also need an id and initialValue

const firstName = useField({
  id: "firstName",
  form,
  initialValue: "",
});

The id is used to access the field when through the form object.

form.fields.firstName === firstName; // true

You can pass a validation function to useField but MobX Easy Form works really well with yup validation library and it's the recommended approach.

const age = useField<string, number>({
  id: "age",
  form,
  initialValue: "",
  validationSchema: yup
    .number()
    .typeError("Age should be a number")
    .required("Age is required"),
});

Note that we're using TypeScript here and we're passing 2 types to the useField generic. The first type is the type of the input value (the value passed to initialValue) and the second type is the output type that our validation function parses. For example, if you have a text input used to enter age, the input type would be string since it's a text input, and the output type would be number since the validationSchema would parse it to a number.

MobX Easy Form uses the validation function for two things:

  1. To check if the value is in the right format.
  2. To parse the value and convert it to something usable.

In the example above, the field state will hold the string value of a number - e.g. "42", but yup will convert it to a number 42 that we can access through age.computed.parsed and is also the value we get in the onSubmit({ values })

IMPORTANT!

The useForm and useField are React hooks that use createForm and createField and cache the result so that React doesn't re-create the form on each render. In other words useForm and useField make sure the form and fields only get created once. If you want to re-create the fields you can pass a dependency array as the second parameter to useForm and useFields.

Render

We can now render the inputs. A great performance optimization is to use the <Observer> component from mobx-react to limit the renders to only the input that needs to change. Alternatively you can create your own input component with the observer HOC.

Each field has the state,computed and actions props.

<Observer>
  {() => {
    return (
      <div>
        <div>First name</div>
        <input
          value={firstName.state.value}
          onChange={(e) => firstName.actions.onChange(e.target.value)}
          onFocus={() => firstName.actions.onFocus()}
          onBlur={() => firstName.actions.onBlur()}
        ></input>
        <div>{firstName.computed.ifWasEverBlurredThenError}</div>
      </div>
    );
  }}
</Observer>

API

useForm / createForm

parametertyperequireddescription
onSubmit(props: OnSubmitArg) => any;trueFunction that will be ran when form.actions.submitForm is called if the form is valid.

Returns a Form instance.

OnSubmitArg

parametertypedescription
fieldsRecord<string, Field>;An object containing all the fields added to the form. Key's are ids passed to the createField prop.
rawValuesRecord<string, any>;An object containing all the raw values from all the fields in the form.
valuesRecord<string, any>;An object containing all the parsed values (returned from the validate or validated using the validation schema) from all the fields in the form.

useField / createField

parametertyperequireddescription
idstringrequiredUnique identifier for the given field. When you add the field to a form, you can later access the field in form.fields.<id>
initialValueanyrequiredThe initial value of the field. If you're using TypeScript, note that this field will be used to type the values you can set through onChange, so for optional fields consider using a cast to add the undefined type. (initalValue: "" as "" \| undefined)
formForm (return value of createForm)requiredThe form to which the newly created field should be added.
validationSchemaYup.ValidationSchemaoptionalYup validation schema used to validate the field value. You can only set one of validationSchema and validate
validate(value: any) => { error?: undefined, parsed: any } | { error: string, parsed?: undefined }optionalValidation function used to validate and parse the value. It receives the value and must return and object with either error or parsed set
initialErrorstringoptionalInitial error for the field

Returns a Field instance

Field

parametertypedescription
state.errorOverridestring | undefinedCustom error message set on the field
state.idstringid of the field
state.isFocusedbooleantrue if the field is focused, false otherwise
state.valueanyCurrent value of the field
state.wasEverBlurredstringtrue if the field was ever blurred, false otherwise
state.wasEverFocusedstringtrue if the field was ever focused, false otherwise
computed.errorstring | undefinedCustom error message set on the field
computed.ifWasEverBlurredThenErrorstring | undefinedif the field was ever blurred returns the value of computed.error, otherwise returns undefined
computed.ifWasEverFocusedThenErrorstring | undefinedif the field was ever focused returns the value of computed.error, otherwise returns undefined
computed.isDirtybooleantrue if value is different from initialValue, false otherwise
computed.parsedanyParsed value of the field. The parsed value returned by validate or cast using the validation schema
actions.onBlur() => voidSets state.isFocused to false. Should be passed to text inputs onBlur
actions.onChange(value: any) => voidSets state.value to the provided value. Additionally, it clears the errorOverride if it was set
actions.onFocus() => voidSets state.isFocused to true. Should be passed to text inputs onFocus
actions.setError(string | undefined) => voidSets state.errorOverride to the provided value

Form

parametertypedescription
state.isSubmittingbooleantrue if the form is submitting, when the onSubmit function is async
state.submitCountnumberNumber of times the form was submitted
state.valuesAtLastSubmitstring | undefinedUsed internally. You shouldn't use this
computed.errorListstring | undefinedAn array of errors for all the fields
computed.isChangedSinceLastSubmitbooleantrue if any field has changed since last submit, false otherwise
computed.isDirtybooleantrue if any field is dirty (value different from initialValue)
computed.isErrorbooleantrue if any field has error different from undefined
computed.isValidbooleantrue if isError is false, false otherwise
computed.valueListany[]An array of all the values. Used internally, you shouldn't use this.
actions.add(field: Field) => voidAdds the provided field to the form
actions.submit(value: any) => PromiseSets isSubmitting to true, increases submitCount, for each field, sets the wasEverFocused and wasEverBlurred to true. Then, if isError is true does nothing, otherwise runs the onSubmit function
2.0.1

8 months ago

2.0.0

1 year ago

1.3.0

2 years ago

1.2.0

2 years ago

1.2.2

2 years ago

1.2.1

2 years ago

1.1.0

2 years ago

1.0.9

2 years ago

1.0.8

2 years ago

1.0.7

2 years ago

1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

2 years ago

1.0.3

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago