5 Writing our React components

Now we’re ready to start creating our React components to view and interact with our API. We’ll just handle listing Vehicles and creating new instances for now.

5.1 Writing the Vehicles component

Let’s start with a Vehicles component, which will render a table of our Vehicle instances.

Create a new JavaScript file under src/main/webapp/app/ called Vehicles.js.

Here’s our Vehicles component:

import React from 'react';
import {Table} from 'react-bootstrap';
import {array} from 'prop-types';

class Vehicles extends React.Component {

  render() {
    function renderVehicleRow(vehicle) {

      return (<tr key={vehicle.id}>
        <td>{vehicle.id}</td>
        <td>{vehicle.name}</td>
        <td>{vehicle.make.name}</td>
        <td>{vehicle.model.name}</td>
        <td>{vehicle.driver.name}</td>
      </tr>);
    }


    return <div>
      <Table striped bordered condensed hover>
        <thead>
        <tr>
          <th>ID</th>
          <th>Name</th>
          <th>Make</th>
          <th>Model</th>
          <th>Driver</th>
        </tr>
        </thead>
        <tbody>

        {this.props.vehicles.map(renderVehicleRow)}

        </tbody>
      </Table>
    </div>;
  }
}

Vehicles.propTypes = {
  vehicles: array
};

export default Vehicles;

Note that in the renderVehicleRow function we are accessing the customized JSON representation of our Vehicle instances, so we can use vehicle.make.name, for example.

The React profile includes React-Bootstrap by default, so we’re using the Bootstrap Table component to streamline our code.

5.2 Writing the Garage app

Now we need to give our Vehicles component access to data from our API. We could have done this from within the Vehicles component, but a more flexible option is to create a "container" component which will obtain the data from the API and pass it down to the "presentational" component (Vehicles, in this case) via props.

With this separated approach, we can expand our app in the future to make additional API calls (and even render different components, perhaps a Drivers table for example), without having to clutter up our Vehicles component. Let’s call our "container" component Garage.

Create a new JavaScript file under src/main/webapp/app/ called Garage.js.

Here’s our Garage component:

import React from 'react';
import ReactDOM from 'react-dom';
import Vehicles from './Vehicles';

class Garage extends React.Component {

  constructor() {
    super();

    this.state = {
      vehicles: [{"id":1,"name":"Pickup","make":{"name":"Nissan","id":1},"model":{"name":"Titan","id":1},"driver":{"name":"Susan","id":1}}],
    }
  }

  render() {
    const {vehicles} = this.state;

    return <div>
      <Vehicles vehicles={vehicles} />
    </div>;
  }
}

ReactDOM.render(<Garage />, document.getElementById('garage'));

The Garage component uses a state object, which is available to all React components but is optional. We did not need state in our Vehicles component because it receives all it’s data via the vehicles prop. A good practice when writing React is to centralize your state in a few components (even a single one) and pass down peices of relevant data to the child components.

Notice that we are hard-coding a single JSON object in the vehicles collection of our state - that’s because we haven’t set up our API calls yet - we’ll get to that part in a couple sections.

The Garage component includes a call to ReactDOM.render in order to render the components onto the page. Now we will create a new page from which to load our React components.

5.3 Render the app

The React 1.0.2 profile relies on Webpack to bundle React components into browser-ready JavaScript bundles, which can then be loaded via the Grails Asset Pipeline. The default application includes a single "index" bundle which is rendered on the index page. Let’s set up a new bundle for our Garage app.

In webpack.config.js, edit the entry section and add a line to load our Garage.js file, as seen below:

webpack.config.js
var path = require('path');

module.exports = {
  entry: {
    index: './src/main/webapp/index.js', (1)
    garage: './src/main/webapp/app/Garage.js' (2)
  },
//...
1 Add the path to Garage.js as the garage entry point
2 Don’t forget the comma!

This will cause Webpack to bundle two different React apps, "index" and "garage". We also need to configure Webpack to output separate bundles for each React app, so we can load them on different pages of our Grails app.

In webpack.config.js, edit the output section and change the filename line as shown below:

webpack.config.js
//...
output: {
  path: path.join(__dirname, 'grails-app/assets/javascripts'),
  publicPath: '/assets/',
  filename: 'bundle-[name].js'
},
//...
1 Add -[name] to the filename property

Now when we start up our Grails app (or run ./gradlew webpack), Webpack will generate two bundles, one called bundle-index.js and one called bundle-Garage.js. We can load these bundles on our page using the Grails Asset Pipeline tags.

Since we changed the filename of the bundle, we’ll need to quickly update our original index.gsp page to use the new name.

Edit grails-app/views/index.gsp, line 66:

        <div id="app"></div>
        <asset:javascript src="bundle-index.js" /> (1)
1 Change "bundle.js" to "bundle-index.js"

Now we are finally ready to create a home page for our Garage app. Create a new Grails controller using a local Grails installation or ./grailsw

./grailsw create-controller demo.GarageController

Make sure that GarageController contains a single index action.

grails-app/controllers/demo/GarageController.groovy
package demo

class GarageController {
    def index() { }
}

Now, create a simple index.gsp page under grails-app/views/garage:

grails-app/views/garage/index.gsp
<!doctype html>
<html>
<head>
    <meta name="layout" content="main"/>
    <title>Garage</title>
    <asset:link rel="icon" href="favicon.ico" type="image/x-ico" />
</head>
<body>

<div id="content" role="main">
    <section class="row colset-2-its">
        <div id="garage"></div>
        <asset:javascript src="bundle-garage.js" />
    </section>
</div>

</body>
</html>

Restart the app, and browse to http://localhost:8080/garage. You should see our new React app loaded on the page, with a single hard-coded row of data.

5.4 Fetching data from the API

Now that our Garage component is set up and rendering the Vehicles table on our page, we can finally hook up our API to load data into our React views. We’ll use the fetch API for this purpose.

Edit src/main/webapp/app/Garage.js:

src/main/webapp/app/Garage.js
//...
import 'whatwg-fetch';    (1)

class Garage extends React.Component {

  constructor() {
    super();

    this.state = {
      vehicles: [] (2)
    }
  }

  componentDidMount() { (3)
    fetch('/vehicle')
      .then(r => r.json())
      .then(json => this.setState({vehicles: json}))
      .catch(error => console.error('Error retrieving vehicles: ' + error));
  }
//...
1 Import the fetch library
2 Remove the hard-coded data.
3 Load data from the API

componentDidMount is one of React’s component lifecycle methods. It is fired as soon as the component is loaded on a page. In this method, we use fetch to make a request (a GET request by default) to our /vehicle endpoint, parse the JSON payload, and call this.setState to update our vehicles collection with the data.

Restart the app (or wait for webpack to reload) to see the changes. You should now see the list of Vehicles from the Grails app displayed in the React Vehicles table.

5.5 Posting data to the API

Our last step in this guide is to create a simple form for posting new Vehicle instances to our API.

Create a new JavaScript file under src/main/webapp/app called AddVehicleForm.js, with the following content:

src/main/webapp/app/AddVehicleForm.js
import React from 'react';
import {array, func} from 'prop-types';

class AddVehicleForm extends React.Component {

  constructor(props) {
    super(props);
    this.state = { (1)
      name: '',
      make: {id: ''},
      model: {id: ''},
      driver: {id: ''}};
  }

  handleSubmit = (event) => { (2)
    event.preventDefault();

    const {name, make, model, driver} = this.state;

    if (!name || !make.id || !model.id || !driver.id) {
      console.warn("missing required field!");
      return;
    }
    this.props.onSubmit( {name, make, model, driver} ); (3)
    this.setState({ name: '', make: {id: ''}, model: {id: ''}, driver: {id: ''}});
  };

  handleNameChange = (event) => { (4)
    this.setState({ name: event.target.value });
  };

  handleMakeChange = (event) => { (4)
    this.setState({ make: {id: event.target.value} });
  };

  handleModelChange = (event) => { (4)
    this.setState({ model: {id: event.target.value} });
  };

  handleDriverChange = (event) => { (4)
    this.setState({ driver: {id: event.target.value} });
  };


  render() {

    function renderSelectList(item) { (5)
      return <option key={item.id} value={item.id}>{item.name}</option>
    }

    return(
      <div>
        <h3>Add a Vehicle:</h3>
        <form className="form form-inline" onSubmit={this.handleSubmit}  >
          <label>Name</label>
          <input className="form-control" name="name" type="text" value={ this.state.name } onChange={ this.handleNameChange } />

          <label>Make</label>
          <select className="form-control" name="make" value={this.state.make.id}
            onChange={this.handleMakeChange}>  {/*<6>*/}
            <option value={null}>Select a Make...</option>
            {this.props.makes.map(renderSelectList)}  {/*<5>*/}
          </select>

          <label>Model</label>
          <select className="form-control" name="model" value={this.state.model.id}
            onChange={this.handleModelChange}>  {/*<6>*/}
            <option value={null}>Select a Model...</option>
            {this.props.models.map(renderSelectList)}  {/*<5>*/}
          </select>

          <label>Driver</label>
          <select className="form-control" name="driver" value={this.state.driver.id}
            onChange={this.handleDriverChange}>  {/*<6>*/}
            <option value={null}>Select a Driver...</option>
            {this.props.drivers.map(renderSelectList)}  {/*<5>*/}
          </select>

          <input className="btn btn-success"  type="submit" value="Add to library" />
        </form>
      </div>
    );

  }
}

AddVehicleForm.propTypes = {
  makes: array,
  models: array,
  drivers: array,
  onSubmit: func
};

export default AddVehicleForm;
1 Initialize state object with all the properties needed to populate a new Vehicle
2 Create event handler to handle form submission
3 Pass the properties from state to the onSubmit callback function
4 Event handlers to update state whenever user input is received
5 Render options in select lists from the arrays in our props
6 Call event handlers whenever user changes input value

This is a fairly complex component, so don’t worry if you don’t understand it all immediately. The key points are that the AddVehicleForm component allows the user to set 4 properties needed to create a new Vehicle instance: name, make, model and driver. It takes a function prop called onSubmit, which is used when the form is submitted.

This pattern of passing functions (handlers) as props is good practice in React. It allows components to be reused easily because specific functionality can be swapped by different callers (e.g., by passing a different function as the onSubmit prop). Similarly as with state, centralizing your functional logic in a few components, and passing down those functions as props to child components, is a good pattern when programming with React. For a more semantic version of this pattern, you might consider a Flux implementation such as Redux to externalize both your state and your logic.

Because make, model, and driver are associations, we need to allow the user to select an ID so that Grails can perform the assignment during databinding. AddVehicleForm takes 3 props which it expects to contain arrays of these associations. We’ll need to provide them in order to use AddUserForm, so let’s edit the Garage component to retrieve those lists.

Edit src/main/webapp/app/Garage.js:

src/main/webapp/app/Garage.js
//..
import AddVehicleForm from './AddVehicleForm'; (1)

class Garage extends React.Component {

  constructor() {
    super();

    this.state = {
      vehicles: [],
      makes: [],            (2)
      models: [],
      drivers: []
    }
  }

  componentDidMount() {
    fetch('/vehicle')
      .then(r => r.json())
      .then(json => this.setState({vehicles: json}))
      .catch(error => console.error('Error retrieving vehicles: ' + error));

    fetch('/make')                  (3)
      .then(r => r.json())
      .then(json => this.setState({makes: json}))
      .catch(error => console.error('Error retrieving makes: ' + error));

    fetch('/model')                 (3)
      .then(r => r.json())
      .then(json => this.setState({models: json}))
      .catch(error => console.error('Error retrieving models ' + error));

    fetch('/driver')                (3)
      .then(r => r.json())
      .then(json => this.setState({drivers: json}))
      .catch(error => console.error('Error retrieving drivers: ' + error));

  }

  render() {
    const {vehicles, makes, models, drivers} = this.state;  (4)

    return <div>
      <AddVehicleForm makes={makes} models={models} drivers={drivers}/> (5)
      <Vehicles vehicles={vehicles} />
    </div>;
  }
}
//...
1 Import AddVehicleForm component
2 Add makes, models, and drivers to state
3 Retrieve data from API
4 Retrieve vehicles, makes, models, drivers from this.state using ES6 destructuring syntax
5 Pass makes, models, and drivers to AddVehicleForm

The final step is to implement the function that we will pass in to AddVehicleForm via the onSubmit prop. This function needs to do two things:

  1. Post the new vehicle details to the API, and retrieve the result from the API

  2. Update the state so that we can display the newly created vehicle in the Vehicles table

Let’s implement this function. Edit src/main/webapp/app/Garage.js one more time:

src/main/webapp/app/Garage.js
//..

class Garage extends React.Component {

  //...

  submitNewVehicle = (vehicle) => {   (1)
    fetch('/vehicle', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(vehicle)
    }).then(r => r.json())
      .then(json => {
        let vehicles = this.state.vehicles;
        vehicles.push({id: json.id, name: json.name, make: json.make, model: json.model, driver: json.driver});
        this.setState({vehicles});
      })
      .catch(ex => console.error('Unable to save vehicle', ex));
  };


  render() {
    const {vehicles, makes, models, drivers} = this.state;

    return <div>
      <AddVehicleForm onSubmit={this.submitNewVehicle} (2)
        makes={makes} models={models} drivers={drivers}/>
      <Vehicles vehicles={vehicles} />
    </div>;
  }
}


ReactDOM.render(<Garage />, document.getElementById('garage'));
//...
1 Create submitNewVehicle function
2 Pass function as onSubmit prop to AddVehicleForm

Again, we’re using the fetch API, this time for a POST request to the /vehicle endpoint. We call JSON.stringify to convert the parameters received from AddVehicleForm into a JSON string, which we can then post to our Grails API. The API will return the newly created vehicle instance, which we can then parse and insert into our state object with this.setState.

Restart the app, or re-run webpack, and you should be able to create new Vehicle instances and see them added to the table. Refresh the page to confirm the new instance was persisted to the database.

  Get the Code