Ryne
Ryne

Reputation: 1415

Best practice for validating input while onChange inside React custom hook?

I built a custom hook to handle a form. One thing I'm having trouble with is calling the validation while the input value is changing.

I have four code snippets included. The second and third are just for context to show how the complete custom hook but feel free to skip them as I'm just curious about how to implement similar functionality from snippet 1 in snippet 4.

The reason I want to do this, in addition to calling it on submit, is that if the input value becomes ' ' I would like to display the error message and when a user started typing it would go away.

This was pretty simple when I wasn't using hooks I would just call a validate function after setState like this:

const validate = (name) => {
  switch(name):
    case "username":
      if(!values.username) {
        errors.username = "What's your username?";
      }
      break;
    default:
      if(!values.username) {
        errors.username = "What's your username?";
      }

      if(!values.password) {
        errors.username = "What's your password?";
      }

      break;
}

const handleChange = (e) => {
  let { name, value } = e.target; 

  this.setState({ ...values, 
    [name]: value 
  }, () => this.validate(name)) 
}

So now using react hooks things are not as easy. I created a custom form handler that returns values, errors, handleChange, and handleSubmit. The form handler is passed an initialState, validate function, and a callback. As of now it looks like this:

import useForm from './form.handler.js';
import validate from './form.validation.js';

const schema = { username: "", password: "" }

export default function Form() {
  const { values, errors, handleChange, handleSubmit } = useForm(schema, validate, submit);

  function submit() {
    console.log('submit:', values);
  }

  return (
    <form></form> // form stuff
  )
}

Here's the validation file. It's simple, it just requires values for two fields.

export default function validate(values) {
  let errors = {};

  if(!values.username) {
    errors.username = "What's your username?";
  }

  if(!values.password) {
    errors.password = "What's your password?";
  }

  return errors;
}

Now here is my form handler, where I'm trying to solve this problem. I have been trying different things around calling setErrors(validate(values)) in the useEffect but can't access the input. I'm not sure, but currently, the custom hook looks like this:

import { useState, useEffect, useCallback } from 'react';

export default function useForm(schema, validate, callback) {
  const [values, setValues] = useState(schema),
        [errors, setErrors] = useState({}),
        [loading, setLoading] = useState(false); // true when form is submitting

  useEffect(() => {
    if(Object.keys(errors).length === 0 && loading) {
        callback();
    }

    setLoading(false);
  }, [errors, loading, callback])

  // I see useCallback used for event handler's. Not part of my questions, but is it to prevent possible memory leak?
  const handleChange = (e) => {
    let { name, value } = e.target;

    setValues({ ...values, [name]: value });
  }

  const handleSubmit = (e) => {
    e.preventDefault();

    setLoading(true);
    setErrors(validate(values));
  }

  return { values, errors, handleChange, handleSubmit }
}

Upvotes: 3

Views: 7427

Answers (1)

HMR
HMR

Reputation: 39270

I'm not sure if it's a good idea to set other state (errors) while in a callback to set state (values) so created a code review

As commented; you can set errors while setting values:

const Component = () => {
  const [values, setValues] = useState({});
  const [errors, setErrors] = useState({});
  const onChange = useCallback(
    (name, value) =>
      setValues((values) => {
        const newValues = { ...values, [name]: value };
        setErrors(validate(newValues));//set other state while in a callback
        return newValues;
      }),
    []
  );
  return <jsx />;
};

Or combine values and errors:

const Component = () => {
  const [form, setForm] = useState({
    values: {},
    errors: {},
  });
  const onChange = useCallback(
    (name, value) =>
      setForm((form) => {
        const values = { ...form.values, [name]: value };
        const errors = validate(values);
        return { values, errors };
      }),
    []
  );
  const { errors, values } = form;
  return <jsx />;
};

Upvotes: 1

Related Questions