React hook form method - setValue - doesn't work - javascript

I have some clearable select, and I want to reset the applets field in state to an empty array.
const defaultFormValues = { device: { ...initialDevice }, applets: [] };
const { control, getValues, setValue, reset, handleSubmit } = useForm<CreateDeviceFormData>({
mode: "all",
reValidateMode: "onChange",
defaultValues: defaultFormValues,
resolver: yupResolver(validationSchema),
});
const onChangeHandler = React.useCallback(
(value: Experience | null) => {
if (value) {
setValue("applets", getApplets(value));
} else {
setValue("applets", []);
// reset(defaultFormValues);
}
setValue("device.experience_id", value ? value.id : undefined);
},
[templateSelector, setValue],
);
console.log("current data", getValues(), control);
return (
<>
<SomeAutocompleteComponent control={control} onChange={onChangeHandler} />
<SelectAppletsComponent control={control} />
</>
);
export const SelectAppletsComponent = ({ control, onChange }) => {
const applets = useWatch({ control, name: "applets" }) as Applet[];
const device = useWatch({ control, name: "device" }) as Device;
if (!applets.length) {
return null;
}
return (
<SpanWrapper className="p-col-8">
{applets.map((applet) => (
<LabelRadio
key={applet.id}
inputId={applet.applet_type}
value={applet.applet_type}
label={applet.name}
checked={device.applet_type === applet.applet_type}
onChange={onChange}
/>
))}
</SpanWrapper>
);
};
the problem is that clearing the selection on UI with setValue("applets", []); not working for some reason, and I don't understand why, and how to do it without reset method, which resets the whole state, not just single property as I understand

You should always register fields if you want to use them as RHF's form state.
React.useEffect(() => {
register("applets");
}, [register]);
This fixes an issue.
Update:
Also a new method resetField is available

Just to follow up on this, it is indeed the right solution provided by AuthorProxy.
Using defaultValues doesn't register the fields (it seems that they are still added to the formData on submit, but since they are not registered, any user triggered changes to these fields won't reflect on the formData).
You have to register every field you want the user to be able to interact with.
We usually register fields via inputs in the JSX, but we also need to register the array since there is no input for it in the JSX.
As per shown by the author of the react hook form library.
https://github.com/react-hook-form/react-hook-form/discussions/3160
And sandbox
https://codesandbox.io/s/inspiring-wood-4z0n0?file=/src/App.tsx

Related

Input onChange doesn't fire when deleting sessionStorage values (Next.js)

I'm loading data with a custom hook from the session storage into an input, and if I delete the whole field, the onChange() function doesn't trigger. If I only add or delete one character, it works fine, but if I select all (or if the input had only one character), then delete it doesn't seem to do anything.
This only applies, when I delete the content after render, without doing anything else in the input beforehand.
//this works fine
const [test, setTest] = useState('test')
<input value={test} onChange={(e) => setTest(e.target.value)} />
//this doesn't trigger, when deleting all content after rendering the default value
const [test2, setTest2] = useSessionStorage({key: 'test2', defaultValue: 'test2'})
<input value={test2} onChange={(e) => setTest2(e.target.value)} />
Here is my custom hook:
export const useSessionStorage = (hookProps) => {
const { key, defaultValue } = hookProps
const [sessionItem, setSessionItem] = useState(() => {
if (typeof window != 'undefined') {
const item = sessionStorage.getItem(key)
return item ? JSON.parse(item) : defaultValue
}
})
useEffect(() => {
sessionStorage.setItem(key, JSON.stringify(sessionItem))
}, [key, sessionItem])
return [sessionItem, setSessionItem]
}
I'm sure it has to do something with the session storage loading in the server first, or just after the first render, but I have no solution.
Your code is working fine. In fact, I did not detect the render when test value changes. I added this useEffect inside the component,
useEffect(() => {
console.log("I am rerendering because test value is changing");
}, [test]);
inside useSessionStorage useEffect, add this
useEffect(() => {
console.log("I am rerendering becasue test2 value is changing");
sessionStorage.setItem(key, JSON.stringify(sessionItem));
}, [key, sessionItem]);
Now test it
SOLVED
The session storage loads from the server side, so it gave undefined (empty string) at first, and the actual value after. The input got the empty string, and when trying to change it to '', the onChange() didn't trigger, due to no changes.
Writing the input in a component and disabling SSR at import works.
import dynamic from 'next/dynamic'
const DynamicInput = dynamic(() => import('../components/Input'), {
ssr: false
})
export default function Page(){
const [test, setTest] = useSessionStorage({ key: 'test', defaultValue: null })
return (
<DynamicInput value={test} onChange={(e) => setTest(e.target.value)} />
)
}

Handling data rendering on redux state change

I'm trying to setup a form. It has Edit feature where on edit I call an API and get the data into state.
I'm struggling to display data in the form after api call. There's no problem utilizing the API or calling the redux functions. Problem is that my Form only displays last data in the redux state but not the updated data.
That's how I'm doing the stuff.
Calling API if isEdit===True at the same time Form is being displayed on component mount.
Updateding state after success as an object called customer
accessing the customer object like this
const { customer } = useSelector((state) => state.customers)
Lets say I have a input field where I want to display the email of customer.
I'm handling this think like that:
email: isEdit ? customer?.email : '', // At this point there is some problem
It loads the previous data that was stored in the state.customer but not the new one.
I believe my email field is rendering first and then doesn't updated the value when change happens in state.customer.
So how I can fix this? So that email value should be changed at the same time if state.customer got changed
Here is the full component. Still removed irrelevant part.
const CustomerNewEditForm = ({ isEdit, id, currentUser}) => {
const dispatch = useDispatch()
const navigate = useNavigate()
console.log('isEdit', isEdit, 'id', id, 'currentUser', currentUser)
// get sales reps
const { customer } = useSelector((state) => state.customers)
// const customer = () => {
// return isEdit ? useSelector((state) => state.customers?.customer) : null
// }
const { enqueueSnackbar } = useSnackbar()
const defaultValues = useMemo(
() => ({
email: isEdit ? customer?.email : '',
name: isEdit ? customer?.name : '',
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[currentUser]
)
const methods = useForm({
resolver: yupResolver(NewUserSchema),
defaultValues
})
const {
reset,
watch,
control,
setValue,
handleSubmit,
formState: { isSubmitting }
} = methods
const values = watch()
useEffect(() => {
if (isEdit === true) {
dispatch(getCustomerDetails(id))
console.log(customer)
}
if (isEdit && currentUser) {
reset(defaultValues)
}
if (!isEdit) {
reset(defaultValues)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isEdit, currentUser])
const onSubmit = async () => {
try {
await new Promise((resolve) => setTimeout(resolve, 500))
reset()
let body = {
email: values.email,
name: values.name,
}
console.log(body)
dispatch(createCustomer(body))
enqueueSnackbar(!isEdit ? 'Create success!' : 'Update success!')
// navigate(PATH_DASHBOARD.admin.root)
} catch (error) {
console.error(error)
}
}
return (
<FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
<Grid item md={3}>
{' '}
<RHFTextField name="name" label="Customer Name" />
</Grid>
<Grid item md={3}>
{' '}
<RHFTextField name="email" label="Email Address" />
</Grid>
</FormProvider>
)
}
export default CustomerNewEditForm
Here in the component defaultValues carries the previous data from customer object if its True and renders the form with those values. but new data comes a miliseconds later but form renders first.
First of all try to console.log your customer data and make sure that it gets a fresh data on last render.
If it gets fresh data, try take a look at your Input component, it might set some initial data, so the input will be editable and controlled by some state.
Try to modify your input's state on redux store update in useEffect.
Currently that's all that I can suggest, update your post with code with your form and input, also post your console.log result, if my answer doesn't helped you.
If the problem would be not in form\input state and console.log wouldn't show you actual updated data in last render, then I will need to see your redux store code to resolve this issue.
Hope it helped

Rendering array data with a condition

I need to make a large number of inputs and transfer this data to the server, I decided that the best solution would be to write all the options of these inputs into an array of objects, but I ran into the fact that I can’t get all my inputs to work. help me please
const test = [
{id: 1,state: 'city'},
{id: 2,state: 'language'},
{id: 3,state: 'brand'},
{id: 4,state: 'shop'},
]
const Auth = () => {
const [description, setDescription] = useState({city: "", language: "", brand: "", shop: ""});
const handleClick = async (event: any) => {
await store.update(description.city, description.brand);
};
const update = async (e: ChangeEvent<HTMLInputElement>) => {
setDescription({
...description,
city: e.target.value
});
};
return (
<>
{test.map(({ state, id}) => (
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description.city}
onChange={update}
/>
))}
<Button
className={styles.saveButton}
variant="contained"
color="inherit"
id="login"
onClick={handleClick}
>
Save
</Button>
</>
)
}
You send to TextField description.city for every input. The correct props are like so:
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={update}
/>
See the change in the value prop.
Also, you only update city in the update function. You have to make it so that the update function adapts to what values you pass to it. If you pass the city then it should update the city, if the language then the language and so on.
Overall this is not a good way to implement inputs. I just suggest you do them one by one and send to each TextField its corresponding value and a separate setState for each one.
But just for the sake of the example. The way you can do it is by passing the state value to the Update function.
So your function will look like this:
const update = async (e: ChangeEvent<HTMLInputElement>, state) => {
setDescription((description) => {
...description,
[state]: e.target.value
});
};
Now you just need to make sure that in the TextField component when you call onChange, you pass to it the event e and state which you have received from props.
Note: If you want to use the value of a state variable in the setState itself, pass to it a callback function like I did in the setDescription
if you want to make it dynamic you would have to send the variable to save to your update method and retrieve your value with description[state]
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={(e)=>update(e, state)}
/>
const update = async (e: ChangeEvent<HTMLInputElement>, state) => {
setDescription({
...description,
[state]: e.target.value
});
};
I think first and foremost you need your configuration data to try and closely match the elements you're building. So instead of { id, state } use { id, type, name }.
(This may not have a huge effect on your example because you're specifically using a TextField component, but if you were using native HTML controls you could add in different input types like number, email, date etc, and your JSX could deal with it easily.)
Second, as I mentioned in the comments, you don't need for those functions to be async - for example, there's no "after" code in handleClick so there's no need to await anything.
So here's a working example based on your code. Note: I've stripped out the Typescript (because the snippet won't understand the syntax), and the references to the UI components you're using (because I don't know where they're from).
const { useState } = React;
// So, lets pass in out inputs config
function Example({ inputs }) {
// I've called the state "form" here as it's a little
// more meaningful
const [form, setForm] = useState({});
// `handleSave` is no longer `async`, and for the
// purposes of this example just logs the updated
// form state
function handleSave() {
console.log(form);
// store.update(form);
}
// Also no longer `async` `handleChange` destructures
// the name and value from the changed input, and updates
// the form state - a key wrapped with `[]` is a dynamic key
// which means you can use the value of `name` as the key value
function handleChange(e) {
const { name, value } = e.target;
setForm({ ...form, [name]: value });
}
// In our JSX we destructure out the id, name, and
// type properties from each input object in the config
// and apply them to the various input element properties.
return (
<div>
{inputs.map(input => {
const { id, name, type } = input;
return (
<input
key={id}
type={type}
name={name}
placeholder={name}
value={form[name]}
onChange={handleChange}
/>
);
})}
<button onClick={handleSave}>Save</button>
</div>
);
}
// Our updated config data
const inputs = [
{ id: 1, type: 'text', name: 'city' },
{ id: 2, type: 'text', name: 'language' },
{ id: 3, type: 'text', name: 'brand' },
{ id: 4, type: 'text', name: 'shop' }
];
ReactDOM.render(
<Example inputs={inputs} />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Pass both the key that you want to update and the value to the update function:
const update = (key: string, value: string) => {
setDescription({
...description,
[key]: value,
});
};
{test.map(({ state, id }) => (
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
update(state, e.target.value)
}
/>
))}

React context custom input validation displays all errors when typing in any input

What I'm looking for is to have my custom input validation to show errors input by input, as I type into them (not on submit). What it does now is showing every errors as soon as I type inside any input. I know the reason : because setErrors() is in handleFormChange(), therefore every errors are set as soon as I type in any input, but I can't find a solution that meets my needs.
What I try to do is put most of the form's logic in the Form component, as it is where FormContext is defined. I believe doing so will give me more reusability, as the only parts I would need to change to reuse the components are only the validate function (to define the input's rules) and the onSubmit function, that are located in the parent component (here App). No need to touch anything on Form or FormInput when everything will be set.
I know react-hook-form and how to use it but I would like to create my own form / input validation logic. Everything works as needed expect this error display problem.
Here is the Form component (including FormContext) :
export const FormContext = createContext({
form: {},
errors: {},
});
export default function Form({
formInitialValues,
onSubmit = () => {},
validate = () => {},
children
}) {
const [form, setForm] = useState(formInitialValues);
const [errors, setErrors] = useState({});
const handleFormChange = (e) => {
const { name, value } = e.target || e;
const updatedForm = {
...form,
[name]: value,
};
const errors = validate(updatedForm);
setForm(updatedForm);
setErrors(errors);
};
return (
<FormComp>
<FormContext.Provider value={{
form,
errors,
handleFormChange,
}}>
{ children }
</FormContext.Provider>
<button type="button" onClick={() => onSubmit(form)}>
Envoyer
</button>
</FormComp>
)
}
Here is my FormInput component :
export default function FormInput({
label,
type = 'text',
name,
forwardRef,
}) {
const formContext = useContext(FormContext);
const { form, errors, handleFormChange } = formContext;
const inputRef = useRef(null);
return (
<InputGroup>
<label>{ label }</label>
<input
type={type}
name={name}
value={form[name]}
onChange={handleFormChange}
ref={forwardRef ? forwardRef : inputRef} />
{ errors && errors[name] && errors[name].length > 0 && <span>{ errors[name] }</span> }
</InputGroup>
)
}
And here is my App component, where I define the validate function (that is used in Form -> handleFormChange()) :
function App() {
const initialValues = {
lastName: '',
firstName: '',
};
const validate = (form) => {
let errors = {};
if (form.lastName === '') {
errors.lastName = 'Last name is required'
}
if (form.firstName === '') {
errors.firstName = 'This field is required';
}
return errors;
};
const onSubmit = (form) => {
console.log({ form });
};
return (
<div className="App">
<h1>S'inscrire</h1>
<Form
formInitialValues={initialValues}
onSubmit={onSubmit}
validate={validate}
>
<FormInput
label="Nom"
name="lastName" />
<FormInput
label="Prénom"
name="firstName" />
</Form>
</div>
);
}
export default App;
Could anyone point me in the right direction ? I tried lots of stuff but nothing works (at least nothing that meets my needs). This should be simple but I don't understand why I can't get it right (even though I know where the problem lies). Thank you very much for your help.
---------- EDIT ----------
Ok so I found a way to make it happen, but it seems a little bit "too much" (and still not working properly), I'm guessing there should be a more simple way to achieve what I want.
So what I did is setting a new state errors in the parent component (App) that I use to set the errors in the validate function (they are not set in Form -> handleFormChange() anymore), that I then pass down as a prop to Form. Here are the modified parts :
App component :
function App() {
//.... Cut down unnecessary parts ....
const [errors, setErrors] = useState({});
const validate = (name, value) => {
const omit = (key, { [key]: _, ...obj }) => obj
switch (name) {
case 'lastName':
if (value === '') {
setErrors({
...errors,
[name]: 'Last name required',
});
}
else {
let newObj = omit('lastName', errors);
setErrors(newObj);
}
break;
case 'firstName':
if (value === '') {
setErrors({
...errors,
[name]: 'First name required',
});
}
else {
let newObj = omit('firstName', errors);
setErrors(newObj);
}
break;
}
//.... Same as before ....
}
Then here is the new Form component (I'm just showing the handleFormChange function, everything is the same expect for the fact that I pass down the errors prop from App to Form (used in handleFormChange() -> validate()) :
const handleFormChange = (e) => {
const { name, value } = e.target || e;
const updatedForm = {
...form,
[name]: value,
};
setForm(updatedForm);
validate(name, value);
};
So as you can see, validate() still runs in Form but the errors are not set in Form anymore (they are set inside validate(), in the parent component App, in the switch statement).
This almost works as I would like : the error messages show input by input, but only one input at the time. What I mean is, if I type in and blank the first input, it displays the error for this input only, but then if I type in the second input while the first error is set, the first error message disappears but the second input error message displays (and even if I don't trigger the second input's error, it still hides the first error message), and vice versa.
What am I doing wrong ? I would greatly appreciate some advice. Thank you very much
Ok so I found the problem and now everything works as expected : as you type in the first input, if an error occurs for that input it displays it. Then if you type in the second input and an error occurs there, it also displays. And when I have no more errors, it hides them input by input.
What I missed was only better defining values in the validate() switch statement. What I mean is, before I only did if (values === ''), where I needed to do if (values.fieldName === '') to set the errors.
Here is the new validate function in the parent component (App) (note that I also use the name variable everywhere I can to avoid repeating lastName and firstName) :
switch (name) {
case 'lastName':
if (values.lastName === '') {
setErrors({
...errors,
[name]: 'Lastname is required'
})
}
else {
let newObj = omit(name, errors);
setErrors(newObj);
}
break;
case 'firstName':
if (values.firstName === '') {
setErrors({
...errors,
[name]: 'Firstname is required'
})
}
else {
let newObj = omit(name, errors);
setErrors(newObj);
}
break;
}
}
And here is the handleFormChange function in Form, where I needed to do validate(name, udpatedForm.values) instead of validate(name, updatedForm) (or as I showed before validate(name, values)) :
const handleFormChange = (e) => {
const { name, value } = e.target || e;
const updatedForm = {
...form,
values: {
[name]: value
},
};
validate(name, updatedForm.values);
setForm(updatedForm);
};
So that was just a silly error on my part... But now everything works great ! I now have my full reusable form / input validation using React context, and if I want to reuse the components, I just have to modify the validate function in the parent component (as well as formInitialValues), and everything is handled within the Form component, holding the context. So no need to touch this anytime soon ! Reusability for life (at least until I decide to add some stuff up maybe).
Note that I still set the errors in the parent component (in validate()), that I then pass down as props to Form. Is anyone as a better suggestion for going about this, please go ahead (I would have liked all the errors to be set in Form but eh, that's what I came up with, it works and meets my needs). Thank you very much

How to set Formik custom component value to Formik value

I'm using Formik for my form with google place auto-complete, I want to render places auto-complete as a custom component in the Formik field.
form.js
<Formik initialValues={location:""}>
<Field name="location" component={PlacesAutoComplete} placeholder="enter your location"/>
{...rest of form}
</Formik>
auto-complete component
import PlacesAutocomplete , {
geocodeByAddress,
geocodeByPlaceId
} from "react-google-places-autocomplete";
export const PlacesAutoComplete = ({
field: { name, ...field }, // { name, value, onChange, onBlur }
form: { touched, errors }, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
classes,
label,
...props
}: any) => {
const [fieldName, setFildName] = React.useState(field.name);
const [address, setAddress] = React.useState(props.value || "");
const error = errors[name];
// const touch = touched[name];
const handleSelect = () => {
// set this value to formik value
};
const handleChange = () => {
// set this value to formik value
};
const handleError = () => {
props.form.setFieldError(fieldName, error);
};
return (
<PlacesAutocomplete
value={address}
onChange={handleChange}
onSelect={handleSelect}
onError={handleError}
name={name}
placeholder={props.placeholder}
id={name}
{...props}
apiKey="Api key here"
>
{({
getInputProps,
suggestions,
getSuggestionItemProps,
loading
}: any) => (
<div>
<input
{...getInputProps({
placeholder: "Search Places ...",
className: "location-search-input form-control"
})}
/>
<div className="autocomplete-dropdown-container">
{loading && <div>Loading...</div>}
{suggestions.map((suggestion: any) => {
const className = suggestion.active
? "suggestion-item--active"
: "suggestion-item";
// inline style for demonstration purpose
const style = suggestion.active
? { backgroundColor: "#fafafa", cursor: "pointer" }
: { backgroundColor: "#ffffff", cursor: "pointer" };
return (
<div
{...getSuggestionItemProps(suggestion, {
className,
style
})}
>
<span>{suggestion.description}</span>
</div>
);
})}
</div>
</div>
)}
</PlacesAutocomplete>
);
};
How I set places auto-complete value to formik value, I'm pretty new to react and confused in handle change and on change functions. also, I found a solution in react class component here, But when converting those codes into functional components I'm stuck in Onchange and onSlecet functions
Better not write functional components as you'll get stuck with the test cases if you are writing.
OnChange is even you type anything, the value gets stored in onChange.
Abe onSelect is when you select anything
Basically on change you need to call formik's field onChange function. So in case you get an event on handleChange, just do this
const handleChange = (event) => {
// set this value to formik value
field.onChange(event.target.value)
};
or in case you get value in handleChange then do this
const handleChange = (value) => {
// set this value to formik value
field.onChange(value)
};
This will sync your formik state with autocomplete state.
Now comes the part for select. In this case also you can take the same route
const handleSelect = (value) => {
// set this value to formik value
field.onChange(value)
};
or you can use the setField function of form to update the value
const handleSelect = (value) => {
// set this value to formik value
form.setField('location',value)
};

Categories