Trying to refactor the onSubmit property using Formik - reactjs

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.

Related

Formik. Dirty check

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.

Antd form doesn't identify input values

I have created my react form with antd. I have added antd validation for the form. But my form doesn't know whether I have filled the form or not. Whenever I filled the form and submitted it, it doesn't call onFinish method. Instead it fails and calls onFinishFailed method and gives me validation error messages.
I have created it in correct way according to my knowledge. But there is something missing I think. Here's my code.
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const history = useHistory();
const [form] = Form.useForm();
const layout = {
labelCol: { span: 4 },
wrapperCol: { span: 8 },
};
const onChangeName = (e) => {
setName(e.target.value);
console.log(name);
}
const onAddCategory = (values) => {
let req = {
"name": values.name,
"description": values.description
}
postCategory(req).then((response) => {
if (response.status === 201) {
message.success('Category created successfully');
history.push('/categorylist');
}
}).catch((error) => {
console.log(error);
message.error('Oops, error occured while adding category. Please try again');
});
}
const onFinishFailed = (errorInfo) => {
console.log('Failed:', errorInfo);
console.log('State:', name, description);
};
return (
<React.Fragment>
<Form
form={form}
name="control-hooks"
onFinish={onAddCategory}
onFinishFailed={onFinishFailed}
{...layout}
size="large"
>
<Form.Item
name="name"
rules={[
{
required: true,
message: 'You can’t keep this as empty'
}, {
max: 100,
message: 'The category name is too lengthy.',
}
]}
>
<label>Category name</label>
<Input
placeholder="Category name"
className="form-control"
value={name}
onChange={onChangeName}
/>
</Form.Item>
<Form.Item
name="description"
rules={[
{
required: true,
message: 'You can’t keep this as empty'
}, {
max: 250,
message: 'The description is too lengthy',
}
]}
>
<label>Description</label>
<Input.TextArea
placeholder="Description"
className="form-control"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</Form.Item>
<Form.Item shouldUpdate={true}>
<Button
type="primary"
htmlType="submit"
className="btn btn-primary"
>
Add category
</Button>
</Form.Item>
</Form>
</React.Fragment>
)
In this form I have managed state using hooks. In onFinishFailed method I have logged my input values with state and they have values. But form doesn't identify it.
How do I resolve this. Please help.
I found the issue. Here I had added label inside form item. It was the reason for the unexpected behavior. Once I took the label outside the form item problem was solved.
<label>Category name</label>
<Form.Item
name="name"
rules={[
{
required: true,
message: 'You can’t keep this as empty'
}, {
max: 100,
message: 'The category name is too lengthy.',
}
]}
>
<Input
placeholder="Category name"
className="form-control"
value={name}
onChange={onChangeName}
/>
</Form.Item>

Handling routes after submitting form

Hello my great teachers of StackOverflow. I'm going through ben awads fullstack tutorial and am trying to add an image upload feature to create post. Looks like everything works well, inserts posts (including image) into db. However, after submitting my form, it wont send me to the home page (stays on the same page with current values inserted). It is set so that if there are no errors, route me to the homepage. Im assumming i have no errors cause the post inserts into database. Any help will be greatly appreciated.
const CreatePost: React.FC<{}> = ({}) => {
const router = useRouter();
const [createPost] = useCreatePostMutation();
return (
<Layout>
<Formik
initialValues={{ title: "", text: "", file: null }}
onSubmit={async (values) => {
console.log(values);
const { errors } = await createPost({
variables: values,
});
if (!errors) {
router.push("/");
}
}}
>
{({ isSubmitting, setFieldValue }) => (
<Form>
<InputField name="title" placeholder="title" label="Title" />
<Box mt={4}>
<InputField name="text" placeholder="text..." label="Body" />
</Box>
<Input
mt={4}
required
type="file"
name="file"
id="file"
onChange={(event) => {
setFieldValue("file", event.currentTarget.files[0]);
}}
/>
<Button mt={5} type="submit" isLoading={isSubmitting}>
create post
</Button>
</Form>
)}
</Formik>
</Layout>
);
};
You can use router.push hook.
You need to pass validate function as props to Fomik to prevent submitting only with 2 fields. I have added a basic validate object. But you can use Yup package also. Formik will return an error object inside the form and you need to make sure form does not get submitted in error state. For that get the errors object and as you are using custom Button component pass true/false by checking if any errors exist. I added the code.
You can get more details here.
router.push("/home");
.....
const validate={values => {
const errors = {};
if (!values.title) {
errors.email = 'Required';
}
if (!values.text) {
errors.text= 'Required';
}
if (!values.file) {
errors.file= 'Required';
}
return errors;
}}
<Formik
initialValues={{ title: "", text: "", file: null }}
validate
onSubmit={async (values) => {
.......
{({ isSubmitting, setFieldValue, errors }) => (
..........
<Button mt={5} type="submit" isLoading={isSubmitting} isFormValid={!Object.values(errors).find(e => e)}>
create post
</Button>

How to use useReducer along with Formik?

I have a file called Context.js in which I have a reducer. All of these are passed to other components using the Context API
const reducer = (state, action) => {
switch (action.type) {
case "SET_NAME":
return {...state, name: action.payload}
case "SET_LASTNAME":
return {...state, lastname: action.payload}
case "SET_EMAIL":
return {...state, email: action.payload}
default:
return state
}
In numerous other components, I am trying to use Formik to work with forms, and would like to save the form info into state, so that if the form is accessed via other components, the info that has already been provided can be found there.
<label htmlFor="name">Nome</label>
<Field maxLength="51"
name="name"
value={name}
type="text"
onChange={(e) => dispatch({ type: "SET_NAME", payload: e.target.value })}
/>
<ErrorMessage name="name" />
If I try to log the 'name', it works just fine, but as I leave the field it gives me the error message as if nothing had been typed.
I can't seem to work out how to use Formik along with useReducer or how to pass its info to other components
Formik handles forms in state so you might have to implement your form like this to help you send your input data to redux reducer: I hope this example helps
import React from "react";
import { Formik } from "formik";
import { useDispatch } from 'react-redux';
const MyForm = () => {
const dispatch = useDispatch();
return (
<Formik
initialValues={{ email: "" }}
onSubmit={async values => {
await new Promise(resolve => setTimeout(resolve, 500));
alert(JSON.stringify(values, null, 2));
}}
>
{props => {
const {
values,
touched,
errors,
dirty,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
handleReset
} = props;
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email" style={{ display: "block" }}>
Email
</label>
<input
id="email"
placeholder="Enter your email"
type="text"
value={values.email}
onChange={(e) => {
console.log(e.target.value);
// send input data to formik
handleChange(e);
// dispatch to reducer
dispatch({ type: "SET_NAME", payload: e.target.value });
}}
onBlur={handleBlur}
className={
errors.email && touched.email
? "text-input error"
: "text-input"
}
/>
{errors.email && touched.email && (
<div className="input-feedback">{errors.email}</div>
)}
<button
type="button"
className="outline"
onClick={handleReset}
disabled={!dirty || isSubmitting}
>
Reset
</button>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
<pre
style={{
background: '#f6f8fa',
fontSize: '.65rem',
padding: '.5rem',
}}
>
<strong>props</strong> ={' '}
{JSON.stringify(formik.values, null, 2)}
</pre>
</form>
);
}}
</Formik>
)
};
export default MyForm;

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