react-hook-form: Mapping onChange to a different property - reactjs

I have a radixUI component that takes in an onPressedChange prop. How can I map this to onChange in react-hook-form?
onChange from react-hook-form doesn't accept a custom function that satisfies the typing when I try as per below.
import * as Toggle from "#radix-ui/react-toggle";
import { useForm } from "react-hook-form";
const MyComponent = () => {
const { register, watch, getValues } = useForm();
return (
<div>
<Toggle.Root {...register("basic")} />
<code>watchAll: {JSON.stringify(watch(), null, 2)}</code>
</div>
);
};
I tried spreading it out like below but my functions don't satisfy the onChange typings...
import * as Toggle from "#radix-ui/react-toggle";
import { useForm } from "react-hook-form";
const MyComponent = () => {
const { register, watch, getValues } = useForm();
const { onChange, onBlur, name, ref } = register('basic');
return (
<div>
<Toggle.Root onPressedChange={onChange} onBlur={onBlur} name={name} ref={ref} />
// ^This doesn't work.
// Neither does `onPressedChange={pressed=>onChange(pressed)}`
<code>watchAll: {JSON.stringify(watch(), null, 2)}</code>
</div>
);
};
The onChange prop from react-hook-form accepts the following ChangeHandler:
type ChangeHandler = (event: {
target: any;
type?: any;
}) => Promise<void | boolean>;
and the typing for onPressedChange is:
type OnPressedChangeHandler = (pressed: boolean) => void

One way to map it is to simply use the setValue method from useForm() instead of onChange.
You could use something like this since you know the name of the element, "basic".
const MyComponent = () => {
const { register, watch, setValue } = useForm();
const { onBlur, name, ref } = register("basic");
const onPressedChange = (boolean) => {
setValue("basic", boolean);
};
return (
<div>
<Toggle.Root
onPressedChange={onPressedChange}
onBlur={onBlur}
name={name}
ref={ref}
/>
<code>watchAll: {JSON.stringify(watch(), null, 2)}</code>
</div>
);
};
Working Sandbox:
https://codesandbox.io/p/sandbox/quizzical-spence-ll8mqm

Related

Edit form with custom component for react-hook-form : default value

It's been 3 months I learn ReactJS + TypeScript. My question is about to use react-hook-form (v7) for editing a form. I want to use my custom component that I created and found how to do it by myself !
Here is a part of my form provider with react-hook-form
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import InputText from 'components/commons/form/InputText';
import { supabase } from 'configs/supabase';
const EditEducation: React.FC = () => {
const { educationId } = useParams();
const [education, setEducation] = useState<education>();
const getEducation = async (educationId: string | undefined) => {
try {
const { data, error } = await supabase
.from('tables1')
.select('data1, data2')
.eq('id', educationId)
.single();
if (error) {
seterror(error.message);
}
if (data) {
return data;
}
} catch (error: any) {
alert(error.message);
}
};
useEffect(() => {
getEducation(educationId).then((data) => {
setEducation(data);
});
// eslint-disable-next-line
}, [educationId]);
const methods = useForm();
const onSubmit = async (formData: any) => {
const updateData = {
data1 = formData.data1,
data2 = formData.data2
};
try {
setSaving(true);
const { error } = await supabase.from('educations').update(updateData);
if (error) {
seterror(error.message);
}
if (!error) {
navigate('/experiences/education');
}
setSaving(false);
} catch (error: any) {
seterror(error.message);
}
};
return (
...
<FormProvider {...methods}>
<form className="p-4" onSubmit={methods.handleSubmit(onSubmit)}>
<InputText
id="data1"
label="Data1"
placeholder="Ex: data1"
defaultValue={education?.data1}
options={{ required: 'This field is required' }}
/>
<Button type="submit">{saving ? 'Saving' : 'Save'}</Button>
</form>
</FormProvider>
...
)
};
Here is my custom component :
import React, { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';
interface InputProps {
id: string;
label: string;
placeholder?: string;
defaultValue?: string;
}
const InputText: React.FC<InputProps> = ({
id,
label,
placeholder,
defaultValue,
options,
...rest
}: InputProps) => {
const {
register,
setValue,
formState: { errors }
} = useFormContext();
useEffect(() => {
if (defaultValue) setValue(id, defaultValue, { shouldDirty: true });
}, [defaultValue, setValue, id]);
return (
<div className="">
<label htmlFor={id} className="">
{label}
</label>
<input
type="text"
placeholder={placeholder}
className=""
id={id}
defaultValue={defaultValue}
{...register(id, options)}
{...rest}
/>
{errors[id] && (
<p className="">
<span className="">*</span> {errors[id]?.message}
</p>
)}
</div>
);
};
export default InputText;
As you can see, I had use a formContext because I want to deconstruct my code into smaller components.
Now I'm having some doubts if I correctly code, specialy when I use ut editing forms : if set my default value via "defaultValue" prop, I have to submit (error show) then clique inside the input to change the state in order to clean the error in the input component.
This is why I have add the useEffect hook to clean the input validation error and it's working. What do you think about this ? Is there a better way to manage it (I think Yup it's a cleaner way to set the validation schema) ?
Thanks in advance and sorry for my rusty English. Great day to all and hope my code will help people.
Use <FormProvider {...methods}> and it's working but I do not know if it's a good way to do it.
Edit : In reality, I have to double submit to get my data so I guess it's not the correct way, any sugestions ?
Edit2 : I have found a "solution" : if I have a defaultValue in my props, I do in my component :
useEffect(() => {
if (defaultValue) setValue(id, defaultValue, { shouldDirty: true });
}, [defaultValue, setValue, id]);
I do not think it is the better solution ...
I wrongly edited my previous answer, here is the original:
You should provide default values to useForm, not to your component (so your InputText doesn't need to know about defaultValue or setValue, it will have the correct value thanks to the register method).
To initialize the form, you can do
useForm({ defaultValues: { data1: education?.data1 } });
If the data you use to provide default values is loading after the form is initialized, you can use the reset method (see docs), which I personally put in a useEffect to watch for data update:
const Component: React.FC = ({ defaultValues }) => {
const {
register,
handleSubmit,
reset,
} = useForm({ defaultValues });
useEffect(() => {
reset(defaultValues);
}, [defaultValues, reset]);
return ...
}
On another note, you should define getEducation in the useEffect that calls it, instead of in the component, so that the method isn't declared every time your component is rendered. Snippet:
useEffect(() => {
const getEducation = () => {
...
};
getEducation();
}, [educationId]);
It's been 3 months I learn ReactJS + TypeScript. My question is about to use react-hook-form (v7) for editing a form. I want to use my custom component that I created and found how to do it by myself !
editForm.tsx
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import InputText from 'components/commons/form/InputText';
import { supabase } from 'configs/supabase';
const EditEducation: React.FC = () => {
const { educationId } = useParams();
const [education, setEducation] = useState<education>();
...
const getEducation = async (educationId: string | undefined) => {
try {
const { data, error } = await supabase
.from('tables1')
.select('data1, data2')
.eq('id', educationId)
.single();
if (error) {
seterror(error.message);
}
if (data) {
return data;
}
} catch (error: any) {
alert(error.message);
}
};
useEffect(() => {
getEducation(educationId).then((data) => {
setEducation(data);
});
// eslint-disable-next-line
}, [educationId]);
const methods = useForm();
const onSubmit = async (formData: any) => {
const updateData = {
data1 = formData.data1,
data2 = formData.data2
};
try {
setSaving(true);
const { error } = await supabase.from('educations').update(updateData);
if (error) {
seterror(error.message);
}
if (!error) {
navigate('/experiences/education');
}
setSaving(false);
} catch (error: any) {
seterror(error.message);
}
};
return (
<FormProvider {...methods}>
<form className="p-4" onSubmit={methods.handleSubmit(onSubmit)}>
<InputText
id="data1"
label="Data1"
placeholder="Ex: data1"
defaultValue={education?.data1}
options={{ required: 'This field is required' }}
/>
<Button type="submit">{saving ? 'Saving' : 'Save'}</Button>
</form>
</FormProvider>
)
...
myCustomComponent.tsx
import React, { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';
interface InputProps {
id: string;
label: string;
placeholder?: string;
defaultValue?: string;
}
const InputText: React.FC<InputProps> = ({
id,
label,
placeholder,
defaultValue,
options,
...rest
}: InputProps) => {
const {
register,
setValue,
formState: { errors }
} = useFormContext();
useEffect(() => {
if (defaultValue) setValue(id, defaultValue, { shouldDirty: true });
}, [defaultValue, setValue, id]);
return (
<div className="">
<label htmlFor={id} className="">
{label}
</label>
<input
type="text"
placeholder={placeholder}
className=""
id={id}
defaultValue={defaultValue}
{...register(id, options)}
{...rest}
/>
{errors[id] && (<p className="">
<span className="">*</span> {errors[id]?.message}
</p>)}
</div>
);
};
export default InputText;
As you can see, I had use a formContext because I want to deconstruct my code into smaller components. Now I'm having some doubts if I correctly code, specialy when I use ut editing forms : if set my default value via "defaultValue" prop, I have to submit (error show) then clique inside the input to change the state in order to clean the error in the input component.
This is why I have add the useEffect hook to clean the input validation error and it's working. What do you think about this ? Is there a better way to manage it (I think Yup it's a cleaner way to set the validation schema) ? Thanks in advance and sorry for my rusty English. Great day to all and hope my code will help people.
Edit1 : In reality, I have to double submit to get my data so I guess it's not the correct way, any sugestions ?
Edit2 : I have found a "solution" : if I have a defaultValue in my props, I do in my custom component :
useEffect(() => {
if (defaultValue) setValue(id, defaultValue, { shouldDirty: true });
}, [defaultValue, setValue, id]);
I do not think it is the better solution ...
Edit3 : Thanks #Jérémy Rippert this is my working solution :
editForm.tsx
...
const methods = useForm();
const { reset } = methods;
useEffect(() => {
reset(education);
}, [reset, education]);
return (
<FormProvider {...methods}>
<form className="p-4" onSubmit={methods.handleSubmit(onSubmit)}>
<InputText
id="degree"
label="Degree"
placeholder="Ex: Master 2 - Design & Marketing"
options={{ required: 'This field is required' }}
/>
</form>
</FormProvider>
)
...
myCustomComponent.tsx
...
const InputText: React.FC<InputTextProps> = ({
id,
label,
placeholder,
options
}: InputTextProps) => {
const {
register,
formState: { isDirty, isValid, touchedFields, dirtyFields, errors }
} = useFormContext();
return (
<input
type="text"
placeholder={placeholder}
className={`block w-full rounded-lg border ${
errors[id] ? 'border-red-600' : 'border-gray-600'
} bg-gray-700 p-2.5 text-sm text-white placeholder-gray-400 focus:outline-none`}
id={id}
{...register(id, options)}
/>
)
...
Thanks again #Jérémy Rippert

Can't access forwareded Ref value in parent component React typescript

I have a situation where i have created a component for input. This is a custom component and i want to access the value entered by user in this input in the parent component(where i am using it).
I am forwarding ref from this Input Component but the parent component is receiving the complete input not the value. How can i use the value.
Below is my code.
Input.tsx
interface AuxProps {
id :''
}
const Input = React.forwardRef<HTMLInputElement,AuxProps>((props, ref) => {
return (
<input
id={props.id}
ref = {ref}
defaultValue = '1'
type='number'
></input>
);
});
export default Input;
HeaderComponent.tsx
const HeaderComponent= () => {
const inputAmount = useRef<HTMLInputElement>(null);
const addProductHandle = (event: any) => {
event.preventDefault();
console.log(inputAmount.current.value); //<----Error:- object is possibly null
};
return (
<form className={classes["form"]}>
<Input id="1s" ref={inputAmount}></Input>
<button onClick={addProductHandle}> + ADD </button>
</form>
);
};
export default HeaderComponent;
Not sure how can i use this ref value.
You were close.
Let's take a look on useRef return type:
interface RefObject<T> {
readonly current: T | null;
}
According to this type signature, current property might be T (in our case HTMLInputElement) or null.
This is why you are using typescript - to avoid errors on PROD.
Since current might be null, TS asks you to double check if current exists.
you can add ? or if condition:
import React, { useRef, MouseEventHandler } from 'react'
interface AuxProps {
id: string
}
const Input = React.forwardRef<HTMLInputElement, AuxProps>((props, ref) => {
return (
<input
id={props.id}
ref={ref}
defaultValue='1'
type='number'
></input>
);
});
const HeaderComponent = () => {
const inputAmount = useRef<HTMLInputElement>(null);
const addProductHandle: MouseEventHandler<HTMLButtonElement> = (event) => {
event.preventDefault();
console.log(inputAmount.current?.value); // ok
if (inputAmount.current) {
console.log(inputAmount.current.value); //ok
}
};
return (
<form >
<Input id="1s" ref={inputAmount}></Input>
<button onClick={addProductHandle}> + ADD </button>
</form>
);
};
export default HeaderComponent;
Btw, you can use MouseEventHandler<HTMLButtonElement> for click handlers. See my example

How to call a function via onChange of a TextField Select type from the MaterialUI package

I'm trying to make the onChange of a Select type JTextField, from the MaterialUI package, to trigger a function.
I created the following component called Select, it is a TextField of type select:
import React from 'react';
import { TextField, MenuItem, TextFieldProps } from '#material-ui/core';
import { useField, useFormikContext } from 'formik';
export type Option = {
id: number;
name: string;
};
type ITextFieldProps = TextFieldProps & {
name: string;
options: Option[];
};
const SelectWrapper: React.FC<ITextFieldProps> = ({
name,
options,
...otherProps
}) => {
const { setFieldValue } = useFormikContext();
const [field, meta] = useField(name);
const handleChange = (evt: React.ChangeEvent<any>) => {
const { value } = evt.target;
setFieldValue(name, value);
};
const configSelect = {
...field,
...otherProps,
select: true,
fullWidth: true,
onChange: handleChange,
error: false,
helperText: '',
};
if (meta && meta.touched && meta.error) {
configSelect.error = true;
configSelect.helperText = meta.error;
}
return (
<TextField variant="outlined" {...configSelect}>
{options.map(opt => {
return (
<MenuItem key={opt.id} value={opt.id}>
{opt.name}
</MenuItem>
);
})}
</TextField>
);
};
export default SelectWrapper;
In another file, I imported this component, I would like when I change the selection, call a certain function. I did it as follows, however, the function is not triggered.
<Select
name="company"
label="Selecione uma Empresa"
options={companies}
size="small"
onChange={e => getUnits(e)}
/>
Edit: if I use onBlur, it works. Only with onChange does not work.
Function:
const getUnits = useCallback(e => {
alert('test');
console.log(e.target.value);
}, []);
In the link below there is a gist with the contents of the file where I call the select component and the function.
https://gist.github.com/fredarend/1bd35d6d613ad547f3b6805f545d5ef4
First, you need to dereference the onChange property from the ITextFieldProps as follows ...
const SelectWrapper: React.FC<ITextFieldProps> = ({
name,
options,
onChange,
...otherProps
})
Then you need to call it in your own event handler ...
FWIW, I would get rid of the const { setFieldValue } = useFormikContext(); statement and use the helper on the useField hook instead. You can use the setValue method of the helper to assign the value for that specific field without needing a form reference. I would then call setTouched for the field value for good measure. (Although it's not 'technically' needed.)
const [field, meta, helper] = useField(name);
... and then
const handleChange = (evt: React.ChangeEvent<any>) => {
const { value } = evt.target;
helper.setValue(value);
setTimeout(() => fieldHelper.setTouched(true));
if (onChange) onChange(value);
};
const configSelect = {
...field,
...otherProps,
select: true,
fullWidth: true,
onChange: handleChange,
error: false,
helperText: '',
};

React how to clear input and fire onChange event

I'm creating react component library with own basic components and some logic on the top of that. I have wrapped classic input field with clearable button. I need implement _onClearInput, but idk how to fire onChange event with empty string value. Is there anyone who is able to help me?
import React, { ComponentPropsWithoutRef, useState } from 'react';
export interface MyInputProps extends ComponentPropsWithoutRef<'input'> {
}
const MyInput = React.forwardRef<HTMLInputElement, MyInputProps>(({
value,
onChange,
...rest
}, ref) => {
const [_value, _setValue] = useState(value || '');
const _onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
_setValue(event.target.value);
if(onChange) {
onChange(event);
}
}
const _onClearInput = () => {
_setValue('');
// I need trigger onChange event somehow with event.target.value = ''
}
return(
<div className={`input-${_value === '' ? 'is-not' : 'is'}-set`}>
<input ref={ref} value={_value} onChange={_onChange} {...rest}/>
<button onClick={_onClearInput}>ClearInput</button>
</div>
);
});
export default MyInput;

Props "Object is possibly 'undefined'" in React Typescript

I am new to Typescript and I have an error I don't understand in React Typescript. I suspect that it comes from the way I write my interface but I am not sure.
First I call my CellEditable component
<CellEditable value={'Hello'} onChange={() => {}} />
CEllEditable has an isEditable state that toggles InputText on click
CellEditable.tsx
import React, { useState } from 'react'
import Cell from './Cell.comp'
import InputText from './InputText.comp'
interface CellEditableProps {
value: string
onChange?: () => void
}
const renderCellInput = (type: string, opts: any) => {
switch (type) {
case 'text':
return <InputText {...opts} />
default:
return <div>Missing!</div>
}
}
const CellEditable = (props: CellEditableProps) => {
const { value, onChange } = props
const [isEditing, setEditing] = useState<boolean>(false)
const handleClick = () => setEditing(!isEditing)
const handleBlur = () => setEditing(!isEditing)
const opts = {
value,
helpers: {
onBlur: handleBlur,
onChange
}
}
return (
<div onClick={handleClick}>
{
isEditing
? renderCellInput('text', opts)
: <Cell value={value} />
}
</div>
)
}
export default CellEditable
InputText.tsx
import React from 'react'
interface InputTextProps {
value?: string
helpers?: HelpersProps
}
interface HelpersProps {
onChange?: () => void
onBlur?: () => void
}
const InputText = (props: InputTextProps) => {
const { value, helpers } = props
console.log('propsInputText:', props) // Empty object in the console
return (
<input type={'text'} value={value} onChange={helpers.onChange} onBlur={helpers.onBlur} />
)
}
export default InputText
The issue is:
helpers.onChange gets this error "Object is possibly 'undefined'. TS2532"
console.log('propsInputText:', props) in InputText.tsx output an empty object.
Is it an issue with typescript and the way I write my interface?
the helpers property in InputTextProps and the onChange property in your HelpersProps are optional. either make them required by removing the question mark or assign to them a default values when destructuing.
const { value, helpers = {} } = props;
const { onChange = () => {} } = helpers;
return (
<input type={'text'} value={value} onChange={onChange} onBlur={helpers.onBlur} />
)
inside your interface:
interface CellEditableProps {
value: string
onChange?: () => void
}
you placed a ? after onChange, that tells the compiler that it can be not passed and hence you get "Object is possibly 'undefined'
To remedy this solution either you can use onChange with ! like onChange!. This thells compiler that you are sure that onChange will not be null. But this is a bad approach.
What you should do is check if it is not null or undefined and then proceed:
if(onChange) {
...do your stuff here
}
your interface declaration clearly states that these can indeed be undefined (? marks these properties as optional). You'll need to check for their existance or fill them.
const value = props.value || '';
const helpers = {
onChange: () => {},
onBlur: () => {},
...(props.helpers || {}),
};
return (
<input type={'text'} value={value} onChange={helpers.onChange} onBlur={helpers.onBlur} />
)
or similar.

Resources