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?
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]);
}