React.js is a lightweight JavaScript framework, which is oriented toward the creation of component-based web UIs. React does not provide any means for communicating with the backend, but we can use any communication library from inside React components.
Here we are developing a simple React application consuming the REST API.
Step 1: React.js Development Environment Set Up
There are number of ways to use React.js. The simplest way is to include React libraries in the <script> tags on the page.
<!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <title></title> </head> <body> <div id="hello_container" class=""></div> <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script> <script> class Hello extends React.Component { constructor(props) { super(props); } render() { return React.createElement( 'div', null, `Hello ${this.props.name}!` ); } } ReactDOM.render(React.createElement(Hello, {name: 'React'}, null), document.querySelector('#hello_container')); </script> </body> </html>
Step 2: Backend Communication Service Implementation
It is a great idea to put all the related functionalities in one place. Putting our functionality behind a service that exposes certain APIs ensures more flexibility and testability for our application. Therefore, we can create a communication service class, which implements all the basic CRUD operations for exchanging data with the server and exposes these operations as a method for all React components. To make your UI more responsive, you need to implement the methods as asynchronous.
src/shared/mock-item-service,js – mock ItemService:
class ItemService { constructor() { this.items = [ {link:1, name:"test1", summary:"Summary Test 1", year:"2001", country:"us", price:"1000", description:"Desc 1"}, {link:2, name:"test2", summary:"Summary Test 2", year:"2002", country:"uk", price:"2000", description:"Desc 2"}, {link:3, name:"test3", summary:"Summary Test 3", year:"2003", country:"cz", price:"3000", description:"Desc 3"}, ]; } async retrieveItems() { return Promise.resolve(this.items); } async getItem(itemLink) { for(var i = 0; i < this.items.length; i++) { if ( this.items[i].link === itemLink) { return Promise.resolve(this.items[i]); } } return null; } async createItem(item) { console.log("ItemService.createItem():"); console.log(item); return Promise.resolve(item); } async deleteItem(itemId) { console.log("ItemService.deleteItem():"); console.log("item ID:" + itemId); } async updateItem(item) { console.log("ItemService.updateItem():"); console.log(item); } } export default ItemService;
Step 3: CRUD UI Implementation
React supports component hierarchies, where each component have a state and that state can be shared between related components. Also, behavior of each component’s can be customized by passing properties to it. Therefore, we can develop the main component that contains the list of collection items and works as a placeholder for displaying the forms for corresponding CRUD actions. Using the stuff that is generated by the create-react-app tool, we can change the content of app.js as follows.
src/App.js – the main component used as the application frame:
import React, { Component } from 'react'; import './App.css'; import ItemDetails from './item-details'; import NewItem from './new-item'; import EditItem from './edit-item'; import ItemService from './shared/mock-item-service'; class App extends Component { constructor(props) { super(props); this.itemService = new ItemService(); this.onSelect = this.onSelect.bind(this); this.onNewItem = this.onNewItem.bind(this); this.onEditItem = this.onEditItem.bind(this); this.onCancel = this.onCancel.bind(this); this.onCancelEdit = this.onCancelEdit.bind(this); this.onCreateItem = this.onCreateItem.bind(this); this.onUpdateItem = this.onUpdateItem.bind(this); this.onDeleteItem = this.onDeleteItem.bind(this); this.state = { showDetails: false, editItem: false, selectedItem: null, newItem: null } } componentDidMount() { this.getItems(); } render() { const items = this.state.items; if(!items) return null; const showDetails = this.state.showDetails; const selectedItem = this.state.selectedItem; const newItem = this.state.newItem; const editItem = this.state.editItem; const listItems = items.map((item) => <li key={item.link} onClick={() => this.onSelect(item.link)}> <span className="item-name">{item.name}</span> | {item.summary} </li> ); return ( <div className="App"> <ul className="items"> {listItems} </ul> <br/> <button type="button" name="button" onClick={() => this.onNewItem()}>New Item</button> <br/> {newItem && <NewItem onSubmit={this.onCreateItem} onCancel={this.onCancel}/>} {showDetails && selectedItem && <ItemDetails item={selectedItem} onEdit={this.onEditItem} onDelete={this.onDeleteItem} />} {editItem && selectedItem && <EditItem onSubmit={this.onUpdateItem} onCancel={this.onCancelEdit} item={selectedItem} />} </div> ); } getItems() { this.itemService.retrieveItems().then(items => { this.setState({items: items}); } ); } onSelect(itemLink) { this.clearState(); this.itemService.getItem(itemLink).then(item => { this.setState({ showDetails: true, selectedItem: item }); } ); } onCancel() { this.clearState(); } onNewItem() { this.clearState(); this.setState({ newItem: true }); } onEditItem() { this.setState({ showDetails: false, editItem: true, newItem: null }); } onCancelEdit() { this.setState({ showDetails: true, editItem: false, newItem: null }); } onUpdateItem(item) { this.clearState(); this.itemService.updateItem(item).then(item => { this.getItems(); } ); } onCreateItem(newItem) { this.clearState(); this.itemService.createItem(newItem).then(item => { this.getItems(); } ); } onDeleteItem(itemLink) { this.clearState(); this.itemService.deleteItem(itemLink).then(res => { this.getItems(); } ); } clearState() { this.setState({ showDetails: false, selectedItem: null, editItem: false, newItem: null }); } } export default App; Then we create nested components for basic operations with collection items. src/new-item.js – created for new collection items: import React, { Component } from 'react'; import './App.css'; import Validator from './shared/validator'; class NewItem extends Component { constructor(props) { super(props); this.validator = new Validator(); this.onCancel = this.onCancel.bind(this); this.onSubmit = this.onSubmit.bind(this); this.handleInputChange = this.handleInputChange.bind(this); this.state = { name: '', summary: '', year: '', country: '', description: '' }; } handleInputChange(event) { const target = event.target; const value = target.value; const name = target.name; this.setState({ [name]: value }); } onCancel() { this.props.onCancel(); } onSubmit() { if(this.validator.validateInputs(this.state)) { this.props.onSubmit(this.state); } } render() { return ( <div className="input-panel"> <span className="form-caption">New item:</span> <div> <label className="field-name">Name:<br/> <input value={this.state.name} name="name" maxLength="40" required onChange={this.handleInputChange} placeholder="item name" /> </label> </div> <div> <label className="field-name">Summary:<br/> <input value={this.state.summary} name="summary" maxLength="40" required onChange={this.handleInputChange} placeholder="summary" /> </label> </div> <div> <label className="field-name">Year:<br/> <input value={this.state.year} name="year" maxLength="4" pattern="[0-9]{1,4}" onChange={this.handleInputChange} placeholder="year" /> </label> </div> <div> <label className="field-name">Country:<br/> <input value={this.state.country} name="country" maxLength="2" pattern="[a-z|A-Z]{2}" onChange={this.handleInputChange} placeholder="country code" /> </label> </div> <div> <label className="field-name">Description:<br/> <textarea value={this.state.description} name="description" onChange={this.handleInputChange} placeholder="description" /> </label> </div> <br/> <button onClick={() => this.onCancel()}>Cancel</button> <button onClick={() => this.onSubmit()}>Create</button> </div> ); } } export default NewItem;
Here, we will use the validator class that provides a simple validation for newly created or edited collection items and this class can be shared between components, i.e. it can be used in NewItem and EditItem components in this case.
src/shared/validatior.js – simple validation method for the item form:
class Validator { validateInputs(inputData) { let errorMsg = ""; if(!inputData.name) { errorMsg +="Please enter name of this item.\n" } if(!inputData.summary) { errorMsg +="Please enter summary of this item.\n" } if(inputData.year.toString().match(/[^0-9]/g)) { errorMsg +="Year must be a number.\n" } if(inputData.country.length > 0 && !inputData.country.match(/^[a-z|A-Z][a-z|A-Z]$/)) { errorMsg +="Country code must be two letters.\n" } if(errorMsg.length == 0){ return true; } else { alert(errorMsg); return false; } } } export default Validator;
src/item-details.js – viewing item details:
import React, { Component } from 'react'; import './App.css'; class ItemDetails extends Component { constructor(props) { super(props); this.onEdit = this.onEdit.bind(this); this.onDelete = this.onDelete.bind(this); } render() { const item = this.props.item; return ( <div className="input-panel"> <span className="form-caption">{ item.name}</span> <div><span className="field-name">Name:</span><br/> {item.name}</div> <div><span className="field-name">Summary:</span><br/> {item.summary}</div> <div><span className="field-name">Year:</span><br/> {item.year}</div> <div><span className="field-name">Country:</span><br/> {item.country}</div> <div><span className="field-name">Description:</span><br/> {item.description}</div> <br/> <button onClick={() => this.onDelete()}>Delete</button> <button onClick={() => this.onEdit()}>Edit</button> </div> ); } onEdit() { this.props.onEdit(); } onDelete() { const item = this.props.item; if(window.confirm("Are you sure you want to delete item: " + item.name + " ?")) { this.props.onDelete(item.link); } } } export default ItemDetails;
src/edit-item.js – editing existing items:
import React, { Component } from 'react'; import './App.css'; import Validator from './shared/validator'; class EditItem extends Component { constructor(props) { super(props); this.validator = new Validator(); this.onCancel = this.onCancel.bind(this); this.onSubmit = this.onSubmit.bind(this); this.handleInputChange = this.handleInputChange.bind(this); const itemToEdit = props.item; this.state = { name: itemToEdit.name, summary: itemToEdit.summary, year: itemToEdit.year, country: itemToEdit.country, description: itemToEdit.description, link: itemToEdit.link }; } handleInputChange(event) { const target = event.target; const value = target.value; const name = target.name; this.setState({ [name]: value }); } onCancel() { this.props.onCancel(); } onSubmit() { if (this.validator.validateInputs(this.state)) { this.props.onSubmit(this.state); } } render() { return ( <div className="input-panel"> <span className="form-caption">Edit item:</span> <span>{this.state.name}</span> <div> <label className="field-name">Name:<br/> <input value={this.state.name} name="name" maxLength="40" required onChange={this.handleInputChange} placeholder="item name" /> </label> </div> <div> <label className="field-name">Summary:<br/> <input value={this.state.summary} name="summary" maxLength="40" required onChange={this.handleInputChange} placeholder="summary" /> </label> </div> <div> <label className="field-name">Year:<br/> <input value={this.state.year} name="year" maxLength="4" pattern="[0-9]{1,4}" onChange={this.handleInputChange} placeholder="year" /> </label> </div> <div> <label className="field-name">Country:<br/> <input value={this.state.country} name="country" maxLength="2" pattern="[a-z|A-Z]{2}" onChange={this.handleInputChange} placeholder="country" /> </label> </div> <div> <label className="field-name">Description:<br/> <textarea value={this.state.description} name="description" onChange={this.handleInputChange} placeholder="description" /> </label> </div> <br/> <button onClick={() => this.onCancel()}>Cancel</button> <button onClick={() => this.onSubmit()}>Update</button> </div> ); } } export default EditItem;
Here we will use the lifting-state-up approach. Instead of maintaining state in each of the child component and synchronizing their states, and hence the appearance of related components, we will lift the shared state up to their closest common ancestor. So, we can maintain state in the parent app component using callback functions that are passed to child components via properties. Then we will call the callback functions inside event handlers in the child components where we change the parent component state accordingly to the user actions triggered in the child components. For instance, see how theApp.onEditItem() method is called in theItemDetails.onEdit() event handler that is triggered when the user clicks the Edit button.
We have provided all the scripts in place, and so we can see the main application at http://localhost:3000:
We can see the item details by clicking on an item in the list:
We can also make the detail view editable with the Edit button, if we need to edit an item:
We can also add new items with the New Item button:
Step 4: Real Communication
While React does not provide any built-in support for sending requests to the server, we can freely use any communication library inside our React applications. Let us use the Fetch API, which is becoming a standard way to send HTTP requests and is supported in almost modern browsers. We have provided our communication interface defined, and so we can easily substitute our mock service implementation with a fully functional version, like the following.
src/shared/item-service,js – functional version of ItemService:
import Configuration from './configuration'; class ItemService { constructor() { this.config = new Configuration(); } async retrieveItems() { return fetch(this.config.ITEM_COLLECTION_URL) .then(response => { if (!response.ok) { this.handleResponseError(response); } return response.json(); }) .then(json => { console.log("Retrieved items:"); console.log(json); const items = []; const itemArray = json._embedded.collectionItems; for(var i = 0; i < itemArray.length; i++) { itemArray[i]["link"] = itemArray[i]._links.self.href; items.push(itemArray[i]); } return items; }) .catch(error => { this.handleError(error); }); } async getItem(itemLink) { console.log("ItemService.getItem():"); console.log("Item: " + itemLink); return fetch(itemLink) .then(response => { if (!response.ok) { this.handleResponseError(response); } return response.json(); }) .then(item => { item["link"] = item._links.self.href; return item; } ) .catch(error => { this.handleError(error); }); } async createItem(newitem) { console.log("ItemService.createItem():"); console.log(newitem); return fetch(this.config.ITEM_COLLECTION_URL, { method: "POST", mode: "cors", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newitem) }) .then(response => { if (!response.ok) { this.handleResponseError(response); } return response.json(); }) .catch(error => { this.handleError(error); }); } async deleteItem(itemlink) { console.log("ItemService.deleteItem():"); console.log("item: " + itemlink); return fetch(itemlink, { method: "DELETE", mode: "cors" }) .then(response => { if (!response.ok) { this.handleResponseError(response); } }) .catch(error => { this.handleError(error); }); } async updateItem(item) { console.log("ItemService.updateItem():"); console.log(item); return fetch(item.link, { method: "PUT", mode: "cors", headers: { "Content-Type": "application/json" }, body: JSON.stringify(item) }) .then(response => { if (!response.ok) { this.handleResponseError(response); } return response.json(); }) .catch(error => { this.handleError(error); }); } handleResponseError(response) { throw new Error("HTTP error, status = " + response.status); } handleError(error) { console.log(error.message); } } export default ItemService;
Here, we will also follow the single-responsibility principle and put all configuration settings into one object, Configuration that can be imported into all relevant components.
Step 5: Running the Front-End Application
We have provided our backend running on http://localhost:8080, we can set its URL in the configuration class.
Configuration class – one-point application configuration:
class Configuration { ITEM_COLLECTION_URL = "http://localhost:8080/collectionItems"; } export default Configuration; And to start up our application: .../project-root/consuming-rest>npm start
This time, we can see the main application screen with real data from the backend:
We can also add new items, as the following screenshot illustrates:
New item added: