Displaying Value of setFieldErrors Using ErrorMessage Component in Formik - reactjs

See Update Below
I have a Login Form Component that I built using Formik that syncs with Firebase Authentication. I have set it up so that I can display errors from Firebase using the setFieldError prop. Here is the relevant sections of the code:
export const LoginForm = () => {
async function authenticateUser(values, setFieldError) {
const { email, password } = values
try {
await firebase.login(email, password)
navigate('/', { replace: true })
} catch (error) {
console.log('Authentication Error: ', error)
await setFieldError('firebaseErrorMessage', error.message)
}
}
return (
<>
<h1>Form</h1>
<Formik
render={props => <RenderForm {...props} />}
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={async (
values,
{ setFieldError, setSubmitting, resetForm }
) => {
setSubmitting(true)
authenticateUser(values, setFieldError)
setSubmitting(false)
resetForm()
}}
/>
</>
)
}
const RenderForm = ({ errors, isSubmitting, isValid }) => (
<Form>
<h3>Sign Up</h3>
<Email name="email" />
<Password name="password" />
<Button disabled={!isValid || isSubmitting} type="submit">
Submit
</Button>
{errors.firebaseErrorMessage && <p>{errors.firebaseErrorMessage}</p>}
</Form>
)
Now, this works just fine. However, if I try to display the error message using Formik's ErrorMessage component, then the message does not show up.
In other words, this works:
{errors.firebaseErrorMessage && <p>{errors.firebaseErrorMessage}</p>}
This does not work:
<ErrorMessage name="firebaseErrorMessage" />
Any idea why this doesn't work and how to get it to work?
Thanks.
UPDATE
Here are my initial values:
const initialValues = {
email: '',
password: '',
}

I don't think that you should use Formik's error for your Firebase error. Formik's errors are meant for validating form inputs.
To store and reference an API error, you can use Formik's status object. Handling API errors is the example he gives for status.
I think the issue is that, in Formik, name is meant to refer to the name of an input. Instead, you're imperatively adding a new name property to the errors object using setFieldError, but firebaseErrorMessage isn't a field in your form. (Share your initialValues object to verify this.)
One annoying part of this is that there is probably some styling associated with <ErrorMessage> that you can't leverage directly. But, in my opinion, it's probably more important to have your system structured correctly, and then you can mimic styles as-needed.
Here's my code suggestion:
const RenderForm = ({ isSubmitting, isValid, status }) => (
<Form>
<h3>Sign Up</h3>
<Email name="email" />
<Password name="password" />
<Button disabled={!isValid || isSubmitting} type="submit">
Submit
</Button>
{status.firebaseErrorMessage && <p>{status.firebaseErrorMessage}</p>}
</Form>
);
export const LoginForm = () => {
async function authenticateUser(values, setStatus, setSubmitting) {
const { email, password } = values;
setSubmitting(true);
try {
await firebase.login(email, password);
navigate("/", { replace: true });
} catch (error) {
console.log("Authentication Error: ", error);
setStatus({
firebaseErrorMessage: error.message
});
} finally {
setSubmitting(false);
}
}
return (
<>
<h1>Form</h1>
<Formik
render={props => <RenderForm {...props} />}
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={async (values, { setStatus, setSubmitting, resetForm }) => {
await authenticateUser(values, setStatus, setSubmitting);
resetForm();
}}
/>
</>
);
};

Related

Why won't the form state visibly change within the submit button using react form hook?

So I have a sign up form using react-hook-form and I want to make the submit input disabled and display a "Signing in..." message. I've console logged the isSubmitting value within the render and that shows true when I submit and then false not long after however the submit button within the form never updates to reflect the isSubmitting status.
What am I doing wrong? Here is the React Hook Form useFormState docs
From what I can see it should work?
Thanks in advance.
import { useState } from "react"
import { useForm, useFormState } from "react-hook-form"
import useAuth from "Hooks/useAuth"
const SignInForm = () => {
const [firebaseError, setFirebaseError] = useState(null)
const { signIn } = useAuth()
const {
register,
handleSubmit,
resetField,
control,
formState: { errors },
} = useForm()
const { isSubmitting, isValidating } = useFormState({ control })
const onSubmit = (data) => {
signIn(data.email, data.password)
.then((response) => console.log(response))
.catch((error) => {
let message = null
if (error.code === "auth/too-many-requests") {
message =
"Too many unsuccessful attempts, please reset password or try again later"
}
if (error.code === "auth/wrong-password") {
message = "Incorrect password, please try again"
}
if (error.code === "auth/user-not-found") {
message = "User does not exist, please try again"
}
resetField("password")
setFirebaseError(message)
})
}
return (
<form
className="signupForm"
onSubmit={handleSubmit(onSubmit)}
autoComplete="off"
>
{console.log(isSubmitting)}
{firebaseError && (
<p className="form-top-error has-text-danger">{firebaseError}</p>
)}
<div className="field">
<input
type="text"
className="input formInput"
placeholder="Email"
{...register("email", {
required: {
value: true,
message: "Field can not be empty",
},
pattern: {
value:
/^(([^<>()[\]\\.,;:\s#"]+(\.[^<>()[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
message: "Invalid email",
},
})}
/>
{errors.email && (
<span className="is-block has-text-danger is-size-7">
{errors.email?.message}
</span>
)}
</div>
<div className="field">
<input
type="password"
className="input formInput"
placeholder="Password"
{...register("password", {
required: "Field can not be empty",
minLength: {
value: 6,
message: "Must be longer than 6 characters",
},
})}
/>
{errors.password && (
<span className="is-block has-text-danger is-size-7">
{errors.password?.message}
</span>
)}
</div>
<input
type="submit"
className="button is-info"
value={isSubmitting ? "Signing In..." : "Sign In"}
disabled={isSubmitting}
/>
</form>
)
}
export default SignInForm
I think you need to refactor your onSubmit function to make it async so isSubmitting will stay true during your signIn call.
const onSubmit = async (data) => {
await signIn(data.email, data.password)
.then((response) => console.log(response))
.catch((error) => {
let message = null
if (error.code === "auth/too-many-requests") {
message =
"Too many unsuccessful attempts, please reset password or try again later"
}
if (error.code === "auth/wrong-password") {
message = "Incorrect password, please try again"
}
if (error.code === "auth/user-not-found") {
message = "User does not exist, please try again"
}
resetField("password")
setFirebaseError(message)
})
}
onSubmit needs to return a Promise for formState to update correctly.
const onSubmit = (payload) => {
// You need to return a promise.
return new Promise((resolve) => {
setTimeout(() => resolve(), 1000);
});
};
References:
https://react-hook-form.com/api/useform/formstate/
https://github.com/react-hook-form/react-hook-form/issues/1363#issuecomment-610681167

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>

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.

How do I access current value of a formik field without submitting?

How do I access value of the SelectField named countryCode in my React component? Use case is that validation scheme should change according to the countryCode.
<Formik
onSubmit={(values, actions) => this.onSubmit(values, actions.setFieldError)}
validationSchema={() => this.registrationValidationSchema()}
enableReinitialize={true}
initialValues={this.props.initialData}
>
<Form>
<Field
name="countryCode"
component={SelectField}
label="Country"
labelClassName="required"
options={Object.entries(sortedCountryList).map(x => ({
value: x[1][1],
label: x[1][0]
}))}
/>
</Form>
</Formik>
I have tried to access it via a ref, then via this.props.values (as suggested in getFieldValue or similar in Formik) but both return just undefined or null. My props don't have any "values" field.
EDIT: Found an ugly way: document.getElementsByName("countryCode")[0].value. A better way is appreciated.
You can use ref, if you need the values outside formik
const ref = useRef(null);
const someFuncton = () => {
console.log(ref.current.values)
}
<Formik
innerRef={ref}
onSubmit={(values, actions) => this.onSubmit(values,
actions.setFieldError)}
validationSchema={() => this.registrationValidationSchema()}
enableReinitialize={true}
initialValues={this.props.initialData}
/>
<form></form>
</Formik>
You can access values like this:
<Formik
onSubmit={(values, actions) => this.onSubmit(values,
actions.setFieldError)}
validationSchema={() => this.registrationValidationSchema()}
enableReinitialize={true}
initialValues={this.props.initialData}
>
{({
setFieldValue,
setFieldTouched,
values,
errors,
touched,
}) => (
<Form className="av-tooltip tooltip-label-right">
// here you can access to values
{this.outsideVariable = values.countryCode}
</Form>
)}
</Formik>
you can get it from formik using the Field comp as a wrapper
import React, { ReactNode } from 'react';
import { Field, FieldProps } from 'formik';
(...other stuffs)
const CustomField = ({
field,
form,
...props
}) => {
const currentError = form.errors[field.name];
const currentField = field.name; <------ THIS
const handleChange = (value) => {
const formattedDate = formatISODate(value);
form.setFieldValue(field.name, formattedDate, true);
};
const handleError = (error: ReactNode) => {
if (error !== currentError) {
form.setFieldError(field.name, `${error}`);
}
};
return (
<TextField
name={field.name}
value={field.value}
variant="outlined"
helperText={currentError || 'happy helper text here'}
error={Boolean(currentError)}
onError={handleError}
onChange={handleChange}
InputLabelProps={{
shrink: true,
}}
inputProps={{
'data-testid': `${field.name}-test`, <---- very helpful for testing
}}
{...props}
/>
</MuiPickersUtilsProvider>
);
};
export default function FormikTextField({ name, ...props }) {
return <Field variant="outlined" name={name} component={CustomField} fullWidth {...props} />;
}
it is very simple just do console.log(formik.values) and you will get all the values without submitting it.

understanding Formik and React

This is probably not the best place to post this question but where, then?
The code below is taken from Formik's overview page and I'm very confused about the onSubmit handlers:
The form element has an onSubmit property that refers to handleSubmit which is passed on that anonymous function : <form onSubmit={handleSubmit}>. Where does that come from?
The Formik component has an onSubmit property as well:
onSubmit={(values, { setSubmitting }) => { ... }
How do these relate to each other? What is going on?
import React from 'react';
import { Formik } from 'formik';
const Basic = () => (
<div>
<h1>Anywhere in your app!</h1>
<Formik
initialValues={{ email: '', password: '' }}
validate={values => {
let errors = {};
if (!values.email) {
errors.email = 'Required';
} else if (
!/^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
) {
errors.email = 'Invalid email address';
}
return errors;
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 400);
}}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
/* and other goodies */
}) => (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
onChange={handleChange}
onBlur={handleBlur}
value={values.email}
/>
{errors.email && touched.email && errors.email}
<input
type="password"
name="password"
onChange={handleChange}
onBlur={handleBlur}
value={values.password}
/>
{errors.password && touched.password && errors.password}
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</form>
)}
</Formik>
</div>
);
export default Basic;
The component takes onSubmit as a prop where you can execute code you want to perform when you submit your form. This prop is also given some arguments such as values (values of the form) for you to use in your onSubmit function.
The handleSubmit form is auto generated from the Formik library that automates some common form logics explained here. The handleSubmit will automatically execute onSubmit function mentioned above as part of its phases (pre-submit, validation, submission). Hope that answers your question!

Resources