My React Forms Have Brains... I'm Just Not Sure How Smart They Are

My React Forms Have Brains... I'm Just Not Sure How Smart They Are

·

7 min read

Update

Sorry the syntax highlighting sucks. I guess Hashnode is still "hashing" out its syntax highlighting options.

🎤 (mic drop... I'll be here all week!)


When I first learned how to use React context... I set up global state to manage my websites forms.

🤦

Hey I was just learning and with every project you get better and better.

Why was this a bad idea?

If it was a simple contact form or email capture form... the entire app would be re-rendered because I put my provider at the root level.

I could have moved it down to just the form component... but that's some overkill.

And most of the time for most forms... you don't need to remember the input values.

That's why I created my...

Form Brain Hook

Let me just preface this.

I don't use any form libraries. I'm sure there's something I'm missing but I like my setup.

My hook manages the forms values and... what I call... options.

Options include:

  • initial ...is the input in it's initial state?
  • touched ...is the input touched?

You can add more in here if you wanted. I'm not sure what... but you might have some ideas when you see how I set this up.

Let's build a simple form that collects a users first name and email address.

I am not going to show you my validation function... that will be for another time.

Here's the full hook.

export type InputValue = {
  value: string;
  valid: boolean;
};

export type InputOptions = {
  initial: boolean;
  touched: boolean;
};

export type InputName = "emailAddress" | "firstName";

export type UpdateValueFunction = (
  event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => void;

export type UpdateOptionsFunction = (
  even: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
) => void;

export const useNameAndEmailAddressForm = () => {
    const [firstName, setFirstName] = useState<InputValue>({
    value: "",
    valid: false,
  });

  const [firstNameOptions, setFirstNameOptions] = useState<InputOptions>({
    initial: true,
    touched: false,
  });

  const [emailAddress, setEmailAddress] = useState<InputValue>({
    value: "",
    valid: false,
  });

  const [emailAddressOptions, setEmailAddressOptions] = useState<InputOptions>({
    initial: true,
    touched: false,
  });

  const [clearData, setClearData] = useState<boolean>(false);

  useEffect(() => {
    if (clearData) {
            setFirstName({
        value: "",
        valid: false,
      });
      setFirstNameOptions({
        initial: true,
        touched: false,
      });
      setEmailAddress({
        value: "",
        valid: false,
      });
      setEmailAddressOptions({
        initial: true,
        touched: false,
      });
      setClearData(false);
    }
  }, [clearData]);

  const updateInputValues: UpdateValueFunction = (
    event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    event.preventDefault();

    const name = event.target.name as InputName;
    const value = event.target.value;

    switch (name) {
        case "firstName": {
            const name = capitalizeName(value)
            const valid = formValidator(name, isRequiredValidationRules);
            setFirstName({
                value: name,
                valid: valid,
            });
            break;
        }
      case "emailAddress": {
        const data = value.toLowerCase();
        const valid = formValidator(data, emailValidationRules);
        setEmailAddress({
          value: data,
          valid: valid,
        });
        break;
      }
      default: {
        throw new Error("You did not handle every input value type.");
      }
    }
  };

  const updateInputOptions: UpdateOptionsFunction = (
    event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    event.preventDefault();

    const name = event.target.name as InputName;

    switch (name) {
        case "firstName": {
            setFirstnameOptions(({ touched }) => {
                return {
                    initial: false,
                    touched: !touched,
                }
            });
            break;
        }
      case "emailAddress": {
        setEmailAddressOptions(({ touched }) => {
          return {
            initial: false,
            touched: !touched,
          };
        });
        break;
      }
      default: {
        throw new Error("You did not exhaust all input types.");
      }
    }
  };

  return {
    setClearData,
    firstName,
    firstNameOptions,
    emailAddress,
    emailAddressOptions,
    updateInputValues,
    updateInputOptions,
  };
};

The Hook Breakdown

export type InputValue = {
  value: string;
  valid: boolean;
};

export type InputOptions = {
  initial: boolean;
  touched: boolean;
};

These are my type definitions for an input value and input options. Just like I mentioned above ⬆️

export type InputName = "emailAddress" | "firstName";

export type UpdateValueFunction = (
  event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => void;

export type UpdateOptionsFunction = (
  even: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
) => void;

I have gotten in the habit of creating types for stings and numbers that different components use.

export type InputName = "emailAddress" | "firstName;

When I create the form that uses this brain... I can import this type and make sure the name I pass in is correct. What can I say... I love autocomplete and I don't like switching between components if I don't have to.

Let me know if there is a performance hit for this... but I can't image there is.

The other two types are the function types for updating the input values and the input options.

Update Values Function

const updateInputValues: UpdateValueFunction = (
    event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    event.preventDefault();

    const name = event.target.name as InputName;
    const value = event.target.value;

    switch (name) {
        case "firstName": {
            const name = capitalizeName(value)
            const valid = formValidator(name, isRequiredValidationRules);
            setFirstName({
                value: name,
                valid: valid,
            });
            break;
        }
      case "emailAddress": {
        const data = value.toLowerCase();
        const valid = formValidator(data, emailValidationRules);

        setEmailAddress({
          value: data,
          valid: valid,
        });
        break;
      }
      default: {
        throw new Error("You did not handle every input value type.");
      }
    }
  };

This is the function that updates the value(s) in state. Remember this is local state and when the form gets unmounted all of this is wiped out.

It looks at the name of the input and updates the correct state based on the input selected.

I love switch statements. I just with JS had half the ability as Swift switch statements.

This function will get placed in the onChange prop of the input field itself.

**Remember those break statements!!!

I always forget one. That's why I throw that error... so I can fix it before launching for real.

Update Options Function

const updateInputOptions: UpdateOptionsFunction = (
    event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    event.preventDefault();

    const name = event.target.name as InputName;

    switch (name) {
        case "firstName": {
            setFirstnameOptions(({ touched }) => {
                return {
                    initial: false,
                    touched: !touched,
                }
            });
            break;
        }
      case "emailAddress": {
        setEmailAddressOptions(({ touched }) => {
          return {
            initial: false,
            touched: !touched,
          };
        });
        break;
      }
      default: {
        throw new Error("You did not exhaust all input types.");
      }
    }
  };

Pretty much the same things is going on here.

I guess I could do all of this in one function but I like to separate it so things don't get too messy.

As you can see, I will write some long names because I want to know what my code is doing.

So...

**Remember that setState or whatever you call it... you know... it's the change function returned from useState...

...it can take a function and that gives you access to the old values... or current values...

I've gotten in the habit of calling it the old values because they are about to be changed... but I guess in the moment... it's actually still the current values.

Anyway...

I take the old touched and I flip it.

I hard code initial as false because it can only be initial once... when the form is first rendered and you haven't touched the input. (Such a pure input)

This updateInputOptions is called with the onFocus and onBlur props of the input.

The Hook Returns...

return {
    setClearData,
    firstName,
    firstNameOptions,
    emailAddress,
    emailAddressOptions,
    updateInputValues,
    updateInputOptions,
};

I can clear the form after it's been submitted... if I don't redirect to an entirely different view.

When I build the inputs... I have access to:

  • The value and whether it's valid. These are tucked away in firstName and emailAddress
  • The initial and the touched states. These are tucked away in the firstNameOptions and emailAddressOptions
  • And the two functions that update everything!

When I use the hook it looks like this...

const {
    firstName,
    firstNameOptions,
    emailAddress,
    emailAddressOptions,
    updateInputValues,
    updateInputOptions,
} = useNameAndEmailAddressForm()

And Bam!

Everything is neatly tucked away.

Your form components stay really clean.

You get some cool functionality because using...

  • value
  • valid
  • initial
  • touched

You can create errors states and more. For example...

const isError = !valid && !touched

I only want to show an error state and message if the input is not valid and it's not touched. If it's touched... the user can still be updating it and it might not stay as not valid.

Here's another...

const isValid = valid && !touched

Remember... valid and touched are actually values tucked in both firstName and emailAddress... this was just for example sake.

How To Use This?

I tend to build a little brain for each form because each brain can handle multiple inputs with the switch statement.

But I guess you could put everything in one giant brain but then it would get confusing really fast.

And it's easy to copy and paste and just change a few things.

Here's A Form I Built Today

My Input States Example

Well... it's actually just a couple inputs for my component library but you can see the initial, touched, error, and valid states and how the inputs change based on this.

And it's all run from my little form brain hook.