reactjstypescriptreact-hooks

React hooks and comparing object references


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?


Solution

  • Destructure 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 {
      // destructure 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;
    }
    

    However, as adsy suggest, since the result is a computed value, you don't need to cause a re-render by using the useState and useEffect combo. It's better to just compute it, and if it's a costly computation, wrap it in useMemo:

    function useRegexValidate(value: string, regex: RegExp, errorMessage: string, deps: any[] = []): ValidationResult {  
      return regex.test(value)
        ? ValidationResult.Valid
        : new ValidationResult(false, errorMessage);
    }
    

    Or wrap with useMemo() if needed:

    function useRegexValidate(value: string, regex: RegExp, errorMessage: string, deps: any[] = []): ValidationResult {
      // destructure the regex to strings
      const { source, flags } = regex;
      
      return useMemo(() => 
        new RegExp(source, flags).test(value)
        ? ValidationResult.Valid
        : new ValidationResult(false, errorMessage)
      , [value, source, flags, errorMessage]);
    }