admin管理员组文章数量:1356464
For no reason (I think), when I put a my validate
method in my useEffect dependencies, that cause a loop ONLY in test because apparently my validate
method is reinstantiated => because my isPatternValid
and isRequiredValid
methods are reinstantiated... But I have no idea why they are reinstantiated in my test...
Input.tsx :
import {FC, useCallback, useContext, useEffect, useMemo, useRef, useState} from "react";
import "./Input.scss";
import {useTranslation} from "react-i18next";
import {FormContext, Tooltip} from "@components";
export type DisabledInput = {
readonly hideStyle: boolean,
readonly isDisabled: boolean
}
export type Validation = {
readonly id: string,
readonly type?: "email" | "mobile" | "uri",
readonly required?: boolean
}
type InputProps = {
readonly defaultValue?: string,
readonly className?: string,
readonly disabled?: DisabledInput,
readonly placeholder?: string,
readonly name?: string,
readonly validationType?: Validation
}
export const Input: FC<InputProps> =
({defaultValue = "", className, disabled, placeholder, name = "", validationType}) => {
const {validation, resetCallback} = useContext(FormContext);
const {t} = useTranslation();
const [inputValue, setInputValue] = useState(defaultValue);
const [errMsg, setErrMsg] = useState<Record<string, string>>({});
const [isValidInput, setIsValidInput] = useState<boolean>(true);
const errMsgLength = Object.values(errMsg).length > 0;
const inputRef = useRef<HTMLInputElement>(null);
const errorMessages = useMemo(() => ({email: t("form.badEmail"), mobile: t("form.badMobile"), uri: t("form.badUri")}), [t]);
const patterns = useMemo(() => ({
email: /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/,
mobile: /^\d{10,15}$/,
uri: /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?(localhost|[a-z0-9*]+([-.]{1}[a-z0-9*]+)*\.[a-z*]{2,5})(:[0-9]{1,5})?(\/.*)?$/
}), []);
const isPatternValid = useCallback((value: string) => {
if (!validationType?.type) return true;
const isValid = patterns[validationType.type].test(value) || value === "";
setErrMsg(prev => {
const newInputsValidation = {...prev};
if (isValid) delete newInputsValidation[validationType.type!];
else newInputsValidation[validationType.type!] = errorMessages[validationType.type!];
return newInputsValidation;
});
return isValid;
}, [errorMessages, patterns, validationType?.type]);
const isRequiredValid = useCallback((value: string) => {
if (validationType?.required && value === "") {
setErrMsg(prev => ({...prev, required: t("form.required")}));
return false;
}
setErrMsg(prev => {
const newInputsValidation = {...prev};
delete newInputsValidation["required"];
return newInputsValidation;
});
return true;
}, [t, validationType?.required]);
const validate = useCallback((value: string) => {
const patternIsValid = isPatternValid(value);
const requiredIsValid = isRequiredValid(value);
setIsValidInput(patternIsValid && requiredIsValid);
}, [isRequiredValid, isPatternValid]);
useEffect(() => {
const resetInput = () => {
validate(defaultValue);
setInputValue(defaultValue);
};
resetCallback.subscribe(resetInput);
return () => resetCallback.unsubscribe(resetInput);
}, [resetCallback, defaultValue, validate]);
useEffect(() => {
if (!validationType?.id) return;
validation.subscribe(validationType?.id, isValidInput);
return () => validation.unsubscribe(validationType?.id);
}, [validationType?.id, isValidInput, validation]);
useEffect(() => validate(defaultValue), [defaultValue, validate]);
return (
<div className={`input ${className || ""}`}>
<input
ref={inputRef}
disabled={disabled?.isDisabled}
name={name}
onChange={e => {
validate(e.target.value);
setInputValue(e.target.value);
}}
data-testid="input"
className={`input__field${errMsgLength ? " error" : ""}${disabled?.hideStyle ? " hideDecoration" : " showDecoration"}`}
type="text"
value={inputValue}
placeholder={placeholder}/>
{disabled?.isDisabled && <Tooltip targetRef={inputRef} tooltipContent={inputValue || placeholder}/>}
{(!disabled?.isDisabled && errMsgLength) &&
<>
<i data-testid="errorIcon" className="ri-error-warning-line errorIcon"></i>
<div className="errorMsg">
{Object.values(errMsg).map((value, i) => (
<span key={i}>* {value}</span>
))}
</div>
</>
}
</div>
);
};
Form.tsx :
import {createContext, forwardRef, ReactNode, useCallback, useMemo, useState} from "react";
import {Button} from "@components";
import {useTranslation} from "react-i18next";
import "./Form.scss";
type FormContext = {
validation: {
subscribe: (name: string, isValid: boolean) => void,
unsubscribe: (name: string) => void
}
resetCallback: {
subscribe: (callback: () => void) => void,
unsubscribe: (callback: () => void) => void
}
}
type FormProps = {
children: ReactNode,
onSubmit: (formData: FormData) => Promise<void>,
onCancel?: () => void,
showBtn?: boolean,
className?: string
}
export const FormContext = createContext<FormContext>({
validation: {
subscribe: () => {},
unsubscribe: () => {}
},
resetCallback: {
subscribe: () => {},
unsubscribe: () => {}
},
});
export const Form = forwardRef<HTMLFormElement, FormProps>(({
children,
onCancel,
onSubmit,
showBtn = true,
className
}, ref
) => {
const {t} = useTranslation();
const [inputsValidation, setInputsValidation] = useState<Record<string, boolean>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [resetCallbacks, setResetCallbacks] = useState<(() => void)[]>([]);
const validation = useMemo(() => ({
subscribe: (id: string, isValid: boolean) => setInputsValidation(prev => ({ ...prev, [id]: isValid })),
unsubscribe: (id: string) =>
setInputsValidation(inputsValidation => {
const newInputsValidation = { ...inputsValidation };
delete newInputsValidation[id];
return newInputsValidation;
})
}), []);
const allInputsValid = Object.values(inputsValidation).every(Boolean);
const resetCallback = useMemo(() => ({
subscribe: (callback: () => void) => setResetCallbacks(prev => [...prev, callback]),
unsubscribe: (callback: () => void) => setResetCallbacks(prev => prev.filter(cb => cb !== callback))
}), []);
const triggerReset = useCallback(() => resetCallbacks.forEach(callback => callback()), [resetCallbacks]);
return (
<FormContext.Provider value={{validation, resetCallback}}>
<form data-testid="form" ref={ref} className={`form ${className || undefined}`} onSubmit={e => {
e.preventDefault();
setIsSubmitting(true);
onSubmit(new FormData(e.target as HTMLFormElement))
.then(() => setIsSubmitting(false));
}}>
{children}
{showBtn &&
<div className="form__formActions">
{onCancel &&
<Button className="form__formActions__cancel" disabled={isSubmitting}
onClick={() => {
onCancel();
triggerReset();
}}>{t("form.cancel")}</Button>}
<Button className="form__formActions__submit" disabled={isSubmitting || !allInputsValid}
isLoading={isSubmitting} type="submit">
{t("form.validate")}
</Button>
</div>
}
</form>
</FormContext.Provider>
);
});
My test :
import {describe, expect, it, vi} from "vitest";
import {Form, FormContext, Input} from "@components";
import {fireEvent, render, screen, waitFor} from "@testing-library/react";
import {FC, useContext, useEffect} from "react";
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
const onCancelMock = vi.fn();
const resetCallbackMock = vi.fn();
const FormTest: FC<{isValid: boolean}> = ({isValid}) => (
<Form onSubmit={() => onSubmitMock()} onCancel={() => onCancelMock()}>
<Input name="test-input1" {...(!isValid && {validationType: {id: "test-id", required: true}})}/>
<Input name="test-input2"/>
<Input name="test-input3"/>
<FormChild/>
</Form>
);
const FormChild: FC = () => {
const {resetCallback} = useContext(FormContext);
useEffect(() => {
resetCallback.subscribe(resetCallbackMock);
return () => resetCallback.unsubscribe(resetCallbackMock);
}, [resetCallback]);
return <></>;
};
describe("<Form/>", () => {
it("shouldn't call submit method if form is invalid", async () => {
render(<FormTest isValid={false}/>);
const validateBtn = screen.getByText("form.validate");
fireEvent.click(validateBtn);
await waitFor(() => {
expect(validateBtn).toBeDisabled();
expect(onSubmitMock).not.toBeCalled();
});
});
});
本文标签: reactjsInfinite loop only when I test my React componentStack Overflow
版权声明:本文标题:reactjs - Infinite loop only when I test my React component - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1744048545a2581991.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论