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="form-group">
        <input
          name="username"
          className="form-control input-lg"
          type="text"
          placeholder="Enter a username"
          required
        />
      </div>
      <div className="form-group">
        <input
          name="email"
          className="form-control input-lg"
          type="email"
          placeholder="Enter an email address"
          required
        />
      </div>
      <input
        type="submit"
        className="btn btn-primary btn-lg btn-block"
        value="Submit"
      />
    </form>
  )
};

export default AddUser;

Import the component in index.js:

import AddUser from '../components/AddUser';

Then update the render method:

render() {
  return (
    <div className="container">
      <div className="row">
        <div className="col-md-6">
          <br/>
          <h1>All Users</h1>
          <hr/><br/>
          <AddUser/>
          <br/>
          <UsersList users={this.state.users}/>
        </div>
      </div>
    </div>
  )
};

Ensure the testdriven-dev machine is up and running and the REACT_APP_USERS_SERVICE_URL environment variable is properly assigned to the IP associated with the testdriven-dev machine. 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();
});

Ensure it passes before moving on.

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 - we prevented the normal browser behavior.

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);
};

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 will not 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 on this, 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="form-group">
  <input
    name="username"
    className="form-control input-lg"
    type="text"
    placeholder="Enter a username"
    required
    value={props.username}
  />
</div>
<div className="form-group">
  <input
    name="email"
    className="form-control input-lg"
    type="email"
    placeholder="Enter an email address"
    required
    value={props.email}
  />
</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}
  handleChange={this.handleChange}
  addUser={this.addUser}
/>

Add it to the form inputs:

<div className="form-group">
  <input
    name="username"
    className="form-control input-lg"
    type="text"
    placeholder="Enter a username"
    required
    value={props.username}
    onChange={props.handleChange}
  />
</div>
<div className="form-group">
  <input
    name="email"
    className="form-control input-lg"
    type="email"
    placeholder="Enter an email address"
    required
    value={props.email}
    onChange={props.handleChange}
  />
</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 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();
  const data = {
    username: this.state.username,
    email: this.state.email
  };
  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();
    this.setState({ username: '', email: '' });
  })
  .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.159s, estimated 1s
Ran all test suites related to changed files.

Review and then commit your code.