Options
All
  • Public
  • Public/Protected
  • All
Menu

🚨 This project has been archived. I recommend using React Hook Form instead.

React Plough 👨‍🌾

A library to help tend your react fields

yarn add react-plough
npm i react-plough

react-plough is yet another react form library. It prioritises developer experience and aims to reduce user error by providing strongly typed and performant interfaces for react form fields. Unlike existing form libraries, plough does not use react context and does not require any manual type annotation. In other words, it is lightweight and hard to screw up.

plough is 3 to 700x faster than Formik *(see bottom)

Play with the Formik comparison

Check out the Docs


Basic Usage

Plough views fields as the atomic unit of user-input and uses a familiar API, but is designed as a library, not a framework. As such, plough fits around your code, instead of fitting your code around itself. A basic example looks like:

import { useTextField } from "react-plough";

const LoginForm = () => {
  const [nameProps, name] = useTextField({
    validate: (name) =>
      name.value.length === 0 ? "Name must be provided" : undefined,
  });

  const [passwordProps, password] = useTextField({
    validate: (password) =>
      password.value.length === 0 ? "Password is required" : undefined,
  });

  const handleSubmit = () => {
    alert(`${name.value} ${password.value}`);
  };

  return (
    <div>
      <input {...nameProps} />
      <input {...passwordProps} />
      <button onClick={handleSubmit}></button>
    </div>
  );
};

The fields are not dependent on each other and don't use any react context. This means it causes the minimum amount of re-renders, and gradual adoption is simple. In the basic case, plough acts like a glorified useState with some input-specific helpers.

However, forms often exist across several layers of the DOM tree, have lists of fields and have more complicated requirements. As such, opt-in utilities exist to enable complicated use-cases too.


A more realistic example

Bear in mind that all of the below is strongly typed -- no guessing or type annotation required

const form = createForm({
  name: "",
  age: "8", // HTML <input/> numbers are still strings
  friendNames: [""],
});

export const ProfileForm = () => {
  const [nameProps] = form.useName();
  const [ageProps] = form.useAge({
    validate: (val) => (Number(val) < 18 ? "Must be over 18" : undefined),
  });

  return (
    <div>
      <input {...nameProps} />
      <input {...ageProps} type="number" />
      <FriendForm />
    </div>
  );
};

// <name> or <age> won't trigger updates here
export const FriendForm = () => {
  const [friends, friendsActions] = form.useFriendNames();

  const handleSave = () => {
    const [values, meta] = form.collect(); // Includes <name> and <age>
    if (!meta.isDirty) {
      throw new Error("Please add some friends");
    }
    alert(values);
  };

  return (
    <div>
      {friends.map((friend, i) => (
        <div>
          <input {...friend.props} />
          <button onClick={friend.actions.remove}>Delete</button>
        </div>
      ))}
      <button onClick={friendsActions.addItem}>Add friend</button>
      <button onClick={handleSave}>Save</button>
    </div>
  );
};

Check out the other functions exported from plough to see other helpers

Notes on the Formik Comparison and Performance

In most cases, performance won't be that important. But when forms do have performance issues, these issues are often hard to fix and very problematic

While this describes Formik, most of the points here apply to other existing form libraries, as they tend to use the same underlying approach

The Formik comparison evaluated various things like number of sibling component re-renders, time to re-render and time to pull out values upon submission. To do this, many inputs (from 1 to 100,000) were created. Of course, no one will likely ever need 100,000 inputs, but this case emulates the behaviour of, say, having expensive calculations run in only a few components.

Sibling re-renders

Formik causes a re-render of sibling components (within the <Formik/> component) when any field in that component changes. Conversely, plough does not do this. So, in effect Formik renders sibling components potentially infinitely more than plough.

Extracting form values for submission

The disparity between Formik and plough here is surprising. With 100,000 inputs, Formik takes 7000ms to retrieve the values, whereas plough only takes 10!

Input-Re-render delay

Across every scale of evaluation, plough outperformed Formik. While smaller trials showed a difference that is negligible in practice, the larger trials showed the fundamental difference in performance between the two libraries.

Index

Type aliases

Action

Action<T>: { index: number; type: "REMOVE_ITEM" } | { index: number; type: "UPDATE_ITEM"; updates: Partial<State<T>[number]> } | { type: "UPDATE_ITEMS"; updates: Partial<State<T>[number]>[] } | { type: "APPEND_ITEM" } | { data?: Partial<State<T>[number]>; index: number; type: "INSERT_ITEM" } | { index: number; type: "RESET"; value: T } | { type: "RESET_ALL"; values: T[] }

Type parameters

  • T

BaseFieldArrayOptions

BaseFieldArrayOptions<T, E>: { defaultValue: T; initialValue: T[] | T; isRequired?: boolean; checkForErrors: any; checkIfEmpty: any; extractValue: any }

Type parameters

  • T

  • E

Type declaration

  • defaultValue: T
  • initialValue: T[] | T
  • Optional isRequired?: boolean
  • checkForErrors: function
  • checkIfEmpty: function
    • checkIfEmpty(val: T): boolean
    • Parameters

      • val: T

      Returns boolean

  • extractValue: function
    • extractValue(target: EventTarget & E): T
    • Parameters

      • target: EventTarget & E

      Returns T

BaseFieldOptions

BaseFieldOptions<T, E>: { initialValue: T; isRequired?: boolean; checkForErrors: any; checkIfEmpty: any; extractValue: any }

Type parameters

  • T

  • E

Type declaration

  • initialValue: T
  • Optional isRequired?: boolean
  • checkForErrors: function
    • Parameters

      Returns undefined | string

  • checkIfEmpty: function
    • checkIfEmpty(val: T): boolean
    • Parameters

      • val: T

      Returns boolean

  • extractValue: function
    • extractValue(target: EventTarget & E): T
    • Parameters

      • target: EventTarget & E

      Returns T

ExtractArrayHookType

ExtractArrayHookType<T>: T extends "binary" ? typeof useBinaryFieldArray : T extends "file" ? typeof useFileFieldArray : typeof useTextFieldArray

Type parameters

  • T

ExtractDataType

ExtractDataType<V, T>: V extends any[] ? FieldArrayDataType<T> : FieldDataType<T>

Type parameters

  • V

  • T

ExtractHookType

ExtractHookType<V, T>: V extends any[] ? ExtractArrayHookType<T> : ExtractPrimitiveHookType<T>

Type parameters

  • V

  • T

ExtractPrimitiveHookType

ExtractPrimitiveHookType<T>: T extends "binary" ? typeof useBinaryField : T extends "file" ? typeof useFileField : typeof useTextField

Type parameters

  • T

ExtractTypeString

ExtractTypeString<T>: T extends boolean ? "binary" : T extends FileList ? "file" : "string"

Type parameters

  • T

FieldActions

FieldActions<T>: { reset: any; setError: any; setIsFocussed: any; setValue: any; setWasTouched: any }

Type parameters

  • T

Type declaration

  • reset: function
    • reset(toValue?: T): void
    • Parameters

      • Optional toValue: T

      Returns void

  • setError: function
    • setError(error?: string): void
    • Parameters

      • Optional error: string

      Returns void

  • setIsFocussed: function
    • setIsFocussed(focussed: boolean): void
    • Parameters

      • focussed: boolean

      Returns void

  • setValue: function
    • setValue(toValue: T): void
    • Parameters

      • toValue: T

      Returns void

  • setWasTouched: function
    • setWasTouched(touched: boolean): void
    • Parameters

      • touched: boolean

      Returns void

FieldArrayActions

FieldArrayActions<T>: { appendItem: any; insertItem: any; removeItem: any; resetAll: any; validate: any }

Type parameters

  • T

Type declaration

  • appendItem: function
    • appendItem(): void
    • Returns void

  • insertItem: function
    • insertItem(index: number, value?: T): void
    • Parameters

      • index: number
      • Optional value: T

      Returns void

  • removeItem: function
    • removeItem(index: number): void
    • Parameters

      • index: number

      Returns void

  • resetAll: function
    • resetAll(toValues?: T[]): void
    • Parameters

      • Optional toValues: T[]

      Returns void

  • validate: function
    • validate(): void
    • Returns void

FieldArrayData

FieldArrayData<T, E>: { actions: FieldArrayItemActions<T>; meta: FieldMeta<T>; props: FieldProps<T, E> }

Type parameters

  • T

  • E

Type declaration

FieldArrayDataType

FieldArrayDataType<T>: { arrayActions: ReturnType<ExtractArrayHookType<T>>["2"]; arrayMeta: ReturnType<ExtractArrayHookType<T>>["1"]; data: ReturnType<ExtractArrayHookType<T>>["0"] }

Type parameters

  • T

Type declaration

FieldArrayItemActions

FieldArrayItemActions<T>: FieldActions<T> & { remove: any; reset: any }

Type parameters

  • T

FieldArrayMeta

FieldArrayMeta: Omit<FieldMeta<any>, "value" | "isRequired">

FieldArrayOptions

FieldArrayOptions<T>: { initialValue?: T[] | T; isRequired?: boolean; label?: string; transform?: any; validate?: any }

Type parameters

  • T

Type declaration

  • Optional initialValue?: T[] | T
  • Optional isRequired?: boolean
  • Optional label?: string
  • transform: function
    • transform(val: T): T
    • Parameters

      • val: T

      Returns T

  • validate: function

FieldData

FieldData<T, E>: [FieldProps<T, E>, FieldMeta<T>]

Type parameters

  • T

  • E

FieldDataType

FieldDataType<T>: { actions: ReturnType<ExtractPrimitiveHookType<T>>[2]; meta: ReturnType<ExtractPrimitiveHookType<T>>[1]; props: ReturnType<ExtractPrimitiveHookType<T>>[0] }

Type parameters

  • T

Type declaration

FieldMeta

FieldMeta<T>: { error: string | undefined; hasError: boolean; isDirty: boolean; isEmpty: boolean; isFocussed: boolean; isRequired: boolean; value: T; wasTouched: boolean }

Type parameters

  • T

Type declaration

  • error: string | undefined
  • hasError: boolean
  • isDirty: boolean
  • isEmpty: boolean
  • isFocussed: boolean
  • isRequired: boolean
  • value: T
  • wasTouched: boolean

FieldMetaWithoutError

FieldMetaWithoutError<T>: Omit<FieldMeta<T>, "error" | "hasError">

Type parameters

  • T

FieldOptions

FieldOptions<T>: { initialValue?: T; isRequired?: boolean; label?: string; transform?: any; validate?: any }

Type parameters

  • T

Type declaration

  • Optional initialValue?: T
  • Optional isRequired?: boolean
  • Optional label?: string
  • transform: function
    • transform(val: T): T
    • Parameters

      • val: T

      Returns T

  • validate: function
    • Parameters

      Returns undefined | string

FieldProps

FieldProps<T, E>: { key?: string | number; required: boolean; type?: string; value: T; onBlur: any; onChange: any; onFocus: any }

Type parameters

  • T

  • E

Type declaration

  • Optional key?: string | number
  • required: boolean
  • Optional type?: string
  • value: T
  • onBlur: function
    • onBlur(): void | Promise<void>
    • Returns void | Promise<void>

  • onChange: function
    • onChange(e: ChangeEvent<E>): void
    • Parameters

      • e: ChangeEvent<E>

      Returns void

  • onFocus: function
    • onFocus(): void | Promise<void>
    • Returns void | Promise<void>

FieldPropsBinary

FieldPropsBinary<T, E>: FieldProps<T, E> & { checked: T }

Type parameters

  • T

  • E

FileFieldArrayProps

FileFieldArrayProps: Omit<FieldArrayData<FileList | null | undefined, HTMLInputElement>, "props"> & { props: FileFieldProps }

FileFieldProps

FileFieldProps: Omit<FieldProps<FileList, HTMLInputElement>, "value">

InitialValueType

InitialValueType: string | boolean | string[] | boolean[] | never[] | { value: string | boolean | string[] | boolean[] } | { type?: "binary" | "file"; value: never[] } | { type?: "binary" | "file" }

ReducerFunction

ReducerFunction: <T>(state: State<T>, action: Action<T>) => State<T>

Type declaration

State

State<T>: { error?: string; id: string; isFocussed: boolean; value: T; wasTouched: boolean }[]

Type parameters

  • T

Functions

composeForm

  • composeForm<O, K, D>(fields: O): [D, Meta]
  • Extract values and run common checks over many fields

    Type parameters

    • O: {}

    • K: string | number | symbol

    • D: {[ key in string | number | symbol]: O[key] extends any[] ? any[any][number]["value"][] : O[key] extends FieldMeta<any> ? any[any]["value"] : unknown }

    Parameters

    • fields: O

    Returns [D, Meta]

createForm

  • createForm<A, K>(initialValues: A): HooksObject & { collect: () => readonly [{[ key in string]: MetaType[key] extends any[] ? any[any][number]["value"][] : MetaType[key] extends FieldMeta<any> ? any[any]["value"] : unknown }, { hasErrors: boolean; isComplete: boolean; isDirty: boolean; isFocussed: boolean; wasTouched: boolean }]; getData: () => FormData; reset: () => void }
  • Creates a form given initial values and configuration

    Type parameters

    • A: {}

    • K: string | number | symbol

    Parameters

    • initialValues: A

    Returns HooksObject & { collect: () => readonly [{[ key in string]: MetaType[key] extends any[] ? any[any][number]["value"][] : MetaType[key] extends FieldMeta<any> ? any[any]["value"] : unknown }, { hasErrors: boolean; isComplete: boolean; isDirty: boolean; isFocussed: boolean; wasTouched: boolean }]; getData: () => FormData; reset: () => void }

    An object of hooks and form utility functions

Const groupFieldArrays

  • groupFieldArrays<O, K>(groups: O): readonly [(FieldArrayItemData & { key: string })[], FieldArrayMeta, { appendItem: () => void; insertItem: (index: number, values?: {[ key in string | number | symbol]: undefined | O[K][0][number]["meta"]["value"] }) => void; removeItem: (index: number) => void; resetAll: () => void; validate: () => void }]
  • Converts an object of {key: Array<value>} to Array<{key: value}>

    Type parameters

    • O: {}

    • K: string | number | symbol

    Parameters

    • groups: O

    Returns readonly [(FieldArrayItemData & { key: string })[], FieldArrayMeta, { appendItem: () => void; insertItem: (index: number, values?: {[ key in string | number | symbol]: undefined | O[K][0][number]["meta"]["value"] }) => void; removeItem: (index: number) => void; resetAll: () => void; validate: () => void }]

Const reduceArrayMeta

  • reduceArrayMeta<T>(items: T[], initialValues?: any[]): FieldArrayMeta
  • Type parameters

    Parameters

    • items: T[]
    • Optional initialValues: any[]

    Returns FieldArrayMeta

Const squashArrayActions

  • squashArrayActions<A>(actions: A): { appendItem: () => void; removeItem: (index: number) => void; resetAll: () => void; validate: () => void }
  • Type parameters

    Parameters

    • actions: A

    Returns { appendItem: () => void; removeItem: (index: number) => void; resetAll: () => void; validate: () => void }

    • appendItem: () => void
        • (): void
        • Returns void

    • removeItem: (index: number) => void
        • (index: number): void
        • Parameters

          • index: number

          Returns void

    • resetAll: () => void
        • (): void
        • Returns void

    • validate: () => void
        • (): void
        • Returns void

useBaseField

useBaseFieldArray

useBinaryField

useBinaryFieldArray

useFileField

  • Parameters

    • Optional options: FieldOptions<FileList | null | undefined>

    Returns [FileFieldProps, FieldMeta<FileList | null | undefined>]

useFileFieldArray

useTextField

useTextFieldArray

Legend

Generated using TypeDoc