React Forms

Part 2, Lesson 7



In this lesson, we’ll create a functional component for adding a new user….


Add two new files:

  1. services/client/src/components/AddUser.jsx
  2. services/client/src/components/__tests__/AddUser.test.jsx

Start with the test:

import React from 'react';
import { shallow } from 'enzyme';
import renderer from 'react-test-renderer';

import AddUser from '../AddUser';

test('AddUser renders properly', () => {
  const wrapper = shallow(<AddUser/>);
  const element = wrapper.find('form');
  expect(element.find('input').length).toBe(3);
  expect(element.find('input').get(0).props.name).toBe('username');
  expect(element.find('input').get(1).props.name).toBe('email');
  expect(element.find('input').get(2).props.type).toBe('submit');
});

Here, we’re asserting that a form with three inputs is present. Run the tests to ensure they fail, and then add the component:

import React from 'react';

const AddUser = (props) => {
  return (
    <form>
      <div className="field">
        <input
          name="username"
          className="input is-large"
          type="text"
          placeholder="Enter a username"
          required
        />
      </div>
      <div className="field">
        <input
          name="email"
          className="input is-large"
          type="email"
          placeholder="Enter an email address"
          required
        />
      </div>
      <input
        type="submit"
        className="button is-primary is-large is-fullwidth"
        value="Submit"
      />
    </form>
  )
};

export default AddUser;

Import the component in index.js:

import AddUser from './components/AddUser';

Then update the render method:

render() {
  return (
    <section className="section">
      <div className="container">
        <div className="columns">
          <div className="column is-half">  {/* new */}
            <br/>
            <h1 className="title is-1">All Users</h1>
            <hr/><br/>
            <AddUser/>  {/* new */}
            <br/><br/>  {/* new */}
            <UsersList users={this.state.users}/>
          </div>
        </div>
      </div>
    </section>
  )
}

Make sure the users service is up and running and the REACT_APP_USERS_SERVICE_URL environment variable is set:

$ export REACT_APP_USERS_SERVICE_URL=http://localhost
$ docker-compose -f docker-compose-dev.yml up -d --build

Run npm start to test. If all went well, you should see the form along with the users.

react forms

Make sure the tests past as well:

PASS  src/components/__tests__/UsersList.test.jsx
PASS  src/components/__tests__/AddUser.test.jsx

Test Suites: 2 passed, 2 total
Tests:       3 passed, 3 total
Snapshots:   1 passed, 1 total
Time:        0.593s, estimated 1s
Ran all test suites related to changed files.

With that, let’s add a snapshot test to AddUser.test.jsx:

test('AddUser renders a snapshot properly', () => {
  const tree = renderer.create(<AddUser/>).toJSON();
  expect(tree).toMatchSnapshot();
});

Now, since this is a single page application, we want to prevent the normal browser behavior when a form is submitted to avoid a page refresh.

Steps:

  1. Handle form submit event
  2. Obtain user input
  3. Send AJAX request
  4. Update the page

Handle form submit event

To handle the submit event, simply update the form element in AddUser.jsx:

<form onSubmit={(event) => event.preventDefault()}>

Enter a dummy username and email address, and then try submitting the form. Nothing should happen, which is exactly what we want.

Next, add the following method to the App component:

addUser(event) {
  event.preventDefault();
  console.log('sanity check!');
};

Since AddUser is a functional component, we need to pass this method down to it via props. Update the AddUser element in the render method like so:

<AddUser addUser={this.addUser}/>

Update the form element again:

<form onSubmit={(event) => props.addUser(event)}>

Then, update the constructor as well:

constructor() {
  super();
  this.state = {
    users: []
  };
  this.addUser = this.addUser.bind(this);  // new
};

Here, we bound the context of this manually via bind():

this.addUser = this.addUser.bind(this);

Without it, the context of this inside the method won’t be correct. Want to test this out? Simply add console.log(this) to addUser() and then submit the form. What’s the context? Remove the bind and test it again. What’s the context now?

For more, review Handling Events from the official React docs.

Test it out in the browser. You should see sanity check! in the JavaScript console on form submit.

react forms

Obtain user input

We’ll use controlled components to obtain the user submitted input. Start by adding two new properties to the state object in the App component:

  this.state = {
    users: [],
    username: '',
    email: '',
  };

Then, pass them through to the component:

<AddUser
  username={this.state.username}
  email={this.state.email}
  addUser={this.addUser}
/>

These are accessible now via the props object, which can be used as the current value of the input like so:

<div className="field">
  <input
    name="username"
    className="input is-large"
    type="text"
    placeholder="Enter a username"
    required
    value={props.username}  // new
  />
</div>
<div className="field">
  <input
    name="email"
    className="input is-large"
    type="email"
    placeholder="Enter an email address"
    required
    value={props.email}  // new
  />
</div>

So, this defines the value of the inputs from the parent component. Test out the form now. What happens if you try to add a username? You shouldn’t see anything being typed since the value is being “pushed” down from the parent - and that value is ''.

What do you think will happen if the initial state of those values was set as test rather than an empty string? Try it.

How do we update the state in the parent component so that it updates when the user enters text into the input boxes?

First, add a handleChange method to the App component:

handleChange(event) {
  const obj = {};
  obj[event.target.name] = event.target.value;
  this.setState(obj);
};

Add the bind to the constructor:

this.handleChange = this.handleChange.bind(this);

Then, pass the method down to the component:

<AddUser
  username={this.state.username}
  email={this.state.email}
  addUser={this.addUser}
  handleChange={this.handleChange}  // new
/>

Add it to the form inputs:

<div className="field">
  <input
    name="username"
    className="input is-large"
    type="text"
    placeholder="Enter a username"
    required
    value={props.username}
    onChange={props.handleChange}  // new
  />
</div>
<div className="field">
  <input
    name="email"
    className="input is-large"
    type="email"
    placeholder="Enter an email address"
    required
    value={props.email}
    onChange={props.handleChange}  // new
  />
</div>

Test the form out now. It should be working. If curious, you can see the value of the state by logging it to the console in the addUser method:

addUser(event) {
  event.preventDefault();
  console.log('sanity check!');
  console.log(this.state);
};

react forms

Now that we have the values, let’s fire off the AJAX request to add the data to the database and then update the DOM.

Send AJAX request

Turn your attention back to the users service. What do we need to send in the JSON payload to add a user - username and email, right?

db.session.add(User(username=username, email=email))

Use Axios to send the POST request:

addUser(event) {
  event.preventDefault();
  // new
  const data = {
    username: this.state.username,
    email: this.state.email
  };
  // new
  axios.post(`${process.env.REACT_APP_USERS_SERVICE_URL}/users`, data)
  .then((res) => { console.log(res); })
  .catch((err) => { console.log(err); });
};

Test it out. Be sure to use a unique username and email. Although this does not matter so much now, it will present problems in the future since unique constraints will be added to the database table.

react forms

If you have problems, analyze the response object from the Network tab in Chrome Developer Tools. You can also fire up the users service outside of Docker and debug using the Flask debugger or with print statements.

Update the page

Finally, let’s update the list of users on a successful form submit and then clear the form:

addUser(event) {
  event.preventDefault();
  const data = {
    username: this.state.username,
    email: this.state.email
  };
  axios.post(`${process.env.REACT_APP_USERS_SERVICE_URL}/users`, data)
  .then((res) => {
    this.getUsers();  // new
    this.setState({ username: '', email: '' });  // new
  })
  .catch((err) => { console.log(err); });
};

That’s it. Manually test it out. Then, run the test suite. Update the snapshot test (by pressing u on the keyboard).

 PASS  src/components/__tests__/AddUser.test.jsx
 PASS  src/components/__tests__/UsersList.test.jsx

Test Suites: 2 passed, 2 total
Tests:       4 passed, 4 total
Snapshots:   2 passed, 2 total
Time:        0.16s, estimated 1s
Ran all test suites related to changed files.

Review and then commit your code.