Skip to content

Instantly share code, notes, and snippets.

@jakobo
Last active May 7, 2020 16:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jakobo/c7e44a77be6860f3b1dd88c716cb9a65 to your computer and use it in GitHub Desktop.
Save jakobo/c7e44a77be6860f3b1dd88c716cb9a65 to your computer and use it in GitHub Desktop.
A small (<100 lines) useForm hook
// Copyright 2020 Aibex, Inc <oss@aibex.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
/**
* useForm hook
* Used to make React forms suck a little less, while staying as true
* to React's functional programming + hooks philosophy. Memoized results
* and using refs minimize rerendering. Because your page shouldn't
* be repainting on keypress.
*
* Heavily inspired by react-hook-form, with 99% of the overhead tossed.
*
* - If you need /nested form components/ use react-hook-form
* - If you need /React Class Components/ use Formik
* - If you just want to stop having a half dozen setState calls, welcome
*/
const useForm = () => {
const refs = useRef({});
const validators = useRef({});
const [errors, setErrors] = useState(null);
/**
* Clear all errors in the form
* @type function
*/
const clearErrors = useCallback(() => {
setErrors(null);
}, []);
/**
* Clear a single error in the form by its reference name
* @type function
* @param {String} name the reference name to clear
*/
const clearError = useCallback((name) => {
setErrors((p) => {
delete p[name];
return p;
});
}, []);
/**
* Reset the hook's internal state
* Currently, this clears the errors under the hood, but this
* api is left in case additional cleanup is ever required
* as part of clicking a button of type="reset"
*/
const reset = useCallback(() => {
setErrors(null);
}, []);
/**
* Wrap a function in our hook's submit handler. Used in a form's
* onSubmit= or a submit button's onClick= property
* <form onSubmit={handleSubmit(handlerFn)}>...</form>
* The `handlerFn` has a signature of (data = {}, event). The form's
* submit event will be blocked. If you do not want this behavior,
* pass a literal `false` as the second parameter to handleSubmit.
*
* As part of form submission, any registered validators will be
* ran. If any errors are thrown in validation, the exception's message
* is set into errors.<name>
*
* @type function
* @param {function} fn The onSubmit handler to connect to this hook
* @param {boolean} preventDefault (true) should the form's default onSubmit be stopped
* @returns {function} a function suitable for an onSubmit or onClick event
*/
const handleSubmit = useCallback((fn, preventDefault = true) => {
return async (e) => {
if (preventDefault) {
e.preventDefault();
e.stopPropagation();
}
const data = {};
let valid = true;
for (const k of Object.keys(refs.current)) {
const value = refs.current[k]();
if (validators[k]) {
try {
await validators[k](value);
} catch (e) {
setError(k, e.message);
valid = false;
}
}
data[k] = value;
}
if (valid) {
fn(data, e);
}
};
}, []);
/**
* Sets an error into the hook. Used for manually enabling an error
* @type function
* @param {string} name the reference name associated with the error
* @param {string} error the error to set
*/
const setError = useCallback((name, error) => {
setErrors((p) => {
if (p === null) {
p = {};
}
return {
...p,
[name]: error,
};
});
}, []);
/**
* Register a form component by it's React ref
* This is the most common way to connect elements using this hook. By
* default, the form element must have a name and be an HTML node whose
* value can be read via <HTMLNode>.value.
* @type function
* @param {HTMLElement || ReactRef} ref an HTML Element or React Ref via ref=
*/
const register = useCallback((ref) => {
if (!ref) return;
const name = ref.name;
refs.current[name] = () => ref.value;
}, []);
/**
* Provides a means of manual registration, when refs are unfeasible
* When register() won't work, you can manually register a name/getter
* combination. Since useForm doesn't really care where the value comes
* from, that's left up to the developer via the getter function.
*
* If no `getter` is provided, this function returns a `setter`, enabling
* you to call it when your form component changes in a meaningful way.
*
* @type function
* @param {string} name the reference name to regsiter
* @param {function || null} getter (null) the value retrieval function to use
* @returns {undefined || function} if `getter` is ommitted, returns a function to set the value
*/
const manual = useCallback((name, getter = null) => {
let val = null;
if (!getter) {
getter = () => val;
}
refs.current[name] = getter;
if (!getter) {
return (v) => (val = v);
}
}, []);
/**
* Removes a registered reference
* Refs are by default cleaned up by React, but if you (for whatver reason) need to
* remove a reference yourself, this function will delete the reference from your
* data payload. As a result, any associated validators will also be skipped.
* @type function
* @param {string} name the name of the reference to remove
*/
const unregister = useCallback((name) => {
refs.current[name] = null;
delete refs.current[name];
}, []);
/**
* Completely removes all references and validators by name
* Combines unregister() and removeValidator(), dropping all references. These
* references are normally cleaned up by React when a component unmounts, but
* if you need them, they're here.
* @type function
* @param {string} name the name of the reference to remove
*/
const remove = useCallback(
(name) => {
unregister(name);
removeValidator(name);
},
[unregister]
);
/**
* Connects a validation function for the specified name
* This adds or replaces the existing validator associated with the reference name.
* @type function
* @param {string} name the name of the reference
* @param {function} fn the validator function (may be async/await)
*/
const validate = useCallback((name, fn) => {
validators.current[name] = fn;
}, []);
/**
* Removes a validator associated with a particular reference.
* Refs are by default cleaned up by React, but if you (for whatver reason) need to
* remove a validator yourself, this function will delete the validator.
*/
const removeValidator = useCallback((name) => {
validators.current[name] = null;
delete validators.current[name];
}, []);
// destroy refs & validators on unmount
// this lets React clean things up properly
useEffect(() => {
return () => {
refs.current = {};
validators.current = {};
};
}, []);
// return the memoized paylod to only cause rerenders when errors
// surface (the absolute minimum number of rerenders possible without)
// externalizing the error handling of the hook
return useMemo(() => {
return {
errors,
clearErrors,
clearError,
handleSubmit,
setError,
register,
manual,
unregister,
remove,
validate,
removeValidator,
reset,
};
}, [errors]);
};
export { useForm };
@jakobo
Copy link
Author

jakobo commented May 6, 2020

useForm (a simple useForm hook for React)

Throw this in your hooks folder, get back to productive form making.

This isn't really meant to be a comprehensive solution to React forms. It's a hook that is simplified down to a minimum set of functions common across (most) React form pain. It uses uncontrolled inputs by default, but can be manually connected to controlled inputs if required.

  • You want to use functional components, hooks, and setState for every element is unwieldy
  • You (may) have custom components that (may or may not) use ref/innerRef properly
  • Your forms are simple enough that you don't have deeply nested components resulting in ref / callback passing headaches

Usage

const MyComponent = () => {
  const { handleSubmit, register, errors, reset } = useForm();
  const onSubmit = async (vales, e) => {
    // do what needs to be done. await-friendly
    // values.myText contains the text entered by the user
  };

  return (<form onSubmit={handleSubmit(onSubmit)}>
    {errors && (<div>Oh no! {errors.myText}</div>)}
    <input
      name="myText"
      type="text"
      required
      minLength="10"
      placeholder="type ten characters..."
      ref={register} />
    <button type="submit">Go!</button>
  </form>);
}

API

The majority of time you'll just need register/handleSubmit. If you're using controlled components, you'll want to manually register the reference with const mySetter = manual(name); and call mySetter as the value is changed.

Code above is documented.

What About...?

No. I mean, we could add more stuff, but if you're making things more complex, select a full battle-tested form library. This is a hook for the simple forms where HTML Validation (or lightweight JS validation) is sufficient, you don't have nested form components that require a Context object, and you're about one more useState() + useCallback() away from your functional component feeling needlessly large.

This hook is small enough you can have it both ways.

❤️ The OSS Team at Aibex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment