skip to Main Content

I have fallen into the too-many-rerenders trap while implementing my own hook. The hook itself looked like this:

function useRegexValidate(value: string, regex: RegExp, errorMessage: string, deps: any[] = []): ValidationResult {

    const [result, setResult] = useState<ValidationResult>(ValidationResult.Valid);

    useEffect(() => {

        if (!regex.test(value)) {

            setResult(new ValidationResult(false, errorMessage));
        } else {

            setResult(ValidationResult.Valid);
        }
    }, [value, regex, errorMessage, ...deps])

    return result;
}

It was kind of tricky to find, but the source of error was passing regular expression as RegExp. During every render a separate instance of RegExp object was created, thus causing a re-render, during which another instance was created – and so on.

I solved this problem by passing the regular expression as a string:

function useRegexValidate(value: string, regex: string, errorMessage: string, deps: any[] = []): ValidationResult {

    const [result, setResult] = useState<ValidationResult>(ValidationResult.Valid);

    useEffect(() => {

        let regexObj = new RegExp(regex);

        if (!regexObj.test(value)) {

            setResult(new ValidationResult(false, errorMessage));
        } else {

            setResult(ValidationResult.Valid);
        }
    }, [value, regex, errorMessage, ...deps])

    return result;
}

I guess I could also solve it by not passing the regular expression in-place, but instead creating a constant value (since the expression is unlikely to change), e.g.

const regex: RegExp = /.../;

useRegexValidate(..., regex, ...);

This would hinder user-friendliness of my hook though, because one would need to remember to call it in a specific way not to cause hard to track bug. So that’s definitely not an option.

I don’t really like the solution I chose, because the RegExp object needs to be created every time the validation is performed. Is there a way to somehow teach React to compare specific objects’ contents instead of merely their references?

Is there a better way to fix my initial code?

3

Answers


  1. import { useEffect, useState } from 'react';
    
    const useRegexValidate = (
      value: string,
      regex: RegExp,
      errorMessage: string,
      deps: any[] = [],
    ) => {
      const [result, setResult] = useState(false);
      const regexJSON = JSON.stringify(regex)
      useEffect(() => {
        const regexObj = new RegExp(JSON.parse(regexJSON));
    
        if (regexObj.test(value)) {
          setResult(true);
        }
      }, [value, regexJSON, errorMessage, ...deps]);
      return result;
    };
    export default useRegexValidate;
    

    Usually my solution when I have objects that I want to use as deps is to JSON.stringify them. This way you can keep the type safety of RegExp.

    Login or Signup to reply.
  2. because one would need to remember to call it in a specific way

    Well, of course. This is React, it is a responsibility of the developer to keep track of everything. That is why useMemo, useCallback and the dependency array exist

    You can use a library like yup to help you validate

    If you don’t want to use library, for your problem, you might not need an effect

    const regexValidate = (value, regex) => 
     regex.test(value)
    

    You don’t need effect, you don’t need state, you don’t need custom hook, you don’t even need an utility function. Just validate it in the body of the component const error = regex.test(value) ? '' : errorMessage

    Validating it on each render is probably way more optimized that calling useEffect on each render.

    There is a compiler for react, that I think is not stable yet, but it might help you with the keeping the reference of the regex between the renders

    Login or Signup to reply.
  3. Deconstruct the RegExp to the source and flags properties. Use them as the dependencies for the useEffect, and then rebuild the RegExp:

    function useRegexValidate(value: string, regex: RegExp, errorMessage: string, deps: any[] = []): ValidationResult {
      // deconstruct the regex to strings
      const { source, flags } = regex;
    
      const [result, setResult] = useState<ValidationResult>(ValidationResult.Valid);
    
      useEffect(() => {
        const currentRegex = new RegExp(source, flags); // rebuild the RegExp
        
        if (!currentRegex.test(value)) {
          setResult(new ValidationResult(false, errorMessage));
        } else {
          setResult(ValidationResult.Valid);
        }
      }, [value, source, flags, errorMessage, ...deps]) // use source and flags as dep
    
      return result;
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search