This article focuses on implementing React based frontend application to perform CRUD operations on backend API which exposes these operations as RESTful APIs.
There are lot of online learning materials and blog posts providing greater insights on getting started with implementing React applications and those which focuses on integrating with RESTful APIs. Instead of rewriting the same, we shall focus on few concepts that needs some insights on using React Hooks, Axios Integration, Internationalization and Containerizing.
Below are the screenshots of what the application looks like when integrated with the RESTful API service.
Basic knowledge of React, Node, Docker is necessary for test driving this application.
Application referred in this article is built and tested on Ubuntu OS
. If you are on windows and would like to make your hands dirty with Unix, then I would recommend going through
At the least, you should have below softwares and tools installed to try out the application:
Node
& Yarn
Docker
& Docker Compose
$ node -v
v12.16.1
$ yarn -v
1.22.4
$ docker -v
Docker version 19.03.6, build 369ce74a3c
There is a previous article published on
Microservice is built using
Below are the Restful APIs
that are exposed by Catalogue Management System microservice application.
HTTP Method | API Name | Path | Response Status Code |
---|---|---|---|
POST | Create Catalogue Item | / | 201 (Created) |
GET | Get Catalogue Items | / | 200 (Ok) |
GET | Get Catalogue Item | /{sku} | 200 (Ok) |
PUT | Update Catalogue Item | /{sku} | 200 (Ok) |
DELETE | Delete Catalogue Item | /{sku} | 204 (No Content) |
Follow the series of steps mentioned in Prerequisites section to boot up the database and supporting containers. To ensure microservice is up and running, access http://localhost:8080/health
in browser and should show status
as UP
.
Project is bootstrapped with
This should clone the application with below project structure and dependencies added to package.json
As observed, below are the dependencies added to the project which are the building blocks of this application.
react-select
- A flexible and beautiful Select Input control.react-i18next
- A powerful internationalization framework for React / React Native which is based on i18next
.tailwindcss
- A Utility-first CSS Framework.Axios
- Promise based HTTP client for the browser and node.js.lodash
- A modern JavaScript utility library delivering modularity, performance & extras.Drilling through the code will give us an understanding on the implementation if you are aware of implementing simple React applications.
Of the whole, we shall focus on below aspects rather than the core concepts of React.
Hooks
are a new addition in React 16.8
. Hooks provide a more direct API to the React concepts you already know: props, state, context, refs, and life cycle. As we will show later, Hooks also offer a new powerful way to combine them.
Refer to the
Below are some highlights:
★ useState
hook allows us to use state in our functional components. A useState hook takes the initial value of our state as the only argument, and it returns an array of two elements. The first element is our state variable and the second element is a function in which we can use the update the value of the state variable.
const [form, setState] = useState({ sku: '',
name: '',
description: '',
category: '',
price: '',
inventory: ''
});
const updateField = e => {
setState({ ...form,
[e.target.name]: e.target.value
});
};
const updateCategoryChange = selectedOption => {
setState({ ...form,
category: selectedOption
});
};
★ useEffect
adds the ability to perform side effects from a function component. It serves the same purpose as componentDidMount
, componentDidUpdate
, and componentWillUnmount
in React classes, but unified into a single API. By default, React runs the effects after every render — including the first render.
const [selectedCatalogueItem, setSeletedCatalogueItem] = useState({
sku: '',
name: '',
description: '',
category: '',
price: '',
inventory: ''
});
useEffect(() => { const catalogueItemSku = sku;
getCatalogueItem(sku);
const selectedCatalogueItem = catalogueItems.find(catalogueItem => catalogueItem.sku === catalogueItemSku);
setSeletedCatalogueItem(selectedCatalogueItem);
}, []);
★ createContext
hook allows us to consume the value of a context. It is introduced part of the Context API
. Context provides a way to pass data through the component tree without having to pass props down manually at every level.
export const GlobalContext = createContext();
return (<GlobalContext.Provider value={{
catalogueItems: state.catalogueItems,
notification: state.notification,
removecatalogueItem,
addcatalogueItem,
editcatalogueItem,
getCatalogueItems,
getCatalogueItem
}}>
{children}
</GlobalContext.Provider>);
★ useContext
hook lets you subscribe to React context without introducing nesting.
const { catalogueItems, getCatalogueItem, editcatalogueItem } = useContext(GlobalContext);
const onSubmit = e => {
e.preventDefault();
editcatalogueItem(selectedCatalogueItem);
history.push('/');
}
★ useReducer
hook lets you manage local state of complex components with a reducer. It allows functional components in React access to reducer functions from your state management. It accepts reducer of type (state, action)
=> newState, and returns the current state paired with a dispatch
method.
export default (state, action) => {
if(action.type === 'API_CALL_FAILED') { console.log(action.payload,action.error);
return {
...state,
notification: prepareNotificationToDisplay("failure", action.payload)
};
}
else if(action.type === 'GET_CATALOGUE_ITEMS') {
//console.log("===> GET_catalogueItemS_ITEMS received", action.payload); return {
...state,
catalogueItems: action.payload
};
}
}
const [state, dispatch] = useReducer(AppReducer, initialState);
function dispatchAPICallFailure(message, e) {
dispatch({ type: 'API_CALL_FAILED',
error: e,
payload: message
});
}
Support for internationalization is achieved with react-i18next
. With simple steps, we can localize the application.
Ensure you have the below dependencies in package.json
"dependencies": {
...
...
"i18next": "^19.4.4",
"i18next-browser-languagedetector": "^4.1.1",
"i18next-http-backend": "^1.0.6",
"react-i18next": "^11.4.0",
...
...
}
Start with configuring i18n by creating i18n.js
at the root of src
folder as below. This will register
/locales/{{lng}}/{{ns}}.json
along with Observing the init
block, The fallback language is set to en
and debug
to true
which will log console messages which will help us to track any missing translation messages in language files.
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
i18n
// load translation using http -> see /public/locales
// learn more: https://github.com/i18next/i18next-http-backend
.use(Backend)
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
fallbackLng: 'en', debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
});
export default i18n;
To bundle this to the application, Import i18n.js
in index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import './stylesheet/styles.css';
import App from './App';
// import i18n (needs to be bundled ;))
import './i18n';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)
As mentioned earlier, translation messages are loaded from Backend
with default path for loading the translation language files from /locales/{{lng}}/{{ns}}.json
. To get this working, create locales
file under public
folder and start creating folders for languages that are being supported with translation json files.
Below is sample for translation.json
from en
and de
language folders:
{
"title": "Catalogue Items Listing",
"controls": {
"add": "Add Catalogue Item",
"addItem": "Add Item",
"editItem": "Edit Item",
"cancel": "Cancel"
},
"nodata": "No Data",
"form": {
"sku": "SKU",
"name": "Name",
"description": "Description",
"category": "Category",
"price": "Price",
"inventory": "Inventory"
},
...
...
}
{
"title": "Catalogusitems",
"controls": {
"add": "Catalogusitem toevoegen",
"addItem": "Voeg item toe",
"editItem": "Item bewerken",
"cancel": "annuleren"
},
"nodata": "Geen informatie",
"form": {
"sku": "SKU",
"name": "Naam",
"description": "Omschrijving",
"category": "Categorie",
"price": "Prijs",
"inventory": "Voorraad"
},
...
...
}
Language can be changed by detecting for the change with i18next-browser-languageDetector
or can be changed with user action.
Navigate to Heading.js
and observe how UI controls are added to switch language.
...
...
export const Heading = () => {
const { t, i18n } = useTranslation(); const { notification } = useContext(GlobalContext);
const [notificationToDisplay, setNotificationToDisplay]
= useState({
display: false,
message: ''
});
const [language, setLanguage] = useState('en');
const changeLanguage = lng => { i18n.changeLanguage(lng);
setLanguage(lng);
};
useEffect(() => {
setLanguage(i18n.language);
setNotificationToDisplay(notification);
}, [notification, i18n.language]);
return (
<Fragment>
<div className="mt-5">
<button className={(language === 'en' ? 'bg-blue-500' : 'bg-blue-200') + ` hover:bg-blue-700 text-white font-bold py-2 px-4 rounded m-2`} onClick={() => changeLanguage('en')}>English</button>
<button className={(language === 'de' ? 'bg-blue-500' : 'bg-blue-200') + ` hover:bg-blue-700 text-white font-bold py-2 px-4 rounded`} onClick={() => changeLanguage('de')}>Dutch</button>
</div>
...
...
As highlighted, language state
is defined and is being set in useEffect
which will be invoked upon rendering the component and also upon onClick action. Based on language value, background
of the button is changed resulting in below output when passing
language as query param http://localhost:3000/?lng=de
or click on language button.
Axios
is the http client used to integrate with backend API. Axios is promise-based
library which uses
Axios
allows us to define a base instance
with which we can define a URL and any other configuration elements. http-common.js
exports a new axios instance with these configuration details.
import axios from "axios";
export default axios.create({
baseURL: "http://localhost:8080/api/v1", headers: {
"Content-type": "application/json" }
});
API integrations to catalogue management backend service are defined in CatalogueService.js
and called upon from context provider functions in GlobalState.js
. Below is the sample for getting catalogue items and adding catalogue item.
const getCatalogueItems = async () => {
await catalogueService
.getAll() .then(response => {
//console.log("======> getCatalogueItems response :: ",response.data);
dispatch({
type: 'GET_CATALOGUE_ITEMS', payload: response.data.data
});
})
.catch(e => {
dispatchAPICallFailure('notifications.failure.itemGetAll', e); });
};
const addcatalogueItem = async (catalogueItem) => {
state.notification.display = false;
await catalogueService .create(catalogueItem)
.then(response => {
//console.log("======> addcatalogueItem response :: ",response);
dispatch({
type: 'ADD_catalogueItem', payload: catalogueItem
});
})
.catch(e => {
dispatchAPICallFailure('notifications.failure.itemAdded', e); });
};
Based on the response status, dispatch event is published which will be handled in AppReducer.js
to display appropriate notification message.
export default (state, action) => {
function prepareNotificationToDisplay(status, message) { return {
display: true,
status: status,
message: message
}
}
if(action.type === 'API_CALL_FAILED') { console.log(action.payload,action.error);
return {
...state,
notification: prepareNotificationToDisplay("failure", action.payload) };
}
else if(action.type === 'ADD_catalogueItem') { //console.log("===> ADD_catalogueItem ::", state, action);
return {
...state,
catalogueItems: [...state.catalogueItems, action.payload],
notification: prepareNotificationToDisplay("success", 'notifications.success.itemAdded') };
}
...
...
...
}
Ensure to install node dependencies before starting the application.
$ yarn install
Below are the scripts configured in package.json
which enables us to start the application in dev
mode, build
and test
the app.
"scripts": {
"dev": "react-scripts start --no-cache",
"build": "react-scripts build'",
"test": "react-scripts test --no-cache",
"eject": "react-scripts eject",
"serve": "serve -s build"
}
Start application in dev
mode. Upon successfully starting, browser should be opened with url pointing to http://localhost:3000
$ yarn dev
Run the below command to create optimized production build, which can be deployed or served using
http://localhost:5000
.$ yarn build
$ yarn serve
In the world of containers, applications are packed as cloud native apps and deployed to platforms which ensure apps to run efficiently and scale easily.
With different environments and their hostname changing for each deployment, it is possible to configure backend applications with service discovery services or injected during runtime when deployed to container orchestration platforms. But this is not possible for frontend applications as we cannot define environment variables inside the browser environment.
To ensure frontend apps are connected to scaling backend services, we need to inject environment variables when we start the container. Then we can read environment variables from inside the container and do appropriate actions such that the frontend app can access the backend service from browser.
Steps on configuring in the application, using it in the code and passing the Runtime Environment Variable
to docker container are explained in detail in
Below are brief changes done as per steps defined in the
.env
file to the root of the project with below environment variableAPI_URL=http://localhost:8080/api/v1
env.sh
with content below. This will read the environment variable if exists or use default one in .env
and create env-config.js
file which will be generated when application is started using yarn dev
or when docker container is ran for the first time.#!/bin/bash
# Recreate config file
rm -rf ./env-config.js
touch ./env-config.js
# Add assignment
echo "window._env_ = {" >> ./env-config.js
# Read each line in .env file
# Each line represents key=value pairs
while read -r line || [[ -n "$line" ]];
do
# Split env variables by character `=` if printf '%s\n' "$line" | grep -q -e '='; then
varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
fi
# Read value of current variable if exists as Environment variable value=$(printf '%s\n' "${!varname}")
# Otherwise use value from .env file
[[ -z $value ]] && value=${varvalue}
# Append configuration property to JS file
echo " $varname: \"$value\"," >> ./env-config.jsdone < .env
echo "}" >> ./env-config.js
env-config.js
in public/index.js
to load the environment variable<html>
<head>
...
...
<title>2much2learn-shopping-app</title>
<script src="%PUBLIC_URL%/env-config.js"></script>
</head>
...
...
</html>
http-common.js
import axios from "axios";
export default axios.create({
baseURL: window._env_.API_URL, headers: {
"Content-type": "application/json"
}
});
Dockerfile
with content below. As highlighted, this is docker multistage
build which first builds the application and then copy the output from application build to nginx
container along with nginx config files available in conf
folder.# => Build container
FROM node:alpine as builder
WORKDIR /app
COPY package.json .
COPY yarn.lock .
RUN yarn
COPY . .
RUN yarn build
# => Run container
FROM nginx:1.17.10-alpine
# Nginx config
RUN rm -rf /etc/nginx/conf.d
COPY conf /etc/nginx
# Static build
COPY /app/build /usr/share/nginx/html/
# Default port exposure
EXPOSE 80
# Initialize environment variables into filesystem
WORKDIR /usr/share/nginx/html
COPY ./env.sh .COPY .env .
# Add bash
RUN apk add --no-cache bash
# Run script which initializes env vars to fs
RUN chmod +x env.sh# RUN ./env.sh
# Start Nginx server
CMD ["/bin/bash", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\""]
API_URL
environment variable is passed when running the docker image which will be used when accessing backend service.$ docker build . -t narramadan/crud-react-2much2learn-shopping-app
$ docker run -p 3000:80 -e API_URL=https://catalogue-crud.api.com -t narramadan/crud-react-2much2learn-shopping-app
$ docker push -t narramadan/crud-react-2much2learn-shopping-app
package.json
to run env.sh
and create env-config.js
in dev
mode."scripts": {
"dev": "chmod +x ./env.sh && ./env.sh && cp env-config.js ./public/ && react-scripts start --no-cache", "build": "sh -ac '. ./.env; react-scripts build'", "test": "react-scripts test --no-cache",
"eject": "react-scripts eject",
"serve": "serve -s build"
}
As Fullstack Developer, its ideal to get your hands dirty with both backend and frontend development and implementing CRUD
application is the easy way to gain hands on knowledge on the technology stack. This is just tip of an iceberg with getting things done with React
.
With capabilities added to use Runtime Environment Variables
to pass backend host details, frontend application can be configured to connect to backend service running in any environment without change in code or docker image.