Formik values didnt update after change - reactjs

I cant get the values from disabled input after fetch data. I think cause i put 2 condition in my input value component like this. is there a way to run properly from this code. i use formik to handle my form
<Input
disabled={true}
value={ProfileData ? moment(ProfileData.BirthDate).format('DD MMMM YYYY') : '' && formik.values.formA.ValueDesc5}
type="text"
name="formA.ValueDesc5"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
/>
ProfileData is a state that contain User Profile

In my case, I use Next JS 13.0.4 + Chakra UI + Formik 2.2.9 and I face the same issues as yours.
I use SWR to fetch data from my server:
const fetcher = async (id: string): Promise<Course> => {
return (await findCourseById(id)) as Course;
...
const { data: dbCourse, error } = useSWR(
urlToServer,
() => fetcher(id),
{
refreshInterval: 1000,
},
);
};
The key to solve the problem is that we need to re-initialize the default values of the form. I use useFormik hook, and it looks like this:
const formik = useFormik({
initialValues: {
name: dbCourse?.name as string,
description: dbCourse?.description as string,
},
validationSchema: updateCourseSchema,
enableReinitialize: true, // <=== check this line
onSubmit: async (values) => {
console.log(values);
},
});
Let me know if it helps!

Related

React using Formik does not clear the data value in Material UI form

I'm using formik.resetForm() to remove values from text fields in a form after submitting the data.
...
const handleSubmitProduct = async (values: Object, resetForm: any) => {
... code to handle my form data ...
resetForm()
if (response.ok) {
console.debug(response.status)
} else {
console.error(response)
}
}
const validate = (values: Object) => {
const errors: any = {}
if (!values.product_name) {
errors.product_name = "Include name"
}
return errors
}
... initialValues defined ...
const formik = useFormik({
initialValues: initialValues,
validate,
onSubmit: (values: Object, { resetForm }) => {
console.debug(JSON.stringify(values, null, 2))
handleSubmitProduct(values, resetForm)
},
})
return (
<FormLabel>Display name</FormLabel>
<TextField
onChange={formik.handleChange}
id="product_name"
onBlur={formik.handleBlur}
error={formik.touched.product_name && Boolean(formik.errors.product_name)}
helperText={formik.touched.product_name && formik.errors.product_name}
/>
<Button onClick={() => formik.handleSubmit()} variant="contained">
Submit
</Button>
)
I know there are many other questions like this but mine is different where I know the underlying Formik resources for values, errors, touched have been cleared but the values are still present in the text boxes.
The issue is I know the underlying Formik objects are cleared because after I submit, the validation triggers and prompts me like there is no value in the text field.
I've tried
resetForm({values: {initialValues}}) has the same result
resetForm(initialValues) has the same result
Use action.resetForm({values: {initialValues}}) in the onSubmit() which same result
https://codesandbox.io/s/mui-formik-fr93hm?file=/src/MyComponent.js but this approach uses the <Formik /> as opposed to useFormik which would change up my entire page but I'm in process to try anyway
I think the problem is that value of TextField is not value of formik. so the TextField is not controlled and by chaning value of formik it won't change.
assigning value of formik to it will do what you want
value={formik.values.firstName}
like this :
<TextField
onChange={formik.handleChange}
id="product_name"
value={formik.values.firstName}
onBlur={formik.handleBlur}
error={formik.touched.product_name && Boolean(formik.errors.product_name)}
helperText={formik.touched.product_name && formik.errors.product_name}
/>

Adding validation to simple React + Typescript application

I have kind of an easy problem, but I'm stuck because I do not know TypeScript well (and I need to get to know it very quickly).
I have to add simple validation on submit which will check if a value is not empty.
I have the simplest React form:
type FormValues = {
title: string;
};
function App() {
const [values, handleChange] = useFormState<FormValues>({
title: ""
});
const handleSubmit = (evt: FormEvent) => {
evt.preventDefault();
console.log("SUBMITTED");
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
id="title"
name="title"
onChange={handleChange}
type="text"
value={values.title}
/>
<button type="submit">submit</button>
</form>
</div>
);
}
Custom hook to handle form:
import { useCallback, useReducer } from "react";
type InputChangeEvent = {
target: {
name: string;
value: string;
};
};
function useFormState<T extends Record<string, string>>(initialValue: T) {
const [state, dispatch] = useReducer(
(prevState: T, { name, value }: InputChangeEvent["target"]) => ({
...prevState,
[name]: value
}),
initialValue
);
const handleDispatch = useCallback(
(evt: InputChangeEvent) => {
dispatch({
name: evt.target.name,
value: evt.target.value
});
},
[dispatch]
);
return [state, handleDispatch] as const;
}
export default useFormState;
Now I'd like to add simple validation on submit and (because of TS) I have no idea how. I've thought about three options:
Put the validation logic to the handleSubmit method.
Put the validation logic inside custom useFormState hook.
Create another custom hook just to manage validation only.
I tried to handle the first (and I think the worst) solution in this CodeSandbox example, but as I said the TS types are stronger than me and the code does not work.
Would anyone be so kind and help me with both cases (pick the most correct solution and then, run the code with TS)?
A common approach would be to just store the form error value in a React useState variable and render it as necessary.
const [error, setError] = React.useState('');
// ...
const handleSubmit = (evt: FormEvent) => {
evt.preventDefault();
// This might be the wrong way to check the input value
// I haven't worked directly with form submit events in a hot minute
if (!evt.target.value) {
setError('Title is required');
}
};
// ...
<input
id="title"
name="title"
onChange={handleChange}
type="text"
value={values.title}
aria-describedby="title-error"
required
aria-invalid={!!error}
/>
{error && <strong id="title-error" role="alert">{error}</strong>}
Notice that the aria-describedby, required, aria-invalid and role attributes are important to enforce semantic relationships between the input and its error, announce the error to screen readers when it appears, and designate the input as "required".
If you had multiple inputs in the form, you can make your error value into an object that can store a separate error for each field of your form:
const [errors, setErrors] = React.useState({});
// ...
setErrors(oldErrors => ({...oldErrors, title: "Title is required"}));
// ...
{errors.title && <strong id="title-error" role="alert">{errors.title}</strong>}
Another common pattern is to clear an input error when it is modified or "touched" to allow the form to be resubmitted:
onChange={(e) => {
setError(''); // or setErrors(({title: _, ...restErrors}) => restErrors);
handleChange(e);
}}
Note that all of this error handling logic can be rolled into your custom hooks for form/input handling in general, but does not have to be.

How do I override the defaultValues in useForm and maintain the isDirty function?

I have a situation where I need to call my DB to obtain some default values. But in the off-chance the data does not exist, I will set some default values in my useForm. Basically, this means that the defaultValues declared in useForm is a fallback value if I fail to obtain the default values from the DB.
From what Im understanding, according to the documentation with regards to useForm,
The defaultValues for inputs are used as the initial value when a component is first rendered, before a user interacts with it.
Or basically, the useForm is one of the first things defined when the page is loaded.
So, unless I can call my DB before useForm is loaded, Im a little stuck on this.
I've read that each Controller field can have something called defaultValue which sounds like the solution, but the documentation mentioned a caveat of
If both defaultValue and defaultValues are set, the value from defaultValues will be used.
I considered setValues but I want to use the isDirty function, which allows field validation and the value used to check whether the form is dirty is based on the useForm defaultValues. Thus, if I were to use setValues, the form would be declared dirty, which is something I do not want.
TL;DR this is what I want:
This is my initial value(the fallback value, result A).
const { formState: { isDirty }, } = useForm(
{defaultValues:{
userID: "",
userName: "",
userClass: "administrator",
}}
);
What I want to do is to make a DB call and replace the data, so that it would now look something like this(result B) if the call is successful(if fail, it will remain as result A).
const { formState: { isDirty }, } = useForm(
{defaultValues:{
userID: "1",
userName: "user",
userClass: "administrator",
}}
);
Please note that the DB call will replace only the userID and userName default values, the default value for userClass will be maintained.
So, the flow is as such:
Case 1: render form -> result A -> DB call -> success -> result B
Case 2: render form -> result A -> DB call -> fail/no data -> result A
So, unless I actually key in an input that is different from the default values of either results depending on the case, both Case 1 and Case 2 should return isDirty==false when I check it.
For react-hook-form#7.22.0 and newer
I think you want to use reset here in combination with useEffect to trigger it when your DB call has finished. You can additionally set some config as the second argument to reset, for example affecting the isDirty state in your case.
Although your answer works there is no need to use getValues here, as reset will only override the fields which you are passing to reset. Your linked answer is for an older version of RHF, where this was necessary.
Also if you're not adding a field dynamically on runtime then you can just pass the whole object you get from your DB call to reset and set the shouldUnregister to true in your useForm config. This way props from your result object which haven't got a corresponding form field will get ignored.
export default function Form() {
const { register, handleSubmit, reset, formState } = useForm({
defaultValues: {
userID: "",
userName: "",
userClass: "administrator"
},
shouldUnregister: true
});
console.log(formState.isDirty);
const onSubmit = (data) => {
console.log(data);
};
const onReset = async () => {
const result = await Promise.resolve({
userID: "123",
userName: "Brian Wilson",
otherField: "bla"
});
reset(result);
};
useEffect(() => {
onReset();
}, []);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>User ID</label>
<input type="text" {...register("userID")} />
<label>User Name</label>
<input type="text" {...register("userName")} />
<label>User Class</label>
<input type="text" {...register("userClass")} />
<input type="submit" />
</form>
);
}
Here is a Sandbox demonstrating the explanation above:
For older versions
Just merge your defaultValues or field values via getValues with the result of your DB call.
export default function Form() {
const {
register,
handleSubmit,
reset,
formState,
getValues,
control
} = useForm({
defaultValues: {
userID: "",
userName: "",
userClass: "administrator"
},
shouldUnregister: true
});
console.log(formState.isDirty);
const onSubmit = (data, e) => {
console.log(data);
};
const onReset = async () => {
const result = await delay();
reset({ ...getValues(), ...result });
};
useEffect(() => {
onReset();
}, []);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>User ID</label>
<input type="text" {...register("userID")} />
<label>User Name</label>
<input type="text" {...register("userName")} />
<label>User Class</label>
<Controller
name="userClass"
control={control}
render={({ field }) => <input type="text" {...field} />}
/>
<input type="submit" />
</form>
);
}
I went and further googled the issues I had with #knoefel's original answer, and I came across this
Thus, the solution I came up with, based on a combination with #knoefel's answer and the answer in the link:
useEffect(async () => {
let dataArray={};
let result= fetch('my-db-call');
if(result) {
dataArray['userID']=result.userID
dataArray['userName']=result.userName
}
reset({...getValues(), ...dataArray})
}, [])
Apparently, what happens is that the the reset function will first set the values, result A, using ...getValues() and any subsequent data after will replace the previously set values only if it exist. (eg. if my dataArray object lacks the userID key, it will not replace the userID default with the new default. ).
Only after both getValues and dataArray is set then will it reset the default values.
As far as I can tell, this is more or less incline with what I need and should give me result B.

Is there a way to stop useMutation from setting `data` to `undefined` before updating it to a new value?

I have a mock mutation like so:
interface Person {
firstName: string;
lastName: string;
}
async function sendPersonApi({ firstName, lastName }: Person) {
await new Promise((res) => setTimeout(res, 1000));
return {
firstName,
lastName,
status: "success"
};
}
I have two components: <Form /> and <Output />. I basically want the mutation to run on form submit of the <Form />, and then show the result of that mutation in the <Output />.
I have a CodeSandbox with this behavior mostly working: https://codesandbox.io/s/long-worker-k350q?file=/src/App.tsx
Form
const personAtom = atom<Person>({
firstName: "",
lastName: ""
});
function Form() {
const [formState, setFormState] = useAtom(personAtom);
const handleSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(event) => {
event.preventDefault();
const formElement = event.currentTarget;
const formData = new FormData(formElement);
const firstName = formData.get("firstName") as string;
const lastName = formData.get("lastName") as string;
setFormState({ firstName, lastName }); // This does not update the `data`/`isLoading`/`isError` in the <Output /> component
},
[setFormState]
);
return (
<form id="name-form" onSubmit={handleSubmit}>
<input name="firstName" /> <br />
<input name="lastName" /> <br />
<button>Submit</button>
</form>
);
}
Output
function Output() {
const [person] = useAtom(personAtom);
const { mutate, data, isLoading, isError } = useMutation(sendPersonApi, {
mutationKey: "sendPerson"
});
useEffect(() => {
mutate(person);
}, [person]);
return (
<output name="name-output" form="name-form">
<p>data: {JSON.stringify(data)}</p>
<p>isLoading: {JSON.stringify(isLoading)}</p>
<p>isError: {JSON.stringify(isError)}</p>
</output>
);
}
Initially, I had wanted to implement this by doing the mutation itself inside the <Form /> submit handler, but it seems like I was not be able to access the data/isLoading/isError in a different component even if I used the same mutationKey in useMutation.
So I ended up changing the implementation so that the submit handler just updates some global state (using jotai atoms), and then in the Output I have an effect that listens for changes to this global state and then calls mutate().
The problem now though is that the data always gets reset to undefined in every subsequent submit before getting a new value. Is there a way to stop useMutation from setting data to undefined before updating it to a new value?
Also, is there a cleaner way of using the result of a mutation in another component? I'd prefer to have the mutate() done inside the Form and then somehow use the data/isLoading/isError in a different component without having to rely on useEffect.
I think I'm going to just create a separate atom to hold the global state for the current data. Then I'll use the onSuccess of the useMutation to update this global state from within <Form /> and then just use that global state inside <Output />.

Async update of Formik initialValues inherited from parent React component state (leveraging useEffect hook?)

I am currently building a multi-step form during a user onboarding process, which is why I need to centralize all form data in a parent React component state.
I need to update initialValues with user information but this is an async process.
I thought of creating a useEffect hook calling setState, but maybe there is a more elegant way of doing so...
Having initialValues as one of useEffect dependencies seems to create an infinite loop (Maximum update depth exceeded). This is why the working solution I found was to duplicate all initialValues within... 😒
So how could I update only specific values from initialValues after getting async user information?
Here is a simplified version of the implementation:
import React, { useState, useEffect } from 'react'
// Auth0 hook for authentication (via React Context).
import { useAuth0 } from '../../contexts/auth/auth'
import { Formik, Form, Field } from 'formik'
export default () => {
const { user } = useAuth0()
const initialValues = {
profile: {
name: '',
address: '',
// Other properties...
},
personalInfo: {
gender: '',
birthday: '',
// Other properties...
},
}
const [formData, setFormData] = useState(initialValues)
const [step, setStep] = useState(1)
const nextStep = () => setStep((prev) => prev + 1)
useEffect(() => {
const updateInitialValues = (user) => {
if (user) {
const { name = '', gender = '' } = user
const updatedInitialValues = {
profile: {
name: name,
// All other properties duplicated?
},
personalInfo: {
gender: gender,
// All other properties duplicated?
},
}
setFormData(updatedInitialValues)
}
}
updateInitialValues(user)
}, [user, setFormData])
switch (step) {
case 1:
return (
<Formik
enableReinitialize={true}
initialValues={formData}
onSubmit={(values) => {
setFormData(values)
nextStep()
}}
>
<Form>
<Field name="profile.name" type="text" />
<Field name="profile.address" type="text" />
{/* Other fields */}
<button type="submit">Submit</button>
</Form>
</Formik>
)
case 2:
return (
<Formik
enableReinitialize={true}
initialValues={formData}
onSubmit={(values) => {
setFormData(values)
nextStep()
}}
>
<Form>
<Field name="personalInfo.gender" type="text" />
<Field name="personalInfo.birthday" type="text" />
{/* Other fields */}
<button type="submit">Submit</button>
</Form>
</Formik>
)
// Other cases...
default:
return <div>...</div>
}
}
it's probably late for me to see this question and I just happen to work on a similar project recently.
For my use case, I'm using only one Formik, and using theory similar to Formik Multistep form Wizard: https://github.com/formium/formik/blob/master/examples/MultistepWizard.js for my multistep forms.
And on each step, I need to fetch API to prefill data, I also use useEffect but since I just call the API onetime when I load the specific step, I force it to behave the same as ComponentDidMount(), which is to leave the [] empty with the comment // eslint-disable-next-line so it won't give me warning.
And I use setFieldValue in the useEffect after data is successfully loaded. I feel mine is also not a good way to handle this situation, and I just found something that might be useful: https://github.com/talor-hammond/formik-react-hooks-multi-step-form, it has a Dynamic initialValues. (Though it's typescript)
I am going to refer to this and also try to use for each of my steps, and probably use Context or Wrap them in a parent and store data in the parent Formik.
And getting infinite loop for you might because setFormData should not be in the dependency, since when you setState, the component re-render, the useEffect calls again.
Not sure if this can help you or you already find out how to implement it, I'll look into this deeper.

Resources