I am using custom register in react-hook-form, and I am unable to get formState.isValid to be true when I enter text into the input (and so satisfy the required condition).
Here is an example code:
interface FormValues {
readonly value: string;
}
export default function App() {
console.log("rendering");
const form: UseFormMethods<FormValues> = useForm<FormValues>({
mode: "onChange",
reValidateMode: "onChange",
defaultValues: { value: "" }
});
useEffect(() => {
form.register({ name: "value" }, { required: true });
}, [form, form.register]);
const { isValid } = form.formState;
const value = form.watch("value");
return (
<div>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
form.setValue("value", e.target.value);
}}
/>
<div>IsValid: {JSON.stringify(isValid)}</div>
<div>Errors: {JSON.stringify(form.errors)}</div>
<div>Value: {JSON.stringify(value)}</div>
</div>
);
}
The question is sepcifically for this type of register use, not other types (ref or Controller).
Here is a full example.
Does someone know why this is the case, what am I missing?
Additionally, but this is less relevant - does anyone know why rendering is triggered twice for each input change?
EDIT
After discussion with Dennis Vash, there was some progress on this issue, but it is still not resolved.
The docs at https://react-hook-form.com/api/#setValue actually do specify an option for triggering validation:
(name: string, value: any, shouldValidate?: boolean) => void
You can also set the shouldValidate parameter to true in order to trigger a field validation. eg: setValue('name', 'value', true)
At the time when I am writing this, the docs refer to version 5 of react-form-hook, I am actually using 6.0.0-rc.5 so the signature changed a bit to something similar to the following:
(name: string, value: any, { shouldValidate: boolean; shouldDirty: boolean; }) => void
However, in an example that I have when using shouldValidate: true, I get an infinite loop:
interface FormValues {
readonly value: string;
}
export default function App() {
console.log("rendering");
const form: UseFormMethods<FormValues> = useForm<FormValues>({
mode: "onChange",
reValidateMode: "onChange",
defaultValues: { value: "" }
});
useEffect(() => {
form.register({ name: "value" }, { required: true, minLength: 1 });
}, [form, form.register]);
const { isValid } = form.formState;
const value = form.getValues("value");
return (
<div>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
form.setValue("value", e.target.value, {
shouldValidate: true
});
}}
/>
<div>IsValid: {JSON.stringify(isValid)}</div>
<div>Errors: {JSON.stringify(form.errors)}</div>
<div>Value: {JSON.stringify(value)}</div>
</div>
);
}
The loop is occurring when isValid is true, but stops when it is false.
You can try it out here. Entering a key will start the continuous re-rendering, and clearing the input will stop the loop...
Referring to https://github.com/react-hook-form/react-hook-form/issues/2147
You need to set the mode to onChange or onBlur
const { register, handleSubmit, formState: { errors, isDirty, isValid }} = useForm({ mode: 'onChange' });
In this case, the 'isValid' will work as expected.
The component rendered twice because of React.StrictMode.
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions: ...
Also, in order to validate you need to submit the form (try pressing enter), if you on onChange mode without using a reference, you should use triggerValidation instead.
export default function App() {
const {
register,
formState,
watch,
errors,
setValue,
handleSubmit
}: UseFormMethods<FormValues> = useForm<FormValues>();
useEffect(() => {
register(
{ name: "firstName", type: "custom" },
{ required: true, min: 1, minLength: 1 }
);
console.log("called");
}, [register]);
const { isValid } = formState;
const values = watch("firstName");
const onSubmit = (data, e) => {
console.log("Submit event", e);
console.log(JSON.stringify(data));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setValue("firstName", e.target.value);
}}
/>
{/*<button>Click</button>*/}
<div>IsValid: {JSON.stringify(isValid)}</div>
<div>Errors: {JSON.stringify(errors)}</div>
<div>Form value: {JSON.stringify(values)}</div>
</form>
);
}
Related
After inserting text in input and later removing it completely with backspace "false" appears in it. It cannot be removed - after removing last letter it appears again. It appears in input which are rendered using map() method. The same problem doesn't occur in textarea which is not rendered using this method so I guess the problem lays somewhere here, but I dont have idea where.
export default function AddTodo({ active, setActive }: AddTodoProps) {
const { isDarkMode } = useContext(DarkModeContext);
const [todoDetails, setTodoDetails] = useState({
task: "",
description: "",
name: "",
deadline: "",
});
const { submitTodo, loading, status } = useAddTodoToDb(todoDetails);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setTodoDetails((prev) => {
return {
...prev,
[e.target.id]: e.target.value || e.target.checked,
};
});
};
useEffect(() => {
if (typeof status == "string")
setTodoDetails({
task: "",
description: "",
name: "",
deadline: "",
});
}, [status]);
return (
{todoInputs.map((item) => {
return (
<StyledLabelAndInput
isDarkMode={isDarkMode}
err={status?.includes(item.id)}
key={item.id}
>
<label htmlFor={item.id}>{item.text}</label>
<input
value={todoDetails[item.id as keyof ITodoDetails]}
type={item.type}
id={item.id}
min={today}
onChange={(e) => handleChange(e)}
/>
</StyledLabelAndInput>
);
})}
<label htmlFor="description">Description (optional)</label>
<textarea
value={todoDetails.description}
id="description"
onChange={(e) =>
setTodoDetails((prev) => {
return {
...prev,
description: e.target.value,
};
})
}
/>
The false value is comming from the input's checked attribute.
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setTodoDetails((prev) => {
return {
...prev,
[e.target.id]: e.target.value || e.target.checked, /* <--- HERE */
};
});
};
You're trying to get the input's value OR checked, and since the input has no value it's returning the checked attribute, which is false by default.
Instead you'll need to check if the item type is checkbox before using the checked attribute.
I have a react-hook-form with a react-select:
When I change the value of react-select, the form becomes both dirty and valid.
The submit button is now enabled and the form can be sent.
Once the form is sent, useForm defaultValue's are reset to the new values
The submit button is disabled again, waiting for a change in the form.
When I change again react-select value, the form is not dirty. The button remains disabled. Yet, the react-select value has changed.
I have tried almost everything: replace onChange by setValue("name", value, {shouldDirty: true}, etc.
Here is the code:
import { useForm, Controller } from "react-hook-form";
import Select from "react-select";
const options = [
{ value: "joe", label: "Joe" },
{ value: "jack", label: "Jack" },
{ value: "averell", label: "Averell" }
];
type Data = { name: string; surname: string };
export default function Form({
defaultValues,
onSend
}: {
defaultValues: Data;
onSend: (data: Data) => void;
}) {
const {
register,
handleSubmit,
reset,
getValues,
control,
formState: { isValid, isDirty }
} = useForm({
mode: "onChange",
defaultValues
});
const onSubmit = (data: Data) => {
onSend(data);
reset(getValues());
console.log("data sent!", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="name"
render={({ field }) => (
<Select
{...field}
value={field.value}
options={options}
onChange={(v) => field.onChange(v)}
/>
)}
/>
<input placeholder="surname" {...register("surname")}/>
<button type="submit" disabled={!isValid || !isDirty}>
submit
</button>
</form>
);
}
How to fix this?
ps: here is also a sandbox: https://codesandbox.io/s/react-hook-form-test-8tdq3?file=/src/FormController.tsx
So the answer is that I needed to add a useEffect to reset the form values after the submission:
useEffect(() => {
if (formState.isSubmitSuccessful) {
reset(getValues());
}
}, [formState, getValues, reset]);
Lately i discovered the react-hook-from plugin which on the first sight seem to be perfect to start using and replace other plugins due to its great performance.
After using the plugin for some really simple forms i came across a complicated form that i wish to handle with the plugin. My form is based on a nested object which has the following structure. (Typescript definition)
type model = {
tag: string;
visible: boolean;
columns?: (modelColumn | model)[];
}
type modelColumn = {
property: string;
}
So in order to handle to handle a n-level nested form i created the following components.
const initialData = {
...
datasource: {
tag: "tag1",
visible: true,
columns: [
{
property: "property",
},
{
property: "property1",
},
{
tag: "tag2",
visible: true,
columns: [
{
property: "property",
},
{
tag: "tag3",
visible: false,
columns: [
{
property: "property",
}
]
}
]
},
{
entity: "tag4",
visible: false,
}
],
},
...
}
export const EditorContent: React.FunctionComponent<EditorContentProps> = (props: any) => {
const form = useForm({
mode: 'all',
defaultValues: initialData,
});
return (
<FormProvider {...form}>
<form>
<Handler
path="datasource"
/>
</form>
</FormProvider>
);
}
In the above component the main form is created loaded with initial data. (I provided an example value). The Handler component has the login of recursion with the path property to call nested logic of the form data type. Here is the sample implementation.
...
import { get, isNil } from "lodash";
import { useFieldArray, useForm, useFormContext, useWatch } from "react-hook-form";
...
export type HandlerProps = {
path: string;
index?: number;
...
}
export const Handler: React.FunctionComponent<HandlerProps> = (props) => {
const { path, index, onDelete, ...rest } = props;
const { control } = useFormContext();
const name = isNil(index)? `${path}` :`${path}[${index}]`;
const { fields, append, insert, remove } = useFieldArray({
control: control,
name: `${name}.columns`
});
...
const value = useWatch({
control,
name: `${name}`,
});
...
const addHandler = () => {
append({ property: null });
};
console.log(`Render path ${name}`);
return (
<React.Fragment>
<Row>
<Col>
<FormField name={`${name}.tag`} defaultValue={value.tag}>
<Input
label={`Tag`}
/>
</FormField>
</Col>
<Col>
<FormField defaultValue={value.visible} name={`${name}.visible`} >
<Switch />
</FormField>
</Col>
<Col>
<button onClick={addHandler}>Add Column Property</button>
</Col>
</Row>
{
fields && (
fields.map((field: any, _index: number) => {
if (field.property !== undefined) {
return (
<Column
path={`${name}.columns`}
index={_index}
control={control}
fields={fields}
onDelete={() => remove(_index) }
/>
)
} else {
return (
<Handler
path={`${name}.columns`}
index={_index}
/>
)
}
})
)
}
</React.Fragment>
);
}
Essentially the Handler Component uses the form's context and call itself if it should render a nested object of the form like datasource.columns[x] which the register to useFieldArray to get it's columns. Everything so far works fine. I render the complete tree (if i can say) like object form correctly.
For reference here is the code of the Column Component as also for the formField helper component FormField.
export const Column: React.FunctionComponent<ColumnProps> = (props) => {
const { fields, control, path, index, onDelete } = props;
const value = useWatch({
control,
name: `${path}[${index}]`,
defaultValue: !isNil(fields[index])? fields[index]: {
property: null,
}
});
console.log(`Render of Column ${path} ${value.property}`);
return (
<Row>
<Col>
<button onClick={onDelete}>Remove Property</button>
</Col>
<Col>
<FormField name={`${path}[${index}].property`} defaultValue={value.property}>
<Input
label={`Column Property name`}
/>
</FormField>
</Col>
</Row>
);
}
export type FormFieldProps = {
name: string;
disabled?: boolean;
validation?: any;
defaultValue?: any;
children?: any;
};
export const FormField: React.FunctionComponent<FormFieldProps> = (props) => {
const {
children,
name,
validation,
defaultValue,
disabled,
} = props;
const { errors, control, setValue } = useFormContext();
return (
<Controller
name={name}
control={control}
defaultValue={defaultValue? defaultValue: null}
rules={{ required: true }}
render={props => {
return (
React.cloneElement(children, {
...children.props,
...{
disabled: disabled || children.props.disabled,
value: props.value,
onChange: (v:any) => {
props.onChange(v);
setValue(name, v, { shouldValidate: true });
},
}
})
);
}}
/>
);
}
The problem is when i remove a field from the array. The Handler component re-renders having correct values on the fields data. But the values of useWatch have the initial data which leads to a wrong render with wrong fields being displayed an the forms starts to mesh up.
Is there any advice on how to render such a nested form the correct way. I guess this is not an issue from the react-hook-form but the implementation has an error which seems to cause the problem.
I have this update form for a place and I fetch its data from the backend to add initial inputs in useEffect but I got this error
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I know the problem is related to unmounted the component before update the state but I try many solutions but not working. Anyone have an idea how to fix that
const UpdatePlace = () => {
const placeId = useParams().pId;
const [loadedPlace, setLoadedPlace] = useState();
// const [isLoading, setIsLoading] = useState(true);
const { error, sendRequest, clearError } = useHttpClient();
const [isLoading, formState, inputHandler, setFormData] = useForm(
{
title: {
value: "",
isValid: false,
},
description: {
value: "",
isValid: false,
},
},
true
);
useEffect(() => {
const fetchPlace = async () => {
try {
const res = await sendRequest(`/api/places/${placeId}`);
await setLoadedPlace(res.data.place);
setFormData(
{
title: {
value: res.data.place.title,
isValid: true,
},
description: {
value: res.data.place.description,
isValid: true,
},
},
true
);
} catch (err) {}
};
fetchPlace();
}, [sendRequest, placeId, setFormData]);
if (!loadedPlace && !error) {
return (
<div className="center" style={{ maxWidth: "400px", margin: "0 auto" }}>
<Card>
<h2>No place found!</h2>
</Card>
</div>
);
}
const placeUpdateSubmitHandler = (e) => {
e.preventDefault();
console.log(formState.inputs, formState.isFormValid);
};
return (
<>
{isLoading ? (
<LoadingSpinner asOverlay />
) : error ? (
<ErrorModal error={error} onClear={clearError} />
) : (
<>
<Title label="Update place" />
<form className="place-form" onSubmit={placeUpdateSubmitHandler}>
<Input
element="input"
type="text"
id="title"
label="Update title"
validators={[VALIDATOR_REQUIRE()]}
errorText="please enter valid title"
onInput={inputHandler}
initialValue={loadedPlace.title}
initialValid={true}
/>
<Input
element="textarea"
id="description"
label="Update description"
validators={[VALIDATOR_REQUIRE(), VALIDATOR_MINLENGTH(5)]}
errorText="please enter valid description (min 5 chars) "
onInput={inputHandler}
initialValue={loadedPlace.description}
initialValid={true}
/>
<Button type="submit" disabled={!formState.isFormValid}>
Update place
</Button>
</form>
</>
)}
</>
);
};
You can use useEffect with [] with cleanup function, as it will execute last one like this:
useEffect(() => {
return () => {
console.log('cleaned up');
}
},[])
This error means that your request completes after you have navigated away from that page and it tries to update a component that is already unmounted. You should use an AbortController to abort your request. Something like this should work:
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
const fetchPlace = async () => {
try {
const res = await fetch(`/api/places/${placeId}`, { signal }).then(response => {
return response;
}).catch(e => {
console.warn(`Fetch 1 error: ${e.message}`);
});
await setLoadedPlace(res.data.place);
setFormData(
{
title: {
value: res.data.place.title,
isValid: true,
},
description: {
value: res.data.place.description,
isValid: true,
},
},
true
);
} catch (err) {}
};
fetchPlace();
return () => {
controller.abort();
};
}, [sendRequest, placeId, setFormData]);
Edit: Fix undefined obj key/value on render
The above warning will not stop your component from rendering. What would give you an undefined error and prevent your component from rendering is how you initiate the constant loadedPlace. You initiate it as null but you use it as an object inside your Input initialValue={loadedPlace.title}. When your component tries to do the first render it reads the state for that value but fails to locate the key and breaks.
Try this to fix it:
const placeObj = {
title: {
value: '',
isValid: true,
},
description: {
value: '',
isValid: true,
};
const [loadedPlace, setLoadedPlace] = useState(placeObj);
Always make sure that when you use an object you don't use undefined keys upon render.
I am trying to re render a component when a prop changes. My component uses a custom hook which holds the state of a form. In useEffect whenever the prop called refresh changes I want to re render the page. I have tried countless solutions including making a forceUpdate function and calling it when the prop changes, I tried changing state so that the component should re render, but the information in my for does not clear. Below is my code.
Component:
const CustomerInformationForm = (props) => {
const [triggerRefresh, setTriggerRefresh] = useState();
const initialState = {
name: {
value: "",
isValid: false,
},
phone: {
value: "",
isValid: false,
},
address: {
value: "",
isValid: true,
},
};
const initialValidity = false;
useEffect(() => {
console.log("customerinfo refreshing");
setTriggerRefresh(props.refresh);
}, [props.refresh]);
let [formState, inputHandler] = useForm(
initialState,
initialValidity
);
return (
<div>
<h3>Customer Information</h3>
<form className="customer-information_form">
<Input
id="name"
label="Name"
type="text"
element="input"
validators={[VALIDATOR_REQUIRE()]}
errorText="Please enter a valid name."
onInput={inputHandler}
onChange={props.customerInfo(formState)}
/>
<Input
type="text"
element="input"
id="phone"
label="Phone"
validators={[VALIDATOR_MINLENGTH(8)]}
errorText="Please enter a valid phone number."
onInput={inputHandler}
onChange={props.customerInfo(formState)}
/>
<Input
type="text"
element="input"
id="address"
label="Address"
validators={[]}
onInput={inputHandler}
onChange={props.customerInfo(formState)}
/>
</form>
</div>
);
};
export default CustomerInformationForm;
Custom hook:
import { useCallback, useReducer } from "react";
const formReducer = (state, action) => {
switch (action.type) {
case "INPUT_CHANGE":
let formIsValid = true;
for (const inputId in state.inputs) {
if (!state.inputs[inputId]) {
continue;
}
if (inputId === action.inputId) {
formIsValid = formIsValid && action.isValid;
} else {
formIsValid = formIsValid && state.inputs[inputId].isValid;
}
}
return {
...state,
inputs: {
...state.inputs,
[action.inputId]: { value: action.value, isValid: action.isValid },
},
isValid: formIsValid,
};
case "SET_DATA":
return {
inputs: action.inputs,
isValid: action.formIsValid,
};
default:
return state;
}
};
export const useForm = (initialInputs, initialValidity) => {
const [formState, dispatch] = useReducer(formReducer, {
inputs: initialInputs,
isValid: initialValidity,
});
const inputHandler = useCallback((id, value, isValid) => {
dispatch({
type: "INPUT_CHANGE",
value: value,
isValid: isValid,
inputId: id,
});
}, []);
const setFormData = useCallback((inputData, formValidity) => {
dispatch({
type: "SET_DATA",
inputs: inputData,
formIsValid: formValidity,
});
}, []);
return [formState, inputHandler, setFormData,];
};
Any solutions as to what I could do to get the form to re render empty??
I think the best way is to use the setFormData method returned by your hook (you have omitted it in your CustomerInformationForm component).
Then you can call it in the form, when some condition is met in an effect:
const initialState = useRef({
name: {
value: "",
isValid: false,
},
phone: {
value: "",
isValid: false,
},
address: {
value: "",
isValid: true,
},
});
let [formState, inputHandler, setFormData] = useForm(
initialState,
initialValidity
);
useEffect(() => {
console.log("customerinfo refreshing");
setTriggerRefresh(props.refresh);
setFormData(initialState.current);
}, [props.refresh]);
You can also store the initialState value with useRef, to avoid re-running the effect unnecessarily.