I have this simple example of Formik where i have a simple input. When i run the page i see in the console that it renders twice. The formik package is exactly the same as the first message.
Why it renders twice if there is nothing changed?
const SignupForm = () => {
const [data, setData] = useState({
firstName: "",
lastName: "",
email: "",
});
return (
<Formik
initialValues={data}
enableReinitialize
validateOnBlur={false}
validateOnChange={false}
onSubmit={(values, { setSubmitting }) => {
}}
>
{(formik) => {
console.log(formik);
return (
<form onSubmit={formik.handleSubmit}>
<label htmlFor="firstName">First Name</label>
<input
id="firstName"
type="text"
{...formik.getFieldProps("firstName")}
/>
{formik.touched.firstName && formik.errors.firstName ? (
<div>{formik.errors.firstName}</div>
) : null}
</form>
);
}}
</Formik>
);
};
That is happening due to enableReinitialize property.
Formik itself has a few useEffects inside of it, and a formikReducer. So when you pass enableReinitialize - formikReducer is called 2 times:
payload: {}, type: "SET_ERRORS"
payload: {}, type: "SET_TOUCHED"
Which is happening due to folowing useEffects inside of the source codes:
React.useEffect(function () {
if (enableReinitialize && isMounted.current === true && !isEqual(initialErrors.current, props.initialErrors)) {
initialErrors.current = props.initialErrors || emptyErrors;
dispatch({
type: 'SET_ERRORS',
payload: props.initialErrors || emptyErrors
});
}
}, [enableReinitialize, props.initialErrors]);
React.useEffect(function () {
if (enableReinitialize && isMounted.current === true && !isEqual(initialTouched.current, props.initialTouched)) {
initialTouched.current = props.initialTouched || emptyTouched;
dispatch({
type: 'SET_TOUCHED',
payload: props.initialTouched || emptyTouched
});
}
}, [enableReinitialize, props.initialTouched]);
And those ifs are passed due to the initialTouched and initialErrors are initialized inside of the Formik with this:
var initialErrors = React.useRef(props.initialErrors || emptyErrors);
var initialTouched = React.useRef(props.initialTouched || emptyTouched);
So initial values are equal to empty ones which are {}. But inside of the if they have !isEqual(initialErrors.current, props.initialErrors)) for example, so comparison between {} and undefined is passed and we are going inside of the if body and updating the Formik internal state. That is what is causing an additional rerender.
So if you pass the following props to Formik component - console.log will be executed only once
initialErrors={{}}
initialTouched={{}}
Now about how to collect that information:
Configure local minimal workspace with React and Formik
Go to node_modules/formik/dist/formik.cjs.development.js and inject some logging code inside of the formikReducer, simple console.log
In the component that is using <Formik> import it from modified development js file. import { Formik } from "formik/dist/formik.cjs.development";
Formik version: 2.2.9
Related
I created an array of objects that has form field definitions in order to quickly generate dynamic user forms, in react.
export const formFieldList = [
{
fieldType: 'input',
inputType: 'date',
name: 'LastLoginDate',
id: 'LastLoginDate',
label: 'Last Login Date',
value: moment(new Date()).format('YYYY-MM-DD'),
classWrapper: 'col-md-3',
}]
The problem occurred when I changed this constant array to a function. I need to get some data from async functions so I changed this array to a async func as well. All other form fields (text, select, checkbox) still work well but date fields, formik doest get date value after then.
export const formFieldList = async () => ([
{
fieldType: 'input',
inputType: 'date',
name: 'LastLoginDate',
id: 'LastLoginDate',
label: 'Last Login Date',
value: moment(new Date()).format('YYYY-MM-DD'),
classWrapper: 'col-md-3',
},
{
fieldType: 'select',
name: 'CurrencyId',
id: 'CurrencyId',
label: 'Currencies',
classWrapper: 'col-md-3',
options: await getCurrencies()
}]
If I write date value as hard-coded, it doesnt change anything, I mean function returns everything well, the problem is formik doesnt get that value as a prop when it returns from a function. My input component that get that values:
<input
{...formik.getFieldProps(Field.name)}
type={Field.inputType}
placeholder={Field.placeholder}
name={Field.name}
id={Field.id || Field.name}
/>
In the DOM, input element is shown as <input value name='xx' ... />.
My FormGenerator component that takes formFieldList and returns it to form:
const FormGenerator:FC<Props> = ({formFieldList, formButton, onSubmitValues}) => {
...
let initialValues = {}
for (const Field of formFieldList) {
if (Field.name !== 'newLine'){
initialValues = {...initialValues, [Field.name]: Field.value}
}
}
const schema = formFieldList.reduce(getValidationSchema, {})
const validationSchema = Yup.object().shape(schema)
const formik = useFormik({
initialValues,
enableReinitialize: true,
validationSchema,
onSubmit: async (values, {setStatus, setSubmitting}) => {
setLoading(true)
try {
onSubmitValues(values)
setLoading(false)
} catch (error) {
console.error(error)
setStatus('Some errors occurred')
setSubmitting(false)
setLoading(false)
}
},
})
return (
...
<form
onSubmit={formik.handleSubmit}
noValidate
>
{ formFieldList?.map((Field, index) => {
const touchedField = Field.name as keyof typeof formik.touched;
const errorsField = Field.name as keyof typeof formik.errors;
switch (Field.fieldType) {
case 'input':
return <InputField Field={Field} formik={formik} touchedField={touchedField} errorsField={errorsField} key={Field.name}/>
case 'select':
return <SelectField Field={Field} formik={formik} key={Field.name}/>
case 'newLine':
return <div className={Field.classWrapper} key={index}></div>
}
})}
...
</form>
</>
)
}
Updating:
After adding 'enableReinitialize' (with help of #Sheraff), form show dates and works well, but console give me an error as 'A component is changing an uncontrolled input to be controlled...'. Then I added value property to input element to make it controlled. But now date picker doesnt update ui when I change date manuelly (it returns correct date as its value). If I enter value 'undefined' in formFieldList then it works again, but this time I coundt initialize my date dynamically.
How can I fix it?
I'm new to React and I'm currently learning about useReducer.
I've created a simple login feature that verifies if the user inputted email includes '#' and the password length is greater than 5.
If these two conditions are met, I want my program to display an alert with success or fail message when pressing on the submit button.
What I'm curious about is that the application displays "Success" on submit when I add dispatch({type: 'isCredValid')} in useEffect(commented out in the code below), but the application displays "fail" when I add the dispatch({type: 'isCredValid'}) in the onSubmit handler without using useEffect. I was expecting the application to display "Success" when adding the dispatch({type: 'isCredValid')} in the onSubmit handler without the help of useEffect. Why is it not displaying "Success"? And why does my application display "Success" when the dispatch function is in the useEffect?
Reducer function :
const credReducer = (state, action) => {
switch(action.type) {
case 'email' :
return {...state, email: action.value, isEmailValid: action.value.includes('#')};
case 'password' :
return {...state, password: action.value, isPasswordValid: action.value.length > 5 ? true : false};
case 'isCredValid' :
return {...state, isCredValid: state.isEmailValid && state.isPasswordValid ? true : false};
default :
return state;
}
}
Component and input handlers
const Login = () => {
const [credentials, dispatch] = useReducer(credReducer, {
email: '',
password: '',
isEmailValid: false,
isPasswordValid: false,
isCredValid: false
})
// useEffect(() => {
// dispatch({type: 'isCredValid'})
// }, [credentials.isEmailValid, credentials.isPasswordValid])
const handleSubmit = (e) => {
e.preventDefault()
dispatch({ type: "isCredValid" })
if (credentials.isCredValid === true) {
alert ("Success!")
} else {
alert ('failed')
}
}
const handleEmail = (e) => {
dispatch({ type: "email", value: e.target.value })
}
const handlePassword = (e) => {
dispatch({ type: "password", value: e.target.value })
}
return (
<Card className={classes.card}>
<h1> Login </h1>
<form onSubmit={handleSubmit}>
<label>Email</label>
<input type="text" value={credentials.email} onChange={handleEmail}/>
<label>Password</label>
<input type="text" value={credentials.password} onChange={handlePassword}/>
<button type="submit"> Submit </button>
</form>
</Card>
)
}
if (credentials.isCredValid === true) {
alert ("Success!")
} else {
alert ('failed')
}
You are probably referring to above alert that you didn't immediately see "Success". That doesn't happen like that, just like with updating state, when you dispatch something, you will see the update on the next render.
This useEffect may work, but you're kind of abusing the dependency array here. You're not actually depending on credentials.isEmailValid or credentials.isPasswordValid. You should use these dependencies to decide which action to dispatch, and maybe that's your plan already.
The reason your handleSubmit doesn't seem to work, is what others point out. You won't be able to see the result until next render, so not inside the handleSubmit function.
// useEffect(() => {
// dispatch({type: 'isCredValid'})
// }, [credentials.isEmailValid, credentials.isPasswordValid])
const handleSubmit = (e) => {
e.preventDefault()
dispatch({ type: "isCredValid" })
if (credentials.isCredValid === true) {
alert ("Success!")
} else {
alert ('failed')
}
}
To see the results, add another useEffect and trigger the alert from there:
useEffect(() => {
if(credentials.isCredValid){
alert('Success!')
}
}, [credentials])
In one of my react project, I have initial state as follow:
const initialValues = {
en: {
notificationTitle: "",
notificationSubTitle: "",
},
hi: {
notificationTitle: "",
notificationSubTitle: "",
},
webUrl: "",
};
const [formData, setFormData] = useState(initialValues);
I'm passing this state as a props to child components which I'm using them as tabs like this
{selectedTab === "EnNotification" ? (
<EnNotification
formData={formData}
setFormData={setFormData}
/>
) : (
<HiNotification
formData={formData}
setFormData={setFormData}
/>
)}
when I enter the data in one tab suppose in EnNotification tab the state updates but when I tried to switch the tab it give me the following error:
TypeError: Cannot read properties of undefined (reading 'notificationTitle')
My input component looks like:
<Input
value={formData.en.notificationSubTitle || ""}
placeholder="Notification Sub Title"
onChange={(event) => {
const tempval = event.target.value;
setFormData((data) => ({
en: { ...data.en, notificationSubTitle: tempval },
}));
}}
/>
I think the problem is from only one component I'm able to update the state, but I want it should be updated from both.
Thank you.
When you are updating the formData state, before replacing the object en, you should also copy the rest of the value from the current state. Try this instead:
setFormData((data) => ({
...data,
en: { ...data.en, notificationSubTitle: tempval },
}));
My goal is to pass values selected in a form, to a get request.
The request should look as follows, following submit of the values.
get(api/csv/pets/?columns=DOG&columns=CAT&columns=FISH)
onSubmit={async (values, { resetForm }) => {
alert(JSON.stringify(values, null, 2));
setCsvData(values);
console.log(csvData);
const getCsvFile = async (values) => {
try {
const { data } = await fetchContext.authAxios.get(
`/api/csv/pets/${id}/?columns=${csvData ? csvData + '&' : null}`, values);
toCsv(data);
} catch (err) {
console.log(err);
}
};
getCsvFile();
However, even though formik takes a payload of values, I still get undefined when placing it in csvData with setCsvData(values).
What can I do to get the values selected in the query, and in the format needed?
My data:
export const CheckList = [
{
id: 'column-dog',
label: 'Dog',
value: 'DOG',
name: 'column',
},
{
id: 'column-cat',
label: 'Cat',
value: 'CAT',
name: 'column',
},
{
id: 'column-turtle',
label: 'Turtle',
value: 'TURTLE',
name: 'column',
},
{
id: 'column-fish',
label: 'Fish',
value: 'FISH',
name: 'column',
},
]
My Form:
const [csvData, setCsvData] = useState([]);
const selectAllData = CheckList.map((checkbox) => checkbox.value);
return (
<Formik
initialValues={{
columns: [],
selectAll: false,
}}
onSubmit={async (values, { resetForm }) => {
alert(JSON.stringify(values, null, 2));
setCsvData(values);
console.log(csvData);
resetForm();
}}
>
{({ values, setFieldValue }) => (
<Form>
<div>
<Field
onChange={() => {
if (!values.selectAll) {
setFieldValue('columns', selectAllData);
} else {
setFieldValue('columns', []);
}
setFieldValue('selectAll', !values.selectAll);
}}
checked={values.selectAll}
type="checkbox"
name="selectAll"
/>{' '}
Select All
</div>
{CheckList.map((checkbox) => (
<div key={checkbox.value}>
<label>
<Field
type="checkbox"
name="columns"
value={checkbox.value}
/>{' '}
{checkbox.label}
</label>
</div>
))}
<button
className="btn"
type="submit"
download
>
DOWNLOAD CSV
</button>
</Form>
)}
</Formik>
)
I think you've over-complicated your code a little bit. Remove csvData from state, and instead pass values into request.
onSubmit={async (values, { resetForm }) => {
const getCsvFile = async () => {
try {
const { data } = await fetchContext.authAxios.get(
`/api/csv/pets/${id}/?columns=${values ? values + '&' : null}`, values);
toCsv(data);
} catch (err) {
console.log(err);
}
};
getCsvFile();
}
You were getting an error is because the request was firing before csvData was set. setState is asynchronous :)
You cannot use setState inside an async function, becuase setState is an async function itself and it doesn't return a promise. As a result, we can't use async/await for setState method.(If it's not possible to make it await, then state change will trigger right away with undefined values instead of setting the given values)
Please refer this for more info: https://iamsongcho.medium.com/is-setstate-async-b1947fbb25e5
Therefore, the best use case is to remove the async from onSubmit() function to make it synchronous. As a result, setState will trigger the state change in component asynchronously.
Then keep getCsvFile(values), after form reset as you've already done that. And keep getCsvFile() function outside the component, since it doesn't do any state relevant changes.
onSubmit={(values, { resetForm }) => {
alert(JSON.stringify(values, null, 2));
setCsvData(values);
console.log(csvData);
resetForm();
getCsvFile(); // this function will trigger asynchronously
}}
I am new to typescript and migrating a react frontend to learn. I am currently having trouble with my login/registration component - a ternary operator is assigned to an onClick handler which dictates whether the login or signup mutation are executed. I implemented a workaround where I wrapped the mutations in functions, but it looks dirty.
Here is a barebones example, which returns a type assignment error because the values in the ternary do not share attributes with the onClick handler. I can provide a more robust, reproduceable example if necessary.
const Auth = () => {
const history = useHistory();
const [formState, setFormState] = useState({
login: true,
firstName: '',
lastName: '',
username: '',
password: '',
email: ''
});
const [loginMutation] = useMutation(LOGIN_MUTATION, {
onCompleted: (response) => {
//...
}
});
const [signupMutation] = useMutation(SIGNUP_MUTATION, {
variables: {
//...
},
onCompleted: (response) => {
// ...
}
});
return (
<>
{!formState.login && (
display extra inputs for registration form
)}
username and password fields
<button onClick={formState.login ? login : signup}>
{formstate.login ? login: signup}
</button>
</>
)
}
As you correctly guessed onClick has signature (e:MouseEvent<HTMLButtonElement, MouseEvent>) => void while mutation has a different signature (something along the lines of (options?: MutationFunctionOptions<T>) => Promise<Result>). So, when you say, onClick={login} you are essentially doing this: onClick={e => login(e)} which is wrong.
One way to fix it is to write an inline function
<button onClick={() => formState.login ? login() : signup()}>
{formstate.login ? login: signup}
</button>