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

I have 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.

Material-table example ReactJS

There are a lot of scenarios in Material-table that you might have difficulty in understanding from the documentation or may be scattered on different forums. I’ll try to cover up the scenarios which I faced while implementing a table in my Project with Actions like Add/Edit/Delete. Another scenario is to make it read-only i.e. remove all the Action buttons.

This is the sample courses React App over which I’ll develop further using Material-Table. You can get the code from the Github Project here. To get started, you’ll need the following packages:

npm i @material-ui/core
npm i @material-ui/icons
npm i @material-ui/lab
npm i material-table
npm i axios

The actions are managed through uncontrolled fields i.e. without managing through state directly. The API calls are as per my example and URL placeholder requires to be updated as per your API host name.

The columns and icons are set in the state. autoFocus property is set to the first TextField for edit mode.

The options prop which I’ve used has Paging, sorting and dragging of columns disabled. Sorting and draggable can simply be set to true and used.

To make the table read-only, the editable prop of Material Table can be set to null conditionally. I’ve used isReadOnly boolean to add the condition, you can have your own state here.

render() {
    const { classes } = this.props;
    return (
      <Fragment>
         <div
            id={this.props.id}
          >
            <MaterialTable
              components={{
                EditRow: (props) => {
                  return (
                    <MTableEditRow
                      {...props}
                      onEditingCanceled={(mode, rowData) => {
                        this.canceledClicked();
                        props.onEditingCanceled(mode);
                      }}
                    />
                  );
                },
              }}
              title=""
              columns={this.state.columns}
              data={this.state.StudentsEnrolled}
              icons={this.state.tableIcons}
              isLoading={this.state.showLoading}
              style={{
                border: "2px solid gray",
                maxWidth: "1450px",
                overflow: "scroll",
                marginTop: "10px",
                marginLeft: "20px",
              }}
              editable={
                isReadOnly
                ? null
                : 
                {
                onRowAdd: (newData) =>
                  new Promise((resolve, reject) => {
                    newData.studentName =
                      studentName_GRC === null ? "" : studentName_GRC;
                    
                    newData.grade = grade_GR === null ? "" : grade_GR;

                    newData.courseName = courseName_GR === null ? "" : courseName_GR;

                    newData.comments = comments_GR === null ? "" : comments_GR;

                    var errorMsg = " ";

                    if (newData.studentName === "") {
                      errorMsg = "Please insert Student Name.";
                    }
                    if (newData.grade === "") {
                      errorMsg = errorMsg + "\nPlease insert grade.";
                    }
                    if (newData.courseName === "") {
                      errorMsg = errorMsg + "\nPlease insert course Name.";
                    }

                    if (errorMsg !== " ") {
                      reject();
                      alert(errorMsg);
                    } else if (this.state.countGrid >= GridDataLimit) {
                      reject();
                      alert(
                        "You cannot add more than " +
                          GridDataLimit +
                          " Students to the list."
                      );
                    } else {
                      this.setState({ showLoading: true });
                      this.postStudentsEnrolledData(newData);
                      resolve();
                    }
                    // }, 600);
                  }),
                onRowUpdate: (newData, oldData) =>
                  new Promise((resolve, reject) => {
                    // setTimeout(() => {
                    //resolve();

                    if (oldData) {
                      newData.studentName =
                        studentName_GRC === null
                          ? oldData.studentName
                          : studentName_GRC === ""
                          ? ""
                          : studentName_GRC;

                      newData.grade =
                        grade_GR === null
                          ? oldData.grade
                          : grade_GR === ""
                          ? ""
                          : grade_GR;

                      newData.courseName =
                        courseName_GR === null
                          ? oldData.courseName
                          : courseName_GR === ""
                          ? ""
                          : courseName_GR;

                      newData.comments =
                        comments_GR === null
                          ? oldData.comments
                          : comments_GR === ""
                          ? ""
                          : comments_GR;

                      var errorMsg = " ";
                      if (newData.studentName === "") {
                        errorMsg = "Please insert Student Name.";
                      }
                      if (newData.grade === "") {
                        errorMsg = errorMsg + "\nPlease insert grade.";
                      }
                      if (newData.courseName === "") {
                        errorMsg = errorMsg + "\nPlease insert course Name.";
                      }
                      if (errorMsg !== " ") {
                        reject();
                        alert(errorMsg);
                      } else {
                        this.setState({ showLoading: true });
                        this.postStudentsEnrolledData(newData);
                        resolve();
                      }
                    }
                    // }, 600);
                  }),
                onRowDelete: (oldData) =>
                  new Promise((resolve) => {
                    // setTimeout(() => {
                    this.setState({ showLoading: true });
                    this.deleteStudentsEnrolledData(oldData);
                    resolve();
                    // }, 600);
                  }),
              }}
              options={{
                paging: false,
                sorting: false,
                draggable: false,
                addRowcourseName:"first",
                rowStyle: { backgroundColor: "#fff" },
              }}
            />
          </div>
      </Fragment>
    );
  }

The isLoading prop manages the hide/show of loader when an action is performed.

There is one more scenario where clicking outside the + icon, doesn’t trigger the onAddRowClick event. This is handled in componentDidMount method.

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>