Deploy ReactJS App Container to Kubernetes on Mac

In the previous post, we Dockerized the ReactJS App and were able expose it from our Container to our machine to run it in the browser on http://localhost:3000. Now we want our Docker containers to be Orchestrated using Kubernetes.

First, we need to enable Kubernetes from Docker Desktop settings. This will install Kubernetes with Apply and Restart option. Just follow the default options.

Verify the installation with to check Client and Server versions:

>kubectl version

Make sure the context is set to docker-desktop which can also be checked from the docker icon at the top.

>kubectl config current-context

docker-desktop

If the context is different (in case you’re also running MiniKube), we can switch it using:

>kubectl config use-context docker-for-desktop

Switched to context "docker-for-desktop"

Check cluster info:

>kubectl cluster-info

Kubernetes master is running at https://kubernetes.docker.internal:6443
KubeDNS is running at https://kubernetes.docker.internal:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

Check the pods running in the cluster:

>kubectl get nodes

NAME             STATUS   ROLES    AGE   VERSION
docker-desktop   Ready    master   23h   v1.19.7

Install the Kubernetes Dashboard with:

>kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc3/aio/deploy/recommended.yaml

The Dashboard application will get deployed as a Pod in the kubernetes-dashboard namespace. We can get a list of all our Pods in all namespaces via the following command:

>kubectl get pods --all-namespaces

kubernetes-dashboard   dashboard-metrics-scraper-c95fcf479-8l8vf   1/1     Running   0          23h
kubernetes-dashboard   kubernetes-dashboard-5bc6d86cfd-6b68m       1/1     Running   1          23h

Just check if your kubernetes-dashboard-* pod is in running state. Now, start the local Proxy server to access your Dashboard:

>kubectl proxy

Starting to serve on 127.0.0.1:8001

Once the proxy server is started, you can access the Dashboard at:

http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy

To Login to the Dashboard, we’ll login using a token. The Token can be generated by running the command:

>kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | awk '/^deployment-controller-token-/{print $1}') | awk '$1=="token:"{print $2}'

Now our basic setup is complete to get Kubernetes running on Mac.

Now we need to get our ReactJS App ready for Deployment on Kubernetes. Create the Deployment and Service manifest to run it on Kubernetes and expose the required port. Create the combined .yml file as below, however these can be created separately as well:

kind: Deployment
apiVersion: apps/v1
metadata:
  name: reactondocker
spec:
  replicas: 2
  selector:
    matchLabels:
      app: reactondocker
  template:
    metadata:
      labels:
        app: reactondocker
    spec:
      containers:
        - name: reactondocker
          image: myimage/reactondocker:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 3000

---

kind: Service
apiVersion: v1
metadata:
  name: reactondocker
spec:
  type: NodePort
  ports:
    - port: 3000
      targetPort: 3000
      protocol: TCP
      nodePort: 30004
  selector:
    app: reactondocker
      

I’m using the NodePort Service to expose it on port 30004. Number of Pods to run is 2.

Note: If you’ve not pushed the image to a registry, then change imagePullPolicy: Never or IfNotPresent so it uses the local image created using Docker on your machine. Also, change the image name accordingly.

Run the below command to create the Deployment and the Service:

>kubectl create -f deployment.yml

deployment.apps/reactondocker created
service/reactondocker created

You can check the Pods running for reactondocker in your Kubernetes dashboard or with the below command:

>kubectl get pods

The status of the Pods should be running. Try accessing the Application in your browser as http://localhost:30004.

Finally, we should delete the ReactJS Application from the Kubernetes cluster:

>kubectl delete service,deployment reactondocker

service "reactondocker" deleted
deployment.apps "reactondocker" deleted
Advertisement

Run ReactJS App in Docker on Mac

This post explains how you can run your ReactJS App using Docker containers on your MacBook. Containers let your Application run in an environment isolated from the rest of your machine. It includes all the necessary files and resources to run your Application as is by creating an image that can be run anywhere as a container.

First you need to download and install the latest version of Docker for Desktop on your machine here. You can check whether your Mac has Intel chip or Apple chip from About This Mac menu option. Follow the installation steps and it should be available to open under Applications.

I’m using VS Code to create the basic ReactJS App. Open VS Code and install create-react-app command if not already installed using Terminal:

npm install -g create-react-app

Use sudo with above command if it gives permission denied error.

Switch to an empty folder and run the following command:

create-react-app reactondocker

Once the App is created, open the folder in VS Code and in the terminal install the required packages using and then build:

npm install
npm run-script build

Now, run the App as below to check it works normally on your machine without Docker:

npm start

Your Application should open fine in a browser. Url would like http://localhost:3000. We need to make sure this port is exposed from the container to your machine to run the Application without issues. This will be done in a Dockerfile.

From the Extensions menu, install Docker extension in your VS Code. Then, open the Command Palette under View menu, search for Dockerfile and select Add Dockerfiles to workspace -> Node.js -> package.json -> confirm port to be exposed, in this case it is 3000.

Dockerfile will look something like this:

FROM node:12.18-alpine
ENV NODE_ENV=production
WORKDIR /usr/src/app
COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"]
RUN npm install --production --silent
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

In short, this configuration is using a base alpine image (linux distribution) with node, sets your working directory to app. Copies the required files, installs packages and exposes the port 3000. The last line is running your Application in the container like you did from VS Code on your machine. You can optionally add a compose file for multi-container Application.

You can create a .dockerignore file to prevent items from getting copied to your image that you don’t want but it is optional.


/node_modules

/build

.git

*.md

.gitignore

Now, time to build the image which can be run as a container. Again, open the command palette and search for Docker Images: build image. It’ll ask for your Dockerfile, select it and observe the command it runs automatically in your VS Code Terminal as below:

docker build --rm --pull -f "/Users/username/Documents/Apps/ReactOnDocker/reactondocker/Dockerfile" --label "com.microsoft.created-by=visual-studio-code" -t "reactondocker:latest" "/Users/username/Documents/Apps/ReactOnDocker/reactondocker"

The above docker build command will create the image which can be found under the Docker menu on the left of your VS Code below extensions.

You can run this image using Docker for Desktop, the image named reactondocker will be available to run. Or right-click on the image name in VS Code and select run interactive. Notice the docker run command as below:

docker run --rm -it  -p 3000:3000/tcp reactondocker:latest

Now you can try running the Application using the same URL http://localhost:3000 this time running inside a container. You can see the running image in the Docker for Desktop as well.

public, src and scripts folder not created with create-react-app

While creating a new ReactJS App on my Windows 10 machine, I faced the issue as described in the title even though I didn’t update any packages recently.

To resolve this, I tried updating node to the latest version from here. But that didn’t work. So I had to uninstall create-react-app package globally using the following command:

npm rm -g create-react-app

Install it again with:

npm install -g create-react-app

Then it worked fine with creating the App again:

npx create-react-app my-app

Download word file without Protected View .Net ReactJS

For an Intranet Application where a .docm template downloaded from a ReactJS UI as a blob is opening in Protected View on the users’ machines. This is happening when downloading in browsers other than IE or Edge browser in IE mode.

When you download the document from Chrome or other browsers, MS Word is considering it as an Internet Application. Hence, the checkbox for Protected View is applying these changes and downloading it in Protected View.

Enable Protected View For Files Originating From The Internet. This setting controls documents opened from a website.

The code to download this as a blob is in ReactJS is available here.

However, if you generate the .docm file on the Server where the API is located and put it in a location on the Server itself. Create a Virtual Directory in IIS to make the document downloadable as http. So just return the hyperlink at the Client-side Javascript. You can receive this hyperlink and assign to anchor link clicking it through code.

var downloadUrl = response.data.Path;
        const link = document.createElement("a");
        link.id = "DownloadLink";
        link.href = downloadUrl;
        link.setAttribute("download", "filename");
        document.body.appendChild(link);
        link.click();
        document.getElementById("DownloadLink").outerHTML = "";

This will prevent it from opening in Protected View.

Also, if your word file has Macros, read-only fields become editable when you click on “Enable Editing” when you open the document in Protected View until you enable the Macros.

Change highlight color Autocomplete Material UI ReactJS

Create the below styles in your ReactJS component:

const styles = (theme) => ({
  ////....
  option: {
    // Hover
 with light-grey
    '&[data-focus="true"]': {
      backgroundColor: '#F8F8F8',
      borderColor: 'transparent',
    },
    // Selected
 has dark-grey
    '&[aria-selected="true"]': {
      backgroundColor: theme.palette.grey.A200,
      borderColor: 'transparent',
    },
  },
});

You can choose the colors based on your preference on selected and for mouse-over for backgroundColor property.

Add the following property in your AutoComplete component

classes={{
	  option: classes.option
	}}

as shown below:

<Autocomplete
	id="combo-box-1"
	options={myvalues}
	getOptionLabel={(option) => option.value}
	size="small"
	className={classes.formControlAutopopulate}
	classes={{
	  option: classes.option
	}}
	value={myvalues.find((x) => x.id === this.state.ID)}
	disabled={this.props.isLocked}
	onChange={this.handleSelected}
	renderInput={(params) => (
	  <TextField
		{...params}
		variant="outlined"
		placeholder="Please Select Value"
		fullWidth
	  />
	)}
/>

Borders not shown in Firefox with border-collapse on material table

I came across this issue that all Material Table used in the ReactJS Application are not showing bottom border in the th row in the Firefox browser. This issue is not reproducible in Chrome.

So, I found css hack where you can simply change the position attribute specifically for the Firefox browser in the default MuiTableCell-head class. This class is applied at the cell level by Material-table.

@-moz-document url-prefix() {
  .MuiTableCell-head {
    position: static !important;
  }
}

The above code will remove the default sticky value for position which causes the issue with border-collapse in the .MuiTable-root class. You can also remove the border-collapse in a similar fashion but do test that it’s not breaking anything else.

The component which has the Material-table code should import the above css to take effect. This hack might also work generally for html table if you’re facing a similar issue without Material-table.

Update Child Components from Parent ReactJS

One common challenge in ReactJS is updating Child Components from Parent Component in a form. I faced this scenario recently where after a series of GET calls where the order is decided by the browser threads, data is not always available in the same order.

If you need to set some permissions based on an API GET call and then enable/disable a child component, you can use the following ReactJS lifecycle method:

//Parent Component
shouldComponentUpdate(nextProps, nextState) {
    if (nextState.formReady) {
      return true;
    } else {
      return false;
    }
  }

Set the flag formReady in the series of successive GET calls to true, where you’re sure will be the last call on your Parent form.

//Parent Component GET call inside axios success.
this.setState(
{
  formReady: true,
},() => console.log(this.state.formReady);
);

The shouldComponentUpdate lifecycle method will check for the boolean value and will re-render or refresh the entire form including Child Components.

However, do refer to the documentation before using this method to decide if this suits your needs.

Set focus on tabbing ReactJS

I was working on Material-UI ExpansionPanel and had come across a task where I had to set focus on tabbing through them using keyboard. So when you press space on Focus, the Panel would open/close.

The ExpansionPanel sample implementation would be as below inside render:

return(
	<div>
	  <div className={classes.root}>
		{this.state.panelTypes.map((item) => (
		  <ExpansionPanel key={item.userType} expanded={this.state.expanded === item.userType} onChange={this.handleChange(item.userType)}>
			<ExpansionPanelSummary
			  expandIcon={<ExpandMoreIcon />}
			  aria-controls="panel1bh-content"
			  id="panel1bh-header"
			  style={{backgroundColor: "#e6e6e6", border: '1px solid silver'}}
			  className={classes.heading}
			>
			  <Typography className={classes.heading}>Heading</Typography>
			</ExpansionPanelSummary>
			<ExpansionPanelDetails>
			... display data here...
			</ExpansionPanelDetails>
		  </ExpansionPanel>
		))}
	  </div>
	</div>
  );

The above code is mapping through an array of PanelTypes for creating multiple ExpansionPanels.

The class would require the pseudo-class :focus to be used for tabbing through the Expansion Panel heading:

const styles = (theme) => ({
	//There might be other classes here..
  heading: {
    fontSize: theme.typography.pxToRem(15),
    flexBasis: '33.33%',
    flexShrink: 0,
    fontFamily: "Verdana,Arial,sans-serif",
    '&:focus': {
      outline: "-webkit-focus-ring-color auto 1px"
    }
  }
});

Similarly, other pseudo-classes can be used inside the styles e.g. ::after, ::before etc. as per their own usage.

Render elements dynamically using map Reactjs

Here I’m using a component that needs to render multiple ExpansionPanel Material UI components based on data fetched in an array. The map is using Arrow function with () for an implicit return.

Below is the sample code for the Component:

import React, { Component } from "react";
import Header from "./Header";
import { withStyles } from '@material-ui/core/styles';
import ExpansionPanel from '@material-ui/core/ExpansionPanel';
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
import Typography from '@material-ui/core/Typography';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import loadingImg from "../images/LoadingImg_EZ.gif";

const styles = (theme) => ({
  root: {
    width: '100%',
  },
  heading: {
    fontSize: theme.typography.pxToRem(15),
    flexBasis: '33.33%',
    flexShrink: 0,
    fontWeight: 'bold',
  },
  secondaryHeading: {
    fontSize: theme.typography.pxToRem(15),
    color: theme.palette.text.secondary,
  },
  OfflineText: {
    fontSize: "20px",
    color: "red",
  },
});

class MyComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      expanded: false,
      panelTypes: null,
      summary: null
    };
  }

  componentWillMount() {
    //get data here and set state.
  }

  handleChange = (panel) => (event, isExpanded) => {
    this.setState({ expanded: isExpanded? panel: false});
  };

  render() {
      const { classes } = this.props;
      
      return(
        <div>
        <Header />
        {this.state.panelTypes !=null ? (
        <>
          <h3 style={{fontSize: "15px", paddingLeft: "4px"}}>Summary</h3>
          <div className={classes.root}>
            {this.state.panelTypes.map((item) => (
              <ExpansionPanel expanded={this.state.expanded === item.types} onChange={this.handleChange(item.types)}>
                <ExpansionPanelSummary
                  expandIcon={<ExpandMoreIcon />}
                  aria-controls="panel1bh-content"
                  id="panel1bh-header"
                  style={{backgroundColor: "#e6e6e6"}}
                >
                  <Typography className={classes.heading}>{this.state.panelTypes !=null ? `${item.types} (Year ${item.year})`:''}</Typography>
                </ExpansionPanelSummary>
                <ExpansionPanelDetails>
                  <Typography>
                    Nulla facilisi. Phasellus sollicitudin nulla et quam mattis feugiat. Aliquam eget
                    maximus est, id dignissim quam.
                  </Typography>
                </ExpansionPanelDetails>
              </ExpansionPanel>
            ))}
          </div>
          </>) : (
          <div id="loading">
            <img id="loading-image" src={loadingImg} alt="Loading..." />
          </div>
        )}
        </div>
      );
  }

}

export default withStyles(styles, { withTheme: true })((MyComponent));
panelTypes will hold the array of items that will be used to create the same number of Expansion Panels using this.state.panelTypes.map() in the render method.

Check Application is Online ReactJS

It is a very common scenario to check whether the user is online while using your Web Application to make it robust. May be you need to just show a message to the user that the Internet connection is disrupted.

There is a very cool react package react-detect-offline available on npm that helps with this. It uses a default polling url to check whether you’re online. You can however change a few settings including the ping URL, enabled boolean and interval for ping duration. You can also change the timeout setting but I’ve not used it here in the polling object.

//say this code is inside constants.js file under common folder..
export const APIUrl = "https://testapi";
export const pingUrl = APIUrl + "/ping";
export const polling = {enabled: true, url: pingUrl, interval: 10000};
export const OfflineText = "You're Offline. Please check your Internet connection.";

In your component, import these objects and use it as below:

import { Offline, Online } from "react-detect-offline";
import { OfflineText, polling } from "../Common/Constants";

const styles = (theme) => ({
  OfflineText: {
    fontSize: "20px",
    color: "red",
  },
});

class MyComponent extends Component {
	render() {
		const { classes } = this.props;
		return (
		  <div id="loading">
            <Online polling={polling}>
              <img id="loading-image" src={loadingImg} alt="Loading..." />
            </Online>
            <Offline polling={polling}>
              <div className={classes.OfflineText}>{OfflineText}</div>
            </Offline>
		  </div>
		)
	}
}

The above example will ping every 10 seconds to check https://testapi for online status. This is called using http HEAD GET request. So please make sure both http verbs are allowed in your API.

For .net core users, the WebAPI should have the [HEAD] attribute set on the action and the method should be allowed in the Startup.cs file and Web.config.

Example of handlers in web.config as below:

<handlers>
            <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
            <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
            <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" resourceType="Unspecified" requireAccess="Script" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
            <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" resourceType="Unspecified" requireAccess="Script" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
        </handlers>