react-hook-form handling server-side errors in handleSubmit - reactjs

I'm having a hard time figuring out how to handle errors that don't necessarily pertain to a single input field in a react-hook-form.
To put it differently, how do I handle handleSubmit errors?
For example, having the following form:
import to from 'await-to-js'
import axios, { AxiosResponse } from 'axios'
import React from "react"
import { useForm } from "react-hook-form"
type LoginFormData = {
username: string,
password: string,
}
export const Login: React.FC = () => {
const { register, handleSubmit } = useForm<LoginFormData>()
const onSubmit = handleSubmit(async (data) => {
const url = '/auth/local'
const [err, userLoginResult] = await to<AxiosResponse>(axios.post(url, data))
if (userLoginResult) {
alert('Login successful')
}
else if (err) {
alert('Bad username or password')
}
})
return (
<div className="RegisterOrLogIn">
<form onSubmit={onSubmit}>
<div>
<label htmlFor="username">username</label>
<input name="username" id="username" ref={register} />
</div>
<div>
<label htmlFor="password">Password</label>
<input type="password" id="password" name="password" ref={register} />
</div>
<button type="submit"> </button>
</form>
</div>
)
}
Is there a react-hook-form way of informing the user that there's an error with either the username or the password?
as in, other than alert()
Perhaps this is answered elsewhere, but I could not find it.
Clarification
The error received from the server does not pertain to a single field:
{
"statusCode": 400,
"error": "Bad Request",
"message": [
{
"messages": [
{
"id": "Auth.form.error.invalid",
"message": "Identifier or password invalid."
}
]
}
],
"data": [
{
"messages": [
{
"id": "Auth.form.error.invalid",
"message": "Identifier or password invalid."
}
]
}
]
}

In order to display the error from the server to your user, you need to use:
setError to set the error programmatically when the server returns an error response.
errors to get the error state of every fields in your form to display to the user.
type FormInputs = {
username: string;
};
const { setError, formState: { errors } } = useForm<FormInputs>();
In your handleSubmit callback
axios
.post(url, data)
.then((response) => {
alert("Login successful");
})
.catch((e) => {
const errors = e.response.data;
if (errors.username) {
setError('username', {
type: "server",
message: 'Something went wrong with username',
});
}
if (errors.password) {
setError('password', {
type: "server",
message: 'Something went wrong with password',
});
}
});
In your component
<label htmlFor="username">username</label>
<input id="username" {...register("username")} />
<div>{errors.username && errors.username.message}</div>
Live Demo

Inspired by #NearHuscarl's answer, I've done the following hack s.t. changes in either the username or the password inputs will remove the single error.
This hack does not scale well if your error is related to multiple fields in the form, but it worked for the login use case.
onSubmit:
const onSubmit = handleSubmit(async (data) => {
const url = '/auth/local'
const [err, userLoginResult] = await to<AxiosResponse>(axios.post(url, data)) // see await-to-js
if (userLoginResult) {
alert('Login successful')
}
else if (err) {
const formError = { type: "server", message: "Username or Password Incorrect" }
// set same error in both:
setError('password', formError)
setError('username', formError)
}
})
component:
return (
<div className="RegisterOrLogIn">
<form onSubmit={onSubmit}>
<div>
<label htmlFor="username">username</label>
<input name="username" id="username" ref={register} />
</div>
<div>
<label htmlFor="password">Password</label>
<input type="password" id="password" name="password" ref={register} />
</div>
<div>{errors.username && errors.password?.message /*note the cross check*/}</div>
<button type="submit"> </button>
</form>
</div>
)
by setting and rendering the error on both errors.password & errors.username, the error will disappear when the user updates either of those fields.

Related

i want to show details on same page

i am developing an application i.e supply chain management application on reactJS, NodeJS and blockchain.
Frontend code:
import React, { Component } from 'react'
import { useState, useEffect } from "react";
import axios from "axios";
import { useNavigate } from 'react-router-dom';
const SignUp = () => {
const navigate = useNavigate();
const flag=0;
const [data, setData] = useState({
uname: "",
email: "",
location: "",
budget: "",
password: ""
});
const handleChange = (e) => {
const value = e.target.value;
setData({
...data,
[e.target.name]: value
});
};
const handleSubmit = (e) => {
e.preventDefault();
const userData = {
uname: data.uname,
email: data.email,
location: data.location,
budget: data.budget,
password: data.password
};
axios
.post("http://localhost:8080/api/signup/", userData)
.then((response) => {
console.log(response);
})
.catch((error) => {
if (error.response) {
console.log(error.response);
console.log("server responded");
} else if (error.request) {
console.log("network error");
} else {
console.log(error);
}
});
navigate(`/home`)
};
return (
<form>
<h3>Sign Up</h3>
<div className="mb-3">
<label>User Name</label>
<input
type="text"
name="uname"
value={data.uname}
className="form-control"
placeholder="User name"
onChange={handleChange}
/>
</div>
<div className="mb-3">
<label>Email address</label>
<input
type="email"
name="email"
value={data.email}
className="form-control"
placeholder="Enter email"
onChange={handleChange}
/>
</div>
<div className="mb-3">
<label>Location</label>
<input
type="text"
name="location"
value={data.location}
className="form-control"
placeholder="Location"
onChange={handleChange}
/>
</div>
<div className="mb-3">
<label>Budget</label>
<input
type="Number"
name="budget"
value={data.budget}
className="form-control"
placeholder="Budget"
onChange={handleChange}
/>
</div>
<div className="mb-3">
<label>Password</label>
<input
type="password"
name="password"
value={data.password}
className="form-control"
placeholder="Enter password"
onChange={handleChange}
/>
</div>
<div className="d-grid">
<button type="submit" onClick={handleSubmit}className="btn btn-primary">
Sign Up
</button>
</div>
<p className="forgot-password text-right">
Already registered sign in?
</p>
</form>
);
};
export default SignUp;
here if user successfully registered then i want to show deatils of the user on the same page. how should i do that?
i have attached the code and the screenshot of the page.
currently i am on my account page.
Inside of your handle submit
You can just navigate after the axios.then callback
Or if you want the behavior to be that user submits -> register success -> show success -> then redirect, you can setTimeout for say 1000ms and then navigate.
axios
.post("http://localhost:8080/api/signup/", userData)
.then((response) => {
console.log(response);
})
.then(() => {
setTimeout(() => navigate(`/home`), 1000);
}
.catch((error) => {
if (error.response) {
console.log(error.response);
console.log("server responded");
} else if (error.request) {
console.log("network error");
} else {
console.log(error);
}
});
If you mean, show the user data after a successful registration and assuming you're calling an api to register the user and you're getting the user details back on success, you can handle that in your handleSubmit method.
Here's an example
const showUserDetails = (userDetails) => {
// Code that shows user details
// Probably using state
};
const handleSubmit = (e) => {
e.preventDefault();
const userData = {
...
axios
.post("http://localhost:8080/api/signup/", userData)
.then((response) => {
// handle here
showUserDetails(response);
})
.catch((error) => {
if (error.response) {
...
} else {
console.log(error);
}
});
};

How to display the message from controller (BE) in toast notification (toastify)

Summary:
I am trying to display a message from controller (backend) in notification (toast) using (react-toastify). And then whenever i try to login (cliking the login button) with a wrong e-mail, a toast appears with a message that we can find in the controller file (exemple: 'User with email does not exist' or 'Please enter all fields') ...etc, the message may vary depending on the status or message ofcourse.
My code (Back):
back/controllers/user.js:
login: (req, res) => {
const { email, password } = req.body
if (!email || !password)
return res.status(400).json({ message: 'Please enter all fields' }) <=== this is the message i want to display in toast.
User.findOne({ email }, async (err, user) => {
if (!user) {
res.status(403).json({ message: 'User with email does not exist' }) <=== this is the message i want to display in toast.
console.log('wrongmail');
} else {
const ismatch = await bcrypt.compare(password, user.password)
if (ismatch) {
console.log('ismatch');
const token = signToken(user._id, user.role);
res.cookie("access_token", token, { maxAge: 3600 * 1000, httpOnly: true, sameSite: true });
return res.status(200).json({ isAuthenticated: true, role: user.role })
} else {
res.status(403).json({ message: 'Invalid password !' }) <=== this is the message i want to display in toast.
}
}
})
},
My code (Front):
Front/src/views/Login/index.jsx:
import React, { useState } from 'react'
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
export default () => {
const notify = () => toast(""); <=== What should i put here to be rendred?
return (
<div className="col-lg-6">
<fieldset>
<input value={email} onChange={(e) => setemail(e.target.value)} type="text" name="email" id="email" pattern="[^ #]*#[^ #]*" placeholder="Your Email" required />
</fieldset>
<fieldset>
<input value={password} onChange={(e) => setpassword(e.target.value)} type="password" name="password" id="subject" placeholder="password" autoComplete="on" />
</fieldset>
<fieldset>
<button type="submit" id="form-submit" className="main-button" onClick={notify}>Login</button>
<ToastContainer />
</fieldset>
</div>
)
}
I added the gist code in my Login/index.jsx that i found it here: https://fkhadra.github.io/react-toastify/installation, but i can't manage to find a way to call any of messages from controller file (BE) to render as i explained at first in summary.
You should use the toast method inside the function where you get the response from BE.
for example:
getUserDetails() {
//call backend here
try {
const data = result_from_backend
toast.success('your toast message')
} catch (e) {
toast.error('show the error here');
}
}

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

Better way to check input values of a form in React/Next Js

I've written a simple form for my web application. I plan on writing a feature that checks to make sure all fields are non-empty and valid, and displaying an error message as a component if not. This is the skeleton:
import { useState } from 'react';
import emailjs from "emailjs-com";
import apiKeys from "../public/credentials/apikeys";
import ContactStyles from "../public/styles/ContactStyles";
function ContactForm() {
const [fieldDict, setFieldDict] = useState(
{
name: "",
email: "",
subject: "",
message: ""
});
function sendEmail(e) {
e.preventDefault();
// TODO: Show success or error message
var blankField = false;
if (fieldDict.name.length === 0) {
console.log("No name"); // To be implemented here and on subsequent lines
blankField = true;
}
if (fieldDict.email.length === 0) {
console.log("No email");
blankField = true;
}
if (fieldDict.subject.length === 0) {
console.log("No subject");
blankField = true;
}
if (fieldDict.message.length === 0) {
console.log("No message");
blankField = true;
}
if (blankField) { return }
emailjs.sendForm(apiKeys.serviceID, apiKeys.templateID, e.target, apiKeys.userID)
.then((result) => {
console.log(result, fieldDict);
}, (error) => {
console.log(error, fieldDict);
});
e.target.reset();
}
return (
<div className="contact-section">
<div className="contact-container">
<h5 className="form-header">Send me an email!</h5>
<form className="contact-form" onSubmit={sendEmail}>
<div className="form-group">
<label className="label">Name</label>
<input className="input" type="text" name="name" autoComplete="off"
onInput={e => {
setFieldDict(prevFieldDict => ({...prevFieldDict, name: e.target.value}));
}}/>
</div>
<div className="form-group">
<label className="label">Email</label>
<input className="input" type="email" name="email" autoComplete="off"
onInput={e => {
setFieldDict(prevFieldDict => ({...prevFieldDict, email: e.target.value}));
}}/>
</div>
<div className="form-group">
<label className="label">Subject</label>
<input className="input" type="subject" name="subject" autoComplete="off"
onInput={e => {
//? Viability of this method?
setFieldDict(prevFieldDict => ({...prevFieldDict, subject: e.target.value}));
}}/>
</div>
<div className="form-group">
<label className="label">Message</label>
<textarea className="input textarea" name="message" rows="6"
onInput={e => {
setFieldDict(prevFieldDict => ({...prevFieldDict, message: e.target.value}));
}}/>
</div>
<div className="submit">
<button type="submit" value="Send">Submit</button>
</div>
</form>
</div>
<style jsx global>{ContactStyles}</style>
</div>
);
}
export default ContactForm;
With every keystroke, the corresponding key (name, email, subject, message) gets updated.
Question
Is there a better way to do this? A more efficient way to do this seems to be only updating the fieldDict dictionary when the use hits submit, but based on how react renders components does this really matter?
Note: The code as is works just fine, I'm just asking if there is a better way to do this? If you want a better way of seeing this, change the content of onInput{...} to e => console.log(e.target.value).
Any insight is much appreciated!
You can iterate through your dict and filter all names that are falsable.
function sendEmail(e) {
e.preventDefault();
const blankFields = Object.keys(fieldDict).filter(fieldName => !fieldDict[fieldName])
blankFields.forEach(fieldName => console.log(`no ${fieldName}`);
if (blankFields.length) { return }
emailjs.sendForm(apiKeys.serviceID, apiKeys.templateID, e.target, apiKeys.userID)
.then((result) => {
console.log(result, fieldDict);
}, (error) => {
console.log(error, fieldDict);
});
e.target.reset();
}

How to test dynamic react components

I'm new to unit testing and I am trying to test validation messages on my login form. When the user enters incorrect information and submits the form, an error message component is displayed. When I try to test if this error component exists I get a falsy response. Any help would be appreciated.
onSubmit = () => {
const errors = this.validate(this.state.data);
this.setState({ errors });
console.log("test");
};
validate = data => {
const errors = {};
if (!Validator.isEmail(data.email)) errors.email = "Invalid Email";
if (!data.password) errors.password = "Password required";
return errors;
};
render() {
const { data, errors } = this.state;
return (
<div>
<Form onSubmit={this.onSubmit}>
<Form.Field>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
placeholder="example#example.com"
value={data.email}
onChange={this.onChange}
/>
{errors.email && <InlineError text={errors.email} />}
</Form.Field>
<Form.Field>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
name="password"
value={data.password}
onChange={this.onChange}
/>
</Form.Field>
<Button type="submit" primary>
Login
</Button>
</Form>
</div>
);
}
and my LoginForm.test.js file
describe("user submits form", () => {
it("Returns no errors with correct data", () => {
const data = {
email: "dennis#gmail.com",
password: "12345"
};
wrapper.find("Button").simulate("click");
expect(wrapper.instance().validate(data)).toEqual({});
});
it("Returns error when error exists", () => {
const data = {
email: "",
password: "12345"
};
wrapper.find("Button").simulate("click");
expect(wrapper.instance().validate(data)).toEqual({
email: "Invalid Email"
});
});
it("Displays InlineError when error exists", () => {
const data = {
email: "",
password: "12345"
};
wrapper.find("Button").simulate("click");
expect(wrapper.contains(<InlineError text="Invalid Email" />)).toBe(true);
});
});
The first two tests pass however the third one fails
For one thing, in the first two tests you are testing the validate method only. The click simulation is doing something, but it does not actually affect the test outcome.
The reason third test fails is likely that setState is async. That means you also have to make your expectation async.

Resources