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:
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/grails-guides/building-a-react-app.git
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 theinitial
folder.
To complete the guide, go to the initial
folder
-
cd
intograils-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.
package demo
import grails.rest.Resource
@Resource(uri = '/vehicle')
class Vehicle {
String name
Make make
Model model
static belongsTo = [driver: Driver]
static constraints = {
}
}
package demo
import grails.rest.Resource
@Resource(uri = '/driver')
class Driver {
String name
static hasMany = [ vehicles: Vehicle ]
static constraints = {
vehicles nullable: true
}
}
package demo
import grails.rest.Resource
@Resource(uri = '/make')
class Make {
String name
static constraints = {
}
}
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:
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:
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:
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:
//...
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 Edit
|
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.
package demo
class GarageController {
def index() { }
}
Now, create a simple index.gsp
page under grails-app/views/garage
:
<!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
:
//...
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:
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
:
//..
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:
-
Post the new vehicle details to the API, and retrieve the result from the API
-
Update the
state
so that we can display the newly created vehicle in theVehicles
table
Let’s implement this function. Edit src/main/webapp/app/Garage.js
one more time:
//..
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.