2022-07-15T10:48:29Z

The React Mega-Tutorial, Chapter 7: Forms and Validation

A 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:

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.

Figure 7.1: Example React-Bootstrap form

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 label
  • Form.Control defines the actual input field
  • Form.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 to text when not given by the parent
  • The error message uses a text-danger style from Bootstrap, which renders the text in red
  • The placeholder and error 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.

Figure 7.2: Login page

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.

Figure 7.3: Form validation errors

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&apos;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.

Figure 7.4: Sever-side validation errors in the registration form

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 to false.
  • flashMessage is an object with message and type properties, defining the alert to display. The type 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.

Figure 7.5: Flashed message

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.

2 comments

  • #1 Tomas said 2022-08-11T11:42:55Z

    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 the errors key to messages. So setFormErrors(data.body.messages.json). Have you come across this before?

    Many thanks, Tomas

  • #2 Miguel Grinberg said 2022-08-11T13:27:20Z

    @Tomas: Are you running an unmodified Microblog-API? This project returns the list of validation errors in the errors field, there is no messages field in the response.

Leave a Comment