Show Navigation

Building a React App

Use the React 1.x profile to create a Grails app with React views

Authors: Zachary Klein

Grails Version: 3.3.5

1 Grails Training

Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.

2 Getting Started

In this guide you are going to create Grails app using React as a view layer. You will be using the 1.0.2 version of the React profile (single project, using Webpack and the Asset Pipeline).

A few changes were made to the initial project (compared to a default Grails project using the 1.0.2 version of the React profile). A patch file containing the changes is provided in the guide repo.

2.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

2.2 How to complete the guide

To get started do the following:

or

The Grails guides repositories contain two folders:

  • initial Initial project. Often a simple Grails app with some additional code to give you a head-start.

  • complete A completed example. It is the result of working through the steps presented by the guide and applying those changes to the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-guides/building-a-react-app/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/building-a-react-app/complete

3 Writing the Application

The React profile includes some default React sample code. Feel free to run the app as is if you want to see the sample in action.

Let’s start by creating our domain model for the application.

$ grails create-domain-class demo.Vehicle
$ grails create-domain-class demo.Driver
$ grails create-domain-class demo.Make
$ grails create-domain-class demo.Model

Now let’s edit our domain class under grails-app/domain/demo/. We’ll add some properties and the @Resource annotation.

grails-app/grails-app/domain/demo/Vehicle.groovy
package demo

import grails.rest.Resource

@Resource(uri = '/vehicle')
class Vehicle {

    String name

    Make make
    Model model

    static belongsTo = [driver: Driver]

    static constraints = {
    }
}
grails-app/grails-app/domain/demo/Driver.groovy
package demo

import grails.rest.Resource

@Resource(uri = '/driver')
class Driver {

    String name

    static hasMany = [ vehicles: Vehicle ]

    static constraints = {
        vehicles nullable: true
    }
}
grails-app/grails-app/domain/demo/Make.groovy
package demo

import grails.rest.Resource

@Resource(uri = '/make')
class Make {

    String name

    static constraints = {
    }
}
grails-app/grails-app/domain/demo/Model.groovy
package demo

import grails.rest.Resource

@Resource(uri = '/model')
class Model {

    String name

    static constraints = {
    }
}

Since we’ve added the @Resource annotation to our domain classes, Grails will generate RESTful URL mappings for each of them. Let’s preload some data:

grails-app/init/demo/BootStrap.groovy
package demo

import demo.Driver
import demo.Make
import demo.Model
import demo.Vehicle

class BootStrap {

    def init = { servletContext ->
        log.info "Loading database..."
        def driver1 = new Driver(name: "Susan").save()
        def driver2 = new Driver(name: "Pedro").save()

        def nissan = new Make(name: "Nissan").save()
        def ford = new Make(name: "Ford").save()

        def titan = new Model(name: "Titan").save()
        def leaf = new Model(name: "Leaf").save()
        def windstar = new Model(name: "Windstar").save()

        new Vehicle(name: "Pickup", driver: driver1, make: nissan, model: titan).save()
        new Vehicle(name: "Economy", driver: driver1, make: nissan, model: leaf).save()
        new Vehicle(name: "Minivan", driver: driver2, make: ford, model: windstar).save()

    }
    def destroy = {
    }
}

4 Running the Application

Now, if we run our app, we can try out the RESTful API that Grails has generated for us. Start up the app using a local installation of Grails 3.2.4 or one of the provided wrappers: ./gradlew bootRun or ./grailsw run-app

$ ./grailsw run-app

Now we can exercise the API using cURL or another API tool.

Make a GET request to /vehicle to get a list of Vehicles:

$ curl -X "GET" "http://localhost:8080/vehicle"

HTTP/1.1 200
X-Application-Context: application:development
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 06 Jan 2017 19:28:49 GMT
Connection: close

[{"id":1,"driver":{"id":1},"make":{"id":1},"model":{"id":1},"name":"Pickup"},
{"id":2,"driver":{"id":1},"make":{"id":1},"model":{"id":2},"name":"Economy"},
{"id":3,"driver":{"id":2},"make":{"id":2},"model":{"id":3},"name":"Minivan"}]

Make a GET request to /driver/1 to get a particular Driver instance:

$ curl -X "GET" "http://localhost:8080/driver/1"

HTTP/1.1 200
X-Application-Context: application:development
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 06 Jan 2017 22:10:33 GMT
Connection: close

{"id":1,"name":"Susan","vehicle":[{"id":2},{"id":1}]}

Make a POST request to /driver to create a new Driver instance:

$ curl -X "POST" "http://localhost:8080/driver" \
      -H "Content-Type: application/json; charset=utf-8" \
      -d '{"name":"Edward"}'

HTTP/1.1 201
X-Application-Context: application:development
Location: http://localhost:8080/driver/3
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 06 Jan 2017 21:55:59 GMT
Connection: close

{"id":3,"name":"Edward"}

Make a PUT request to /vehicle to update a Vehicle instance:

$ curl -X "PUT" "http://localhost:8080/vehicle/1" \
       -H "Content-Type: application/json; charset=utf-8" \
       -d '{"name":"Truck","id":1}'

HTTP/1.1 200
X-Application-Context: application:development
Location: http://localhost:8080/vehicle/1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 06 Jan 2017 22:12:31 GMT
Connection: close

{"id":1,"driver":{"id":1},"make":{"id":1},"model":{"id":1},"name":"Truck"}

4.1 Customizing the API

By default, the RESTful URLs generated by Grails provide only the IDs of associated objects.

HTTP/1.1 200
X-Application-Context: application:development
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 06 Jan 2017 23:55:33 GMT
Connection: close

{"id":1,"name":"Pickup","make":{"id":1},"driver":{"id":1}}

This is sufficient for many usages, but we’ll need to get a bit more data for our React component in a moment. This is an excellent place for JSON Views. Let’s create a new JSON view to render our Vehicle list:

$ mkdir grails-app/views/vehicle/

By convention, any JSON views in the corresponding view directory for a restful controller (like those generated by @Resource) will be used in lieu of the default JSON representation. Now we can customize our JSON output for each Vehicle by creating a new JSON template for Vehicle:

$ vim grails-app/views/vehicle/_vehicle.gson

Edit the file to include the following:

grails-app/views/vehicle/_vehicle.gson
import demo.Vehicle

model {
    Vehicle vehicle
}
json {
    id vehicle.id

    name vehicle.name

    make name: vehicle.make.name,
        id: vehicle.make.id

    model name: vehicle.model.name,
            id: vehicle.model.id

    driver name: vehicle.driver.name,
        id: vehicle.driver.id
}

Now when we access our API, we’ll see the name and id of each make, model, and driver are included.

HTTP/1.1 200
X-Application-Context: application:development
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 07 Jan 2017 00:24:18 GMT
Connection: close

{"id":1,"name":"Pickup","make":{"name":"Nissan","id":1},"model":{"name":"Titan","id":1},"driver":{"name":"Susan","id":1}}

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.

6 Next Steps

There’s plenty of opportunities to expand the scope of this application. Here are a few ideas for improvements you can make on your own:

  • Create a modal dialog form to add new Drivers, Makes & Models. Use React-Bootstrap’s Modal component to give you a headstart.

  • Add support for updates to existing Vehicles. A modal dialog might work well for this as well, or perhaps an editable table row

  • Currently Makes & Models are independent. Add an appropriate GORM association between Make & Model, and change the select lists to only display Models for the currently select Make. You will want to make use of the JavaScript Array.filter method.

7 Do you need help with Grails?

Object Computing, Inc. (OCI) sponsored the creation of this Guide. A variety of consulting and support services are available.

OCI is Home to Grails

Meet the Team