Frontend

A Step by Step guide on Implementing & Dockerizing React CRUD RESTful API Integrated Application

2much2learn - A Step by Step guide on Implementing & Dockerizing React CRUD RESTful API Integrated Application
Clone the source code of the article from dockerizing-react-crud-restful-api-integrated-application

Introduction

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.

"Catalogue Listing"
Catalogue Listing

"Adding Catalogue Item"
Adding Catalogue Item

"Editing Catalogue Item with labels displayed in dutch"
Editing Catalogue Item with labels displayed in dutch

"Delete confirmation in dutch"
Delete confirmation in dutch

Technology stack used in this Article to build and test drive the application

Prerequisites

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 Configure Development environment on Ubuntu 19.10 article which has detailed steps on setting up development environment on Ubuntu.

At the least, you should have below softwares and tools installed to try out the application:

  • Node& Yarn
  • Docker & Docker Compose
Prechecks
$ node -v
v12.16.1

$ yarn -v
1.22.4

$ docker -v
Docker version 19.03.6, build 369ce74a3c

Bootup RESTful API needed for this integration

There is a previous article published on Creating RESTful Microservice using Quarkus which exposes APIs that are compatible to be used with this react application.

Microservice is built using Quarkus to expose APIs and also support handling/publishing events to Kafka. This is integrated with Graylog for centralized logging and Jaeger for distributed tracing.

Below are the Restful APIs that are exposed by Catalogue Management System microservice application.

HTTP
Method
API NamePathResponse
Status Code
POSTCreate Catalogue Item/201
(Created)
GETGet Catalogue Items/200
(Ok)
GETGet Catalogue Item/{sku}200
(Ok)
PUTUpdate Catalogue Item/{sku}200
(Ok)
DELETEDelete Catalogue Item/{sku}204
(No Content)
Clone the source code of the article from restful-event-driven-microservice-using-quarkus-jpa-kafka

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.

Exploring the React Application

Project is bootstrapped with create-react-app and configured with additional dependencies.

Clone the source code of the article from dockerizing-react-crud-restful-api-integrated-application

This should clone the application with below project structure and dependencies added to package.json

"Project Structure"
Project Structure

"Project dependencies"
Project Dependencies

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.

React Hooks

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 documentation for detailed understanding on React Hooks.

Below are some highlights:

  • Hooks allow you to reuse stateful logic without changing your component hierarchy.
  • Hooks let you split one component into smaller functions based on what pieces are related.
  • Hooks let you use more of React’s features without classes.
  • Hooks work side-by-side with existing code so you can adopt them gradually.

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.

Initialize state with current state and function to update it
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.

Accessing logic upon every 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.

Creating GlobalContext which will be available to all components
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.

Getting and calling context functions
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.

AppReducer.js
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
      };
  }
}
using reducer hook
const [state, dispatch] = useReducer(AppReducer, initialState);
function dispatchAPICallFailure(message, e) {
    dispatch({        type: 'API_CALL_FAILED',
        error: e,
        payload: message
    });
}

Internationalization support

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

package.json
"dependencies": {
  ...
  ...
  "i18next": "^19.4.4",
  "i18next-browser-languagedetector": "^4.1.1",
  "i18next-http-backend": "^1.0.6",
  "react-i18next": "^11.4.0",
  ...
  ...
}

Configure i18n

Start with configuring i18n by creating i18n.js at the root of src folder as below. This will register Backend making HTTP calls to load translation files from default path /locales/{{lng}}/{{ns}}.json along with LanguageDetector which detects user language in the browser by many different means.

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.

i18n.js
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

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')
)

Translate the application

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:

"Translation files"
Translation files

English Translation
{
  "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"
  },
  ...
  ...
}
Dutch Translation
{
  "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"
  },
  ...
  ...
}

Changing language

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.

Heading.js
...
...
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.

"When language is set to English"
When language is set to English

"When language is set to Dutch"
When language is set to Dutch

API Integration

Axios is the http client used to integrate with backend API. Axios is promise-based library which uses async and await for implementing asynchronous code.

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.

http-common.js
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.

CatalogueService.js
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.

AppReducer.js
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')      };
  }
  ...
  ...
  ...
}

"Upon successfully adding catalogue item"
Upon successfully adding catalogue item

"Upon failure when adding catalogue item"
Upon failure when adding catalogue item

Starting & Building the application

Ensure to install node dependencies before starting the application.

Ensure to install dependencies
$ 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.

package.json
"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

Start in dev mode
$ yarn dev

Run the below command to create optimized production build, which can be deployed or served using serve package and accessible at http://localhost:5000.

Build and serve %the application
$ yarn build

$ yarn serve

Dockerizing React Application with Runtime environment variables

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 this article.

Below are brief changes done as per steps defined in the article.

  • Create .env file to the root of the project with below environment variable
.env
API_URL=http://localhost:8080/api/v1
  • Create 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.
env-config.js
#!/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
  • Include env-config.js in public/index.js to load the environment variable
index.js
<html>
  <head>
    ...
    ...
    <title>2much2learn-shopping-app</title>
    <script src="%PUBLIC_URL%/env-config.js"></script>
  </head>
  ...
  ...
</html>
  • Use the environment variable in http-common.js
http-common.js
import axios from "axios";

export default axios.create({
  baseURL: window._env_.API_URL,  headers: {
    "Content-type": "application/json"
  }
});
  • Create 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.
Dockerfile
# => 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 --from=builder /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;\""]
  • Now, it’s time to create and test drive the docker image. Follow below commands to create the build and run it. API_URL environment variable is passed when running the docker image which will be used when accessing backend service.
Create docker image and run it
$ 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
  • To support this during development, minor changes needs to be done to package.json to run env.sh and create env-config.js in dev mode.
package.json
"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"
}

Conclusion

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.

Clone the source code of the article from dockerizing-react-crud-restful-api-integrated-application
author

Madan Narra21 Posts

Software developer, Consultant & Architect

Madan is a software developer, writer, and ex-failed-startup co-founder. He has over 10+ years of experience building scalable and distributed systems using Java, JavaScript, Node.js. He writes about software design and architecture best practices with Java and is especially passionate about Microservices, API Development, Distributed Applications and Frontend Technologies.

  • Github
  • Linkedin
  • Facebook
  • Twitter
  • Instagram

Contents

Related Posts

Get The Best Of All Hands Delivered To Your Inbox

Subscribe to our newsletter and stay updated.