DEV Community

No more tears, handling Forms in React using Formik, part I

Chris Noring on March 20, 2019

Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris 50% if React developers DON'T use a form library, they bu...
Collapse
 
softchris profile image
Chris Noring

Well, I think it's gonna take a long time before Hooks become a thing everyone uses and a lot of it.. I've dug some into Hooks as you can see from one of my other articles.. Also Hook is a new thinng and added to a late version of React so a lot of old React projects wont be able to use it until they upgrade. It's an interesting topic though.. I'm gonna look into it Forms + Hooks I mean :) Thanks for the comment

Collapse
 
nickytonline profile image
Nick Taylor

I definitely like the render props version over the higher-order component (HOC) version. I understand HOCs, but they always seem to hurt my brain.

I know Jared Palmer, creator of Formik, is working on a hooks implementation of Formik. Not sure if it's finished or in beta.

Thread Thread
 
softchris profile image
Chris Noring

Ah cool, looking forward to the hooks version :)

Thread Thread
 
nickytonline profile image
Nick Taylor

Looks like the issue is still open.

Hooks Rewrite #1046

import React, {
  useContext,
  useState,
  createContext,
  createElement,
} from 'react';

const FormikContext = createContext(null);

export function Formik({
  initialValues,
  validate,
  onSubmit,
  validateOnBlur = true,
  validateOnChange = true,
  ...props
}) {
  const [values, updateValues] = useState(initialValues);
  const [errors, updateErrors] = useState({});
  const [touched, updateTouched] = useState({});
  const [submitAttemptCount, updateSubmitAttemptCount] = useState(0);
  const [isSubmitting, updateIsSubmitting] = useState(false);
  const [isValidating, updateIsValidating] = useState(false);

  function validateForm(vals = values) {
    updateIsValidating(true);
    return Promise.resolve(validate ? validate(vals) : {})
      .then(x => x, e => e)
      .then(e => {
        // return or take a callback?
        updateErrors(e);
        updateIsValidating(false);
      });
  }

  function getFieldProps(name, type) {
    return {
      value:
        type === 'radio' || type === 'checkbox'
          ? undefined // React uses checked={} for these inputs
          : values[name],
      onChange(e) {
        e.persist();
        updateValues(
          prevValues => ({ ...prevValues, [name]: e.target.value }),
          () => {
            if (validateOnChange && validate) {
              validateForm();
            }
          }
        );
      },
      onBlur() {
        updateTouched(
          prevTouched => ({ ...prevTouched, [name]: true }),
          () => {
            if (validateOnChange && validate) {
              validateForm();
            }
          }
        );
      },
    };
  }

  async function submitForm() {
    updateTouched(setNestedObjectValues(values, true));
    updateIsSubmitting(true);
    updateSubmitAttemptCount(prev => prev++);
    try {
      await validateForm();
      const errors = await onSubmit(values);
      if (errors) {
        updateErrors(errors);
      }
      updateIsSubmitting(false);
    } catch (errors) {
      updateErrors(errors);
      updateIsSubmitting(false);
    }
  }

  function handleSubmit(e) {
    e.preventDefault();
    submitForm();
  }

  const ctx = {
    values,
    updateValues,
    errors,
    updateErrors,
    touched,
    updateTouched,
    submitAttemptCount,
    updateSubmitAttemptCount,
    isSubmitting,
    updateIsSubmitting,
    isValidating,
    validateOnChange,
    validateOnBlur,
    getFieldProps,
    handleSubmit,
    submitForm,
  };

  return (
    <FormikContext.Provider value={ctx}>
      {props.children}
    </FormikContext.Provider>
  );
}

export function Form(props) {
  const formik = useContext(FormikContext);
  return <form onSubmit={formik.handleSubmit} {...props} />;
}

export function Debug(props) {
  const formik = useContext(FormikContext);
  console.log({ formik });
  return null;
}

// Backwards compatible Field
export function Field({
  component = 'input',
  render,
  children,
  name,
  ...props
}) {
  const { getFieldProps } = useContext(FormikContext);

  const fieldProps = {
    ...getFieldProps(name, props.type),
    ...props,
  };

  if (children && typeof children === 'function') {
    return children(fieldProps);
  }

  if (render && typeof render === 'function') {
    return render(fieldProps);
  }

  return createElement(component, fieldProps, children);
}

/** @private is the given object an Object? */
export const isObject = obj => obj !== null && typeof obj === 'object';

/**
 * Recursively a set the same value for all keys and arrays nested object, cloning
 * @param
 object
 * @param
 value
 * @param
 visited
 * @param
 response
 */
export function setNestedObjectValues(
  object,
  value,
  visited = new WeakMap(),
  response = {}
): T {
  for (let k of Object.keys(object)) {
    const val = object[k];
    if (isObject(val)) {
      if (!visited.get(val)) {
        visited.set(val, true);
        // In order to keep array values consistent for both dot path  and
        // bracket syntax, we need to check if this is an array so that
        // this will output  { friends: [true] } and not { friends: { "0": true } }
        response[k] = Array.isArray(val) ? [] : {};
        setNestedObjectValues(val, value, visited, response[k]);
      }
    } else {
      response[k] = value;
    }
  }

  return response;
}
Thread Thread
 
nickytonline profile image
Nick Taylor

Looks like there's an alpha release for Formik with hooks now. 🎉

Thread Thread
 
softchris profile image
Chris Noring

sweet :)

Collapse
 
mahmoudjbor profile image
MahmoudJbor

In the first example, you missed the 'name' property of the input, which might be confusing for others.

value={values.name}
type="text"
placeholder="Name">

Collapse
 
softchris profile image
Chris Noring

thank you Mahmoud.. I've updated that throughout :)

Collapse
 
lthebaultsw profile image
Loïc Thebault • Edited

There is an error on the validation snippet


validate={values => {
let errors = {};
if(!errors.name) {
errors.name = 'Name is required';
}
return errors;
}}

It's should be !values.name instead of !errors.name

Collapse
 
softchris profile image
Chris Noring

of course it should.. thank you :)

Collapse
 
panta82 profile image
panta82

This library is in an uncomfortable spot where it adds some functionality, but you'll still want to create more of a framework around it, tuned for the particular project.

For something like that, I'd always veer on the side of owning the entire feature code (by coding it myself or extracting code from the library's ), rather than having to hack around a black box that doesn't quite fit all my needs.