Formik. Dirty check - reactjs

Can someone tell me how to make the addition of data work together with the check through dirty. The rendering of the button works, but when I click on it the initialState gets new data and is updated, hence the dirty should return to false, but it is not.
state:
const [store, setStore] = useState<UserDataType>({
firstName: 'Artem',
lastName: 'Bugay',
email: '',
age: '',
country: '',
region: '',
placeOfWork: '',
occupation: '',
});
Function what save changes to local store:
const changeState = (values: UserDataType) => {
setStore(values);
};
Component return:
return (
<Styled.WrapperContainer>
<Styled.Container>
<GlobalStyled.Title>Your profile</GlobalStyled.Title>
<Formik initialValues={store} onSubmit={updateProfile}>
{({ values, isSubmitting, handleChange, dirty }) => {
return (
<GlobalStyled.FormFormik>
{console.log('dirty', dirty)}
<Styled.Field
type="text"
name="firstName"
label="First Name"
value={values?.firstName}
onChange={handleChange}
/>
<Styled.Field
type="text"
name="lastName"
label="Last Name"
value={values?.lastName}
onChange={handleChange}
/>
<Styled.Field
type="email"
name="email"
label="Email"
value={values?.email}
onChange={handleChange}
/>
...
<Styled.ButtonAntd
data-testid="profile"
htmlType="submit"
disabled={!dirty}
onClick={() => changeState(values)}
>
Update
{/* <Spinner loading={isLoading === StateStatus.Pending} size={20} /> */}
</Styled.ButtonAntd>
</GlobalStyled.FormFormik>
);
}}
</Formik>
</Styled.Container>
</Styled.WrapperContainer>
);

onSubmit={(values, { setSubmitting, resetForm }) => {
setSubmitting(true);
setTimeout(async () => {
resetForm({ values });
}, 100);
}}

You don't need to use setStore, you have variable {values}.
Every time field value changes, you have updated data in {values}. You can check with your custom function onChange element you create. Check this info too.
Also, you can check fields data with advanced lib.

Related

How to solve uncontrolled warning in react formik?

Let's say we have a form that has two fields, first name, and last name.
I want to control this form using React Formik and I have simulated API response using setTimeout
The problem is that when API returns null for some properties, I get the dirty uncontrolled warning of React.
I'm using Formik's 'name' prop to mutually bind my JSON to my form inputs.
How can I solve this problem?
Here's my code:
export default function Home() {
const [initialValues, setInitialValues] = useState({
firstName: '',
lastName: '',
});
useEffect(() => {
console.log(initialValues);
}, [initialValues]);
useEffect(() => {
setTimeout(() => {
setInitialValues({ firstName: 'api', lastName: null });
}, 3000);
}, []);
const onSubmit = (values) => {
console.log(values);
};
const validationSchema = Yup.object({
firstName: Yup.string().required('Required'),
lastName: Yup.string().required('Required'),
});
return (
<div>
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
validationSchema={validationSchema}
validateOnChange={false}
validateOnBlur={false}
enableReinitialize
>
<Form>
<br />
<div>
<Field
type="text"
id="firstName"
name="firstName"
label="firstName"
placeholder="First Name"
/>
<ErrorMessage name="firstName" />
</div>
<br />
<div>
<Field
type="text"
id="lastName"
name="lastName"
placeholder="Last Name"
/>
<ErrorMessage name="lastName" />
</div>
<br />
<button type="submit">Submit</button>
</Form>
</Formik>
</div>
);
}
And here's an online sample in StackBlits
Don't trust the data coming back from your API to be complete, then. Pull each value you need into a new object with a default value of an empty string. Something like
let newInitialValues = {};
newInitialValues.firstName = apiResult.firstName ? apiResult.firstName : '';
newInitialValues.lastName = apiResult.lastName ? apiResult.lastName : '';
This ensures you'll always have a valid value.

React state sets the same value to all of the state values

I'm new to react JS, I'm trying to get the values from a form, and send it to a node JS middleware.
I set up two values in the state, one for the email and another one for the password. I set the state for both values in the set state method.
class LoginForm extends Component {
constructor(props){
super(props)
this.state = {
email : '',
password : '',
}
}
handleChange = (e) => {
this.setState({ email : e.target.value, password : e.target.value})
}
handleSubmit = (e) => {
console.log('state', this.state)
};
render () {
return (
<div style = {styles.form} >
<Fragment>
<Form
{...layout}
name="basic"
initialValues={{
remember: true,
}}
onFinish={this.handleSubmit}
>
<Form.Item
name="email"
rules={[
{
type: 'email',
message: 'The input is not valid E-mail!',
},
{
required: true,
message: 'Please input your E-mail!',
},
]}
hasFeedback
>
<Input
placeholder={t`Email`}
value={this.state.email}
onChange={this.handleChange} />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true }]} hasFeedback
>
<Input.Password
placeholder={t`Password`}
value={this.state.password}
onChange={this.handleChange}
/>
</Form.Item>
<Button
type="primary"
htmlType="submit"
>
<span style = {styles.button} >Sign in</span>
</Button>
</Form>
</Fragment>
</div>
)
}
}
}
I created the handle submit function and linked it to the onsubmit method inside the form and tried console logging the current state of my values inside the handle submit function. To my surprise the value from the password gets console logged for the value email too. Like this
state {email: "123", password: "123"}
I cannot figure out what am I doing wrong.
I think if you change your handleChange function to this, it should work.
handleChange = (e) => {
this.setState({ [e.target.id] : e.target.value})
}
And add id to the input fields like this
<Input id="email" placeholder={t`Email`} value={this.state.email} onChange {this.handleChange} />
<Input.Password id="password" placeholder={t`Password`} value {this.state.password} onChange={this.handleChange} />
Here is the solution:
handleChange = (e) => {
let email = ''
let password = ''
if (e.target.name === 'email') {
email = e.taget.value
} else {
password = e.taget.value
}
this.setState({ email, password})
}

Trying to refactor the onSubmit property using Formik

Brushing up my development skills with React. I'm trying to figure a way to refactor the onSubmit property. My application is a contact form using the Formik component which sends the data to Firebase Cloudstore as well as sending an email via emailjs. If it's a success, it'll have a popup using Material UI's Snackbar. It works, but just trying to clean up the code. Please help!
onSubmit={(values, { resetForm, setSubmitting }) => {
emailjs.send("blah","blah", {
email: values.email,
name: values.name,
message: values.message
},
'blah',);
//this is sent to firebase cloudstore
db.collection("contactForm")
.add({
name: values.name,
email: values.email,
message: values.message,
})
.then(() => {
handleClick();
})
.catch((error) => {
alert(error.message);
});
setTimeout(() => {
resetForm();
setSubmitting(false);
/* console.log(values);
console.log(JSON.stringify(values, null, 2)); */
}, 500);
}}
Here's the complete function
function Contact() {
const [open, setOpen] = React.useState(false);
const handleClose = (event, reason) => {
if (reason === "clickaway") {
return;
}
setOpen(false);
};
const handleClick = () => {
setOpen(true);
};
const classes = useStyles();
return (
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={(values, { resetForm, setSubmitting }) => {
emailjs.send("blah","blah", {
email: values.email,
name: values.name,
message: values.message
},
'blah',);
//this is sent to firebase cloudstore
db.collection("contactForm")
.add({
name: values.name,
email: values.email,
message: values.message,
})
.then(() => {
handleClick();
})
.catch((error) => {
alert(error.message);
});
setTimeout(() => {
resetForm();
setSubmitting(false);
/* console.log(values);
console.log(JSON.stringify(values, null, 2)); */
}, 500);
}}
>
{({ submitForm, isSubmitting }) => (
<Form>
<Snackbar open={open} autoHideDuration={6000} onClose={handleClose}>
<Alert onClose={handleClose} severity="success">
Your message has been sent!
</Alert>
</Snackbar>
<div>
<Field
component={TextField}
label="Name"
name="name"
type="name"
/>
<ErrorMessage name="name" />
</div>
<div>
<Field
component={TextField}
label="Your email"
name="email"
type="email"
/>
<ErrorMessage name="email" />
</div>
<br />
<br />
<div>
<Field
as="textarea"
placeholder="Your Message"
label="message"
name="message"
type="message"
rows="15"
cols="70"
/>
<ErrorMessage name="message" />
</div>
{isSubmitting && <LinearProgress />}
<Button
variant="contained"
color="primary"
disabled={isSubmitting}
onClick={submitForm}
>
Submit
</Button>
</Form>
)}
</Formik>
);
}
I would recommend making the onSubmit property it's own function in the component body, you will want to memoize this using useCallback. Additionally, you can create a hook to allow you to control the alert component, you can also allow the hook to control weather it's an error or success type, reducing the need to duplicate code if it fails to save.
Your submission handler could look like this, note that I omitted the sending of the email and mocked the firebase portion. Also you can call finally on the promise, rather than calling setSubmitting in both the then and catch blocks.
const handleSubmit = React.useCallback(
(values, { setSubmitting, resetForm }) => {
db.collection("contact")
.add(values)
.then((res) => {
show({ message: "Your message has been sent" });
})
.catch((err) => {
show({ variant: "error", message: "Failed to send your message." });
})
.finally(() => {
setSubmitting(false);
});
},
[show]
);
The show function in the above example would be part of your hook to control the alert. The hook could look something like this, it could be extended based on your usecase.
import React from "react";
const useAlert = () => {
const [state, setState] = React.useState({
variant: "success",
visibile: false,
message: null
});
const show = React.useCallback(
(options = {}) => {
setState((prev) => ({
...prev,
...options,
visible: true
}));
},
[setState]
);
const hide = React.useCallback(() => {
setState((prev) => ({ ...prev, visibile: false }));
}, [setState]);
return { ...state, show, hide };
};
export default useAlert;
Additionally, since you're using material ui, you'll want to take advantage of their built in components. This would remove the need for your multiple <br />s for spacing, as well as help to keep the UI consistent.
<Box marginBottom={1}>
<Field component={TextField} label="Name" name="name" type="name" />
<ErrorMessage name="name" />
</Box>
<Box marginBottom={1}>
<Field
component={TextField}
label="Email"
name="email"
type="email"
/>
<ErrorMessage name="email" />
</Box>
Also, you could use the built in component for the text area, keeping the design consistent. Using the multiline prop allows you to make the input a text area.
<Box marginBottom={2}>
<Field
component={TextField}
placeholder="Your Message"
label="Message"
name="message"
type="message"
rows={5}
multiline
fullWidth
/>
<ErrorMessage name="message" />
</Box>
I'm personally not a huge fan of using the LinearProgress in the manner than your did. I personally think that the circular process looks better, specifically when used inside the submit button. Here are the relevant docs.
<Button
variant="contained"
color="primary"
disabled={isSubmitting}
onClick={submitForm}
endIcon={isSubmitting && <CircularProgress size={15} />}
>
Submit
</Button>
I've put a working example together in a codesandbox.

onchange in a form using formik the value of the field is not updated

I'm new to react, and I'm trying to apply validations to a form.
For some reason when adding the property:
onChange={onChange}
I want to send the values to the parent component. That's why I'm using the onchange.
Nothing I write is shown in my text fields, why does this happen?
export const Son = props => {
const { onChange } = props;
return (
<Formik
initialValues={{
fullname: "",
email: ""
}}
validationSchema={Yup.object().shape({
fullname: Yup.string()
.min(2, "Your name is too short")
.required("Please enter your full name"),
email: Yup.string()
.email("The email is incorrect")
.required("Please enter your email")
})}
onSubmit={(values, { setSubmitting }) => {
const timeOut = setTimeout(() => {
console.log(values);
setSubmitting(false);
clearTimeout(timeOut);
}, 1000);
}}
>
{({
values,
errors,
touched,
handleSubmit,
isSubmitting,
validating,
valid
}) => {
return (
<Form name="contact" method="post" onSubmit={handleSubmit}>
<label htmlFor="fullname">
Fullname
<Field
type="text"
name="fullname"
autoComplete="name"
placeholder="your fullname"
onChange={onChange}
/>
</label>
{<ErrorMessage name="fullname">{msg => <p>{msg}</p>}</ErrorMessage>}
{/*errors.fullname && touched.fullname && <p>{errors.fullname}</p>*/}
<br />
<label htmlFor="email">
Email
<Field
type="email"
name="email"
autoComplete="email"
placeholder="your email"
onChange={onChange}
/>
</label>
<ErrorMessage name="email">{msg => <p>{msg}</p>}</ErrorMessage>
<br />
<button type="submit" disabled={!valid || isSubmitting}>
{isSubmitting ? `Submiting...` : `Submit`}
</button>
</Form>
);
}}
</Formik>
);
};
this is my live code:
https://stackblitz.com/edit/react-qotvwb?file=components/son_component.js
you're not using the formik handleChange at all.
I highlighted the changes that I made in https://stackblitz.com/
and you can test this working here

Formik form no validated using Yup on test environment with jest

I'm trying to test if validation is raised when a required field is empty. In my example, I have an email input, which I set to empty, and when I simulate submit action, the onSubmit function is executed when in reality it shouldn't. I'm using validationSchema property using Yup to validate my form. I've added console.log() inside my submit function which is displayed in debug mode (and it shouldn't).
This is working in dev environment (validations raised, onSubmit function no executed) but for some reason, it doesn't work in test env.
It's worth mentioning that I'm full mounting the component to test it using Enzyme.
Thanks in advance.
I've tried with .update to check if at least the view is updated after simulating the action, but it still invokes the submit function.
Here's my code:
form.js
render() {
const { intl } = this.props;
return (
<div className="signupForm">
<Formik
initialValues={{ email: '', password: '', passwordConfirmation: '' }}
onSubmit={this.submitForm}
validationSchema={SignUpSchema}
render={ formProps => (
<Form>
<p className="signupForm__message">{formProps.errors.general}</p>
<FormControl margin="normal" fullWidth>
<Field
type="text"
name="email"
component={TextField}
className='signupForm__input'
label={intl.formatMessage(messages.email)}
/>
</FormControl>
<FormControl margin="normal" fullWidth>
<Field
type="password"
name="password"
component={TextField}
className='signupForm__input'
label={intl.formatMessage(messages.password)}
fullWidth
/>
</FormControl>
<FormControl margin="normal" fullWidth>
<Field
type="password"
name="passwordConfirmation"
component={TextField}
className='signupForm__input'
label={intl.formatMessage(messages.passConfirmation)}
fullWidth
/>
</FormControl>
<Button
type="submit"
fullWidth
variant="contained"
className='signupForm__button'
disabled={formProps.isSubmitting}
>
<FormattedMessage id="login.form.submit" />
</Button>
{formProps.isSubmitting && <Loading />}
</Form>
)
}
/>
</div>
);
}
test.js
describe('submit with blank password', () => {
beforeEach(() => {
subject = mount(withStore(<SignUpPage />, store));
// load invalid data to the form
email.simulate('change', { target: { name: 'email', value: 'joe#joe.com' } });
password.simulate('change', { target: { name: 'password', value: '' } });
passwordConfirmation.simulate('change', { target: { name: 'passwordConfirmation', value: 'password' } });
form.simulate('submit');
});
it('should display an error in the password field', () => {
subject.update();
const passwordInput = subject.find('TextField').at(1);
expect(passwordInput.props().error).toBeTruthy();
});
});
Though Formik's handleSubmit is a sync function, validation with yup functions are called asynchronously.
The following worked for me.
test("Formik validation", async () => {
const tree = mount(<YourForm />);
// Submit the form and wait for everything to resolve.
tree.simulate('submit', {
// Formik calls e.preventDefault() internally
preventDefault: () => { }
});
await new Promise(resolve => setImmediate(resolve));
tree.update();
expect(yourPasswordInput.props().error).toBeTruthy();
});

Resources