The React Mega-Tutorial, Chapter 7: Forms and Validation
Posted by
on underA big part of most web applications is accepting, validating and processing user input. In this chapter you are going to learn how to perform these tasks with React by creating the user registration and login forms for Microblog.
The complete course, including videos for every chapter is available to order from my courses site. Ebook and paperback versions of this course are also available from Amazon. Thank you for your support!
For your reference, here is the complete list of articles in this series:
- Introducing the React Mega-Tutorial
- Chapter 1: Modern JavaScript
- Chapter 2: Hello, React!
- Chapter 3: Working with Components
- Chapter 4: Routing and Page Navigation
- Chapter 5: Connecting to a Back End
- Chapter 6: Building an API Client
- Chapter 7: Forms and Validation (this article)
- Chapter 8: Authentication
- Chapter 9: Application Features
- Chapter 10: Memoization
- Chapter 11: Automated Testing
- Chapter 12: Production Builds
Introduction to Forms with React-Bootstrap
The React-Bootstrap documentation has a section on forms that starts with a nice example of a login form. You can see how this example form looks in Figure 7.1.

Below is a portion of the HTML source code that generates this form. For simplicity, I have only included the first field.
<Form>
<Form.Group className="mb-3" controlId="formBasicEmail">
<Form.Label>Email address</Form.Label>
<Form.Control type="email" placeholder="Enter email" />
<Form.Text className="text-muted">
{"We'll never share your email with anyone else."}
</Form.Text>
</Form.Group>
// ... more fields here
</Form>
The Form
component from React-Bootstrap is used as the top-level parent of the form. Several subcomponents of Form
are used to create the different parts of each field:
Form.Label
defines the labelForm.Control
defines the actual input fieldForm.Text
defines a message that appears below the field
As you see, defining an input field involves a decent amount of boilerplate, and having to repeat all this for every input field in every form is not ideal. You can probably guess that a better idea is to create a reusable input field component.
A Reusable Form Input Field
Below is the definition of a new component called InputField
.
src/components/InputField.js: A generic form input field
import Form from 'react-bootstrap/Form';
export default function InputField(
{ name, label, type, placeholder, error, fieldRef }
) {
return (
<Form.Group controlId={name} className="InputField">
{label && <Form.Label>{label}</Form.Label>}
<Form.Control
type={type || 'text'}
placeholder={placeholder}
ref={fieldRef}
/>
<Form.Text className="text-danger">{error}</Form.Text>
</Form.Group>
);
}
This component accepts several props, to allow for the most flexibility in rendering the field. The parent component must pass the field name, the label text, the field type (text, password, etc.), the placeholder text that appears inside the field when it is empty, and a validation error message, which will appear below the field.
The props are all included as JSX template expressions in the rendered input field. There are a few minor differences between this field and the example one from React-Bootstrap:
- The label is omitted if the
label
prop was not passed by the parent component - The
type
prop is optional, and defaults totext
when not given by the parent - The error message uses a
text-danger
style from Bootstrap, which renders the text in red - The
placeholder
anderror
props are optional, and will render as empty text when not provided by the parent
There is one more prop in this component called fieldRef
, which is in turn passed to the Form.Control
component as a ref
prop. A reference provides a way for the application to interact with a rendered element. You will learn about React references in detail later in this chapter.
The top-level component in the input field was given the InputField
class name to make it easier to customize how it looks on the page. Below is a small CSS addition to index.css that adds top and bottom margins to this component.
src/index.css: Styles for the input field
.InputField {
margin-top: 15px;
margin-bottom: 15px;
}
The Login Form
Using the generic input field defined above it is now possible to build the Login page in src/pages/LoginPage.js.
src/pages/LoginPage.js: Login page
import { useState } from 'react';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import Body from '../components/Body';
import InputField from '../components/InputField';
export default function LoginPage() {
const [formErrors, setFormErrors] = useState({});
const onSubmit = (ev) => {
ev.preventDefault();
console.log('handle form here');
};
return (
<Body>
<h1>Login</h1>
<Form onSubmit={onSubmit}>
<InputField
name="username" label="Username or email address"
error={formErrors.username} />
<InputField
name="password" label="Password" type="password"
error={formErrors.password} />
<Button variant="primary" type="submit">Login</Button>
</Form>
</Body>
);
}
The page uses the Body
component as its main wrapper, as all other pages. The login page will not offer navigation links, so for that reason the sidebar
prop is not included.
The Form
component has a onSubmit
prop, which configures a handler function that will be invoked when the form is submitted. The handler starts by doing something very important: it disables the browser's own form submission logic, by calling the ev.preventDefault()
method of the event object that was passed as an argument. This is necessary to prevent the browser from sending a network request with the form data.
This version of the component does not implement the form handling logic yet, it just logs a message to the browser's console to confirm that the event was received. The actual form submission code will be added in the following sections.
The form implements a standard two field login form, using the InputField
component defined above. The name
, label
and error
props are passed for both fields. For the first field type
isn't passed, so the default type of text
is used. The password field needs to have the type
explicitly set, so that the characters are not visible. The placeholder
prop is not used in any of the fields in this form.
The error
props in both input fields is set to an attribute of a formErrors
state variable that is defined inside the component. The state variable is initialized as an empty object, so the error messages for both fields are going to be initially set to undefined
, which means they are not going to render any visible text. You will learn how to make validation errors visible later.
The submit button is defined directly with the Button component from React-Bootstrap.
Figure 7.2 shows how the login page looks.

Controlled and Uncontrolled Components
An important part of implementing a form is to be able to read what the user enters in the form fields. There are two main techniques to handle user input in React, using controlled or uncontrolled components.
A controlled component is coded with event handlers that catch all changes to input fields. Every time a change handler triggers for a field, the updated contents of the field are copied to a React state variable. With this method, the values of the input fields for a form can be obtained from a state variable that acts as a mirror of the field.
An uncontrolled component, on the other side, does not have its value tracked by React. When the field's data is needed, DOM APIs are used to obtain it directly from the element.
An often cited disadvantage of the controlled method, especially for forms with large number of fields, is that every form field needs a state variable and one or more event handlers to capture all the changes the user can make to them, making them tedious to write. Uncontrolled components also have some boilerplate code required, but overall they need significantly less code.
For this project, the uncontrolled method will be used in all forms.
Accessing Components through DOM References
When working with vanilla JavaScript, the standard method to reference an element is to give it an id
attribute, which then makes it possible to retrieve the element with the document.getElementById()
function. In complex applications it is difficult to maintain unique id
values for all the elements that need to be addressed on the page, and it is easy to inadvertently introduce duplicates.
React has a more elegant solution based on references. A reference eliminates the need to come up with a unique identifier for every element, a task that gets harder as the number of elements and page complexity grows.
A reference can be created with the useRef()
hook inside a component's render function:
export default function MyForm() {
const usernameField = useRef();
...
}
To associate this reference with an element rendered to the page, the ref
attribute is added to the element when it is rendered.
export default function MyForm() {
const usernameField = useRef();
return (
<form>
<input type="text" ref={usernameField} />
</form>
);
}
The reference object has a current
attribute that can be used in side effect functions and event handlers to access the actual DOM object associated with the component:
export default function MyForm() {
const usernameField = useRef();
const onSubmit = (ev) => {
ev.preventDefault();
alert('Your username is: ' + usernameField.current.value);
};
return (
<form onSubmit={onSubmit}>
<input type="text" ref={usernameField} />
</form>
);
}
Let's implement references for the two fields in the LoginPage
component.
As you recall, the InputField
component accepts a fieldRef
prop. The parent component can use this prop to pass a reference object. This reference is assigned to the ref
prop on the input field element. With this solution, the parent gets access the input field and can obtain its value when processing the form submission.
Why is the prop in the InputField
component called fieldRef
and not also ref
? The reason is that ref
is an attribute name that React handles in a special way, similar to the key
attribute, so these attributes cannot be used as prop names. If you feel strongly about passing references to a subcomponent using a prop named ref
, React provides a forwarding ref option that makes it possible.
The following listing shows the LoginPage
component updated to have references for the two input fields.
src/pages/LoginPage.js: References in the login page
import { useState, useEffect, useRef } from 'react';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import Body from '../components/Body';
import InputField from '../components/InputField';
export default function LoginPage() {
const [formErrors, setFormErrors] = useState({});
const usernameField = useRef();
const passwordField = useRef();
useEffect(() => {
usernameField.current.focus();
}, []);
const onSubmit = (ev) => {
ev.preventDefault();
const username = usernameField.current.value;
const password = passwordField.current.value;
console.log(`You entered ${username}:${password}`);
};
return (
<Body>
<h1>Login</h1>
<Form onSubmit={onSubmit}>
<InputField
name="username" label="Username or email address"
error={formErrors.username} fieldRef={usernameField} />
<InputField
name="password" label="Password" type="password"
error={formErrors.password} fieldRef={passwordField} />
<Button variant="primary" type="submit">Login</Button>
</Form>
</Body>
);
}
The usernameField
and passwordField
references are created at the start of the render function, and are passed as fieldRef
props to the two InputField
components, which in turn will set these references on the actual input elements of the form.
This new version of the LoginPage
component has a new side effect function that shows a nice trick that takes advantage of the new references. The function runs only the first time the login page is rendered (note that the dependency array is empty), and makes the first field in the form focused. This means that the user can start typing the username right away, without having to click on the field with the mouse first. The focus() method used here is part of the DOM API.
The onSubmit
event handler for the form retrieves the values of the referenced input fields in the DOM, and for now, logs them to the console so that you can verify that everything is working when the form is submitted.
Client-Side Field Validation
When the user submits the form, the application must perform validation of all the form fields. It is important that all the data that is submitted to the server is validated there, because validation tasks performed in the client are easy to bypass by malicious users.
With the purpose of making applications more responsive, it is common for clients to perform complementary validation tasks in the client, with the goal to catch the most basic errors without having to make a trip to the server and back. For example, checks can be added to ensure that both the username and password fields are not empty before submitting the form. The server will check this again, but it is simple enough to check in the client as well.
The formErrors
state variable added to the LoginPage
component earlier is used to hold validation error messages for form fields. The listing below shows the additions to the onSubmit
handler in this component that are necessary to validate that the username and password fields are not empty when the form is submitted.
src/pages/LoginPage.js: Validate input fields
const onSubmit = (ev) => {
ev.preventDefault();
const username = usernameField.current.value;
const password = passwordField.current.value;
const errors = {};
if (!username) {
errors.username = 'Username must not be empty.';
}
if (!password) {
errors.password = 'Password must not be empty.';
}
setFormErrors(errors);
if (Object.keys(errors).length > 0) {
return;
}
// TODO: log the user in
};
The errors
constant is initialized to an empty object, and populated with username
and/or password
keys when any of these fields are found to be empty. The values inserted under these keys are the text of the error messages. These values are already passed as the error
props on the InputField
components.
After the errors
object is populated, it is set as the updated value of the formErrors
state variable. When the value of the state value changes, React will re-render the component, and during the re-render, the error messages will be displayed below each corresponding field. If there is at least one error, the function returns early, to prevent any actual actions defined later to execute when the form submission was deemed invalid.
After incorporating these changes, navigate to http://localhost:3000/login on your browser and try submitting the form with empty and non-empty fields to see how the client-side validation provides immediate feedback. Figure 7.3 shows the form after it was submitted with both fields empty.

The User Registration Form
You will learn how to complete the implementation of the login form in the next chapter, when the topic of user authentication is discussed. In preparation for that work, in this section you'll implement another important form, the one dedicated to registering new users into the system.
The new form will be in a RegistrationPage
component, with the same structure as LoginPage
. You can see it below.
src/pages/RegistrationPage.js: A user registration page
import { useState, useEffect, useRef } from 'react';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import Body from '../components/Body';
import InputField from '../components/InputField';
export default function RegistrationPage() {
const [formErrors, setFormErrors] = useState({});
const usernameField = useRef();
const emailField = useRef();
const passwordField = useRef();
const password2Field = useRef();
useEffect(() => {
usernameField.current.focus();
}, []);
const onSubmit = async (event) => {
// TODO
};
return (
<Body>
<h1>Register</h1>
<Form onSubmit={onSubmit}>
<InputField
name="username" label="Username"
error={formErrors.username} fieldRef={usernameField} />
<InputField
name="email" label="Email address"
error={formErrors.email} fieldRef={emailField} />
<InputField
name="password" label="Password" type="password"
error={formErrors.password} fieldRef={passwordField} />
<InputField
name="password2" label="Password again" type="password"
error={formErrors.password2} fieldRef={password2Field} />
<Button variant="primary" type="submit">Register</Button>
</Form>
</Body>
);
}
The registration form has four fields for username, email, password and password confirmation. These fields all have a reference assigned to them. A formErrors
state variable is added on this form as well, to keep track of validation error messages. The first field of the form is given the focus with a side effect function, as in LoginPage
.
This page needs to be associated with the /register route in App.js.
src/App.js: Registration page route
... // <-- no changes to existing imports
import RegistrationPage from './pages/RegistrationPage';
export default function App() {
return (
<Container fluid className="App">
<BrowserRouter>
<ApiProvider>
<Header />
<Routes>
<Route path="/" element={<FeedPage />} />
<Route path="/explore" element={<ExplorePage />} />
<Route path="/user/:username" element={<UserPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegistrationPage />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</ApiProvider>
</BrowserRouter>
</Container>
);
}
The login page can include a link to the new registration page below the login form's submit button.
src/pages/LoginPage.js: Link to user registration page
... // <-- no changes to existing imports
import { Link } from 'react-router-dom';
export default function LoginPage() {
... // <-- no changes in the body of the function
return (
<Body>
<h1>Login</h1>
... // <-- no changes to the form
<hr />
<p>Don't have an account? <Link to="/register">Register here</Link>!</p>
</Body>
);
}
Form Submission and Server-Side Field Validation
Registering a user with Microblog API is a straightforward operation that only requires sending a POST
request to /api/users with the new user's username, email and chosen password. This request can be made through the MicroblogApiClient
instance, which can be accessed in this component through the useApi()
custom hook.
The listing below shows the changes to the registration page to support the form submission.
src/pages/RegistrationPage.js: Registration form submission
... // <-- no changes to existing imports
import { useNavigate } from 'react-router-dom';
import { useApi } from '../contexts/ApiProvider';
export default function RegistrationPage() {
... // <-- no changes to state variables and references
const navigate = useNavigate();
const api = useApi();
const onSubmit = async (event) => {
event.preventDefault();
if (passwordField.current.value !== password2Field.current.value) {
setFormErrors({password2: "Passwords don't match"});
}
else {
const data = await api.post('/users', {
username: usernameField.current.value,
email: emailField.current.value,
password: passwordField.current.value
});
if (!data.ok) {
setFormErrors(data.body.errors.json);
}
else {
setFormErrors({});
navigate('/login');
}
}
};
... // <-- no changes to returned JSX
}
In the body of the component function, the api
instance is imported with the custom useApi()
hook function. In addition to that, the navigate()
function from the React-Router package is included, through the useNavigate()
hook. This function is used to automatically redirect the user to the /login route after a successful user registration.
The validation of this form has two passes. First, a client-side validation pass is done in the onSubmit()
handler function. For this form, a check is done to ensure that the password
and password2
fields are equal. If an error is found, the formErrors
state variable is updated with an error message and the form submission is suspended.
The client-side validation could be expanded to also check that none of the fields are empty, that the email address is syntactically correct, and many other checks. But given that the back end performs all these checks already, an argument can be made that it is best to avoid repetition.
If the client-side validation of the two password fields does not detect any errors, then the api
instance is used to send the POST
request to the server. The first argument to api.post()
is the last part of the endpoint URL (after /api), and the second argument is an object with the request body, which is sent to the server in JSON format.
If the request fails due to validation, the Microblog API back end returns an error response. The body of the response includes detailed information about the error, and in particular, the errors.json
object contains validation error messages for each field. This is actually very convenient, because the formErrors
state variable uses the same format. To display the validation errors from the server, this object is set directly on the state variable. Figure 7.4 shows an example of server-side validation errors displayed on the form.
If the request succeeds, the formErrors
state variable is cleared of any previous errors, and then the navigate()
function from React-Router is used to issue a redirect to the login page.

The user registration functionality is now fully functional. You can register users by navigating to http://localhost:3000/register in your browser and submitting the registration form.
Flashing Messages to the User
If you tried to register a user, you must have noticed that after the user registration is processed, you are unceremoniously redirected to the login page, without any indication of success or failure.
A standard user interface pattern in web applications is to indicate the status of an operation by "flashing" a message to the user. A flashed message often appears at the top of the page, styled to call attention to it. In React-Bootstrap, the Alert component can create this type of user interface component.
Implementing message flashing in a reusable way presents an interesting challenge. To flash a message, a component needs to share the message to be flashed with the component that renders the alert message, which will very likely be in a different part of the component tree.
Before going into implementation specifics, let's think about a nice design for flashing a message. Below you can see how an example MyForm
component might flash a message after processing a form:
export default function MyForm() {
const { flash } = useFlash();
const onSubmit = (ev) => {
ev.preventDefault();
... // form processing here
flash('Your registration has been successful', 'success');
};
return ( ... );
}
}
This is a really nice pattern, because any component that needs to flash a message can just get the flash()
function from the useFlash()
hook, without having to worry about how or where the alert is going to render in the page.
But how do you implement something like this? React contexts are actually powerful enough to make a solution like this work. The component hierarchy that supports message flashing for the example MyForm
component above might look like this:
<FlashProvider>
<Alert />
<MyForm />
</FlashProvider>
And this is starting to look more familiar. The FlashProvider
component can share a context that includes a flash()
function, which child components such as the MyForm
of the example above can use to set the alert message. The context will also need to share the text and style of the message, so that the component in charge of rendering the alert, also a child of FlashProvider
, has the information it needs to do it.
The actual implementation has an additional complication omitted above. On a traditional, server-rendered application, a flashed message goes away as soon as the user navigates to a new page. Since real page navigation does not exist in a React application, an alert may end up being displayed for a long time, so a mechanism to hide an alert after a reasonable amount of time needs to be included in this solution.
The listing below shows the complete implementation of FlashProvider
, with support for automatically hiding the alert after a specified number of seconds.
src/contexts/FlashProvider.js: A Flash context
import { createContext, useContext, useState } from 'react';
export const FlashContext = createContext();
let flashTimer;
export default function FlashProvider({ children }) {
const [flashMessage, setFlashMessage] = useState({});
const [visible, setVisible] = useState(false);
const flash = (message, type, duration = 10) => {
if (flashTimer) {
clearTimeout(flashTimer);
flashTimer = undefined;
}
setFlashMessage({message, type});
setVisible(true);
if (duration) {
flashTimer = setTimeout(hideFlash, duration * 1000);
}
};
const hideFlash = () => {
setVisible(false);
};
return (
<FlashContext.Provider value={{flash, hideFlash, flashMessage, visible}}>
{children}
</FlashContext.Provider>
);
}
export function useFlash() {
return useContext(FlashContext).flash;
}
The overall structure of this component is similar to that of ApiProvider
. The FlashContext
object is created in the global scope, and the FlashContext.Provider
component is then rendered as a wrapper to the component's children. The value shared by this context is an object with four elements:
flash
is the function that components can use to flash a message to the page.hideFlash
is a function that updates the visible state of the flash message tofalse
.flashMessage
is an object withmessage
andtype
properties, defining the alert to display. Thetype
property can be any of the styling options supported by Bootstrap's alerts, for example:success
,danger
,info
, etc.visible
is the current visible state of the alert.
The value={{ ... }}
prop of the FlashContext.Provider
component has a syntax that may look strange. In React, when a prop needs to be assigned a value that is an object, two sets of braces are required. The outer pair of braces is what tells the JSX parser that the value of the prop is given as a JavaScript expression. The inner pair of braces are the object braces. The elements of the object, which are normally provided in key: value
format, are in this case given as simple variables, using the object property shorthand, which takes the key and the value from the same variable.
The component has two state variables. The flashMessage
state variable holds the message and the type of the current alert. The visible
variable is a boolean that keeps track of when the alert is displayed.
The flashTimer
global variable is going to manage a JavaScript timer instance that is created each time an alert is displayed, with the purpose of automatically hiding the alert after enough time has passed. You may wonder why this variable is declared as a global variable and not a state variable inside the component. State variables have the unique feature that they cause any components that use them to re-render when they change. The timer used by this component is an internal implementation value that has no connections to anything that is visible on the page that might need to re-render, so for that reason a global variable is used.
The flash()
function is defined with three arguments message
, type
and duration
. The function starts by checking if there is an active flash timer. If there is a timer, that means that there is currently an alert on display that is going to be replaced. In that case the timer needs to be canceled, since a new timer will be created for the new alert.
The function then updates the flashMessage
state with the provided message
and type
arguments, sets the visible
state to true
, and finally creates a timer that calls the hideFlash()
function after the number of seconds given in duration
have passed. The duration
argument defaults to 10 seconds, and the caller can pass 0 to skip the timer creation and display an alert that remains visible until the user closes it manually.
The hideFlash()
function just sets the visible
state variable to false
, to indicate that the alert should now be hidden.
The JSX returned by this component just creates the context provider with all the children components inside.
As in previous contexts, a useFlash()
custom hook function is defined to return the flash
function. This is a convenience function that all components can use to easily flash a message, without having to deal with the context directly.
The flash context needs to be placed high enough in the component tree that all the components that may need to flash messages, plus the alert component that renders these messages are all children. As before, it is a good idea to do this in the App
component, where all the application wide behaviors are defined.
src/App.js: The flash context
... // <-- no changes to existing imports
import FlashProvider from './contexts/FlashProvider';
export default function App() {
return (
<Container fluid className="App">
<BrowserRouter>
<FlashProvider>
<ApiProvider>
<Header />
<Routes>
... // <-- no changes to routes
</Routes>
</ApiProvider>
</FlashProvider>
</BrowserRouter>
</Container>
);
}
The FlashProvider
component is inserted as a parent of ApiProvider
, which was the top-most application-specific component. This would enable ApiProvider
and all of its children to work with flashed messages.
The FlashMessage Component
The second part of this solution is the component that displays the flashed messages. This is the task of the FlashMessage
component. Below is the implementation of this component.
src/components/FlashMessage.js: Display a flashed message
import { useContext } from 'react';
import Alert from 'react-bootstrap/Alert';
import Collapse from 'react-bootstrap/Collapse';
import { FlashContext } from '../contexts/FlashProvider';
export default function FlashMessage() {
const { flashMessage, visible, hideFlash } = useContext(FlashContext);
return (
<Collapse in={visible}>
<div>
<Alert variant={flashMessage.type || 'info'} dismissible
onClose={hideFlash}>
{flashMessage.message}
</Alert>
</div>
</Collapse>
);
}
To be able to render alerts, this component needs access to the data shared by the FlashProvider
component. The useFlash()
hook function only provides access to the flash()
function, which components can use to display an alert. Because this component needs access to the remaining elements of FlashContext
, it accesses the context with the useContext
hook. There is no great benefit in adding custom hook functions for these attributes of the flash context, since their use is limited to this component.
This component takes advantage not only of the Alert component of React-Bootstrap, but also Collapse, which adds a nice sliding animation when the alert is shown or hidden. The in
prop of Collapse
determines if the components needs to be shown or hidden, so it is directly assigned the value of the visible
state variable that was obtained from the flash context.
The Collapse
documentation indicates that to have a smooth animation it is often necessary to wrap the collapsible elements in a <div>
, so this is done here to wrap Alert
. The alert itself uses the attributes of flashMessage
to configure the styling and the message.
To make these alerts more friendly, the dismissible
prop is added, so that there is a close button on the alert that the user can click to immediately dismiss it, without having to wait for the timer to do it. The onClose
prop is the handler for the close action, which is directly sent to the same hideFlash
function that the timer uses.
The FlashMessage
can be added to the Body
component, so that it is available in all the pages of the application, above the content area:
src/components/Body.js: Show a flashed message in the page
... // <-- no changes to existing imports
import FlashMessage from './FlashMessage';
export default function Body({ sidebar, children }) {
return (
<Container>
<Stack direction="horizontal" className="Body">
{sidebar && <Sidebar />}
<Container className="Content">
<FlashMessage />
{children}
</Container>
</Stack>
</Container>
);
}
The FlashMessage
component is inserted as the first child of the Content
container, so that it appears on top of any page content. When the sidebar is enabled, the alert only covers the extent of the content portion.
The RegistrationPage
component can now retrieve the flash()
function from the useFlash()
hook, and display a success message after registration.
src/pages/RegistrationPage.js: Flash a message after registration
... // <-- no changes to existing imports
import { useFlash } from '../contexts/FlashProvider';
export default function RegistrationPage() {
... // <-- no changes to state variables, references and other hooks
const flash = useFlash();
... // <-- no changes to side effect function
const onSubmit = async (event) => {
event.preventDefault();
if (passwordField.current.value !== password2Field.current.value) {
... // <-- no changes to client-side validation
}
else {
... <-- no changes
if (!data.ok) {
... // <-- no changes
}
else {
setFormErrors({});
flash('You have successfully registered!', 'success');
navigate('/login');
}
}
};
... // <-- no changes to returned JSX
}
Figure 7.5 shows how the flashed message looks after a user is registered.

Chapter Summary
- Use a React reference to access DOM elements in side effect or event handler functions.
- Use DOM APIs to obtain or set the value of an uncontrolled input component through a reference.
- Use client-side validation for forms as a complement, but not as a replacement for server-side validation.
- A React context is not only useful when a parent needs to share data with its children. It can also be used to enable children components to pass information between themselves with the parent as intermediary.
-
#1 Tomas said
Hi Miguel,
I just noticed that
setFormErrors(data.body.errors.json)
for client-side validation on the registration form doesn't act as expected. I had to change theerrors
key tomessages
. SosetFormErrors(data.body.messages.json)
. Have you come across this before?Many thanks,
Tomas -
#2 Miguel Grinberg said
@Tomas: Are you running an unmodified Microblog-API? This project returns the list of validation errors in the
errors
field, there is nomessages
field in the response.