TL;DR: There are several tools available for developers to aid the building of various types of websites and applications. One such tool is Create React App(CRA), the CLI tool that helps JavaScript developers create react apps with no build configuration. As awesome as CRA is, developers still need a way of tweaking, adding special scripts and modules that doesn't come bundled with CRA. Today, I'll teach you how to create custom create-react-app
scripts for you and your team!
Many developers already use create-react-app to build their React applications, but like I mentioned earlier, developers are still screaming for more configuration options! Some are interested in having support for:
- PostCSS
- CSS Modules
- LESS
- SASS
- ES7
- MobX
- Server Rendering
..and a lot more out of the box!
A lot of developers, including JavaScript newbies create React apps from scratch daily, so the CRA team at Facebook built the create-react-app tool to make the process of creating such apps less tedious and error-prone.
As a developer that needs support for some of the technologies I highighted earlier, one way of going about it is running npm run eject
. This command copies all the config files and dependencies right into your project, then you can manually configure your app with all sorts of tools to satisfaction.
One major challenge developers might face with eject is not been able to enjoy the future features of CRA . Another challenge with eject would be ineffecient synchronised setup across React developers working in team. One great way of solving this later challenge is publishing a fork of react-scripts
for your team, then all your developers can just run create-react-app my-app --scripts-version mycompany-react-scripts
and have the same setup across board. Let's learn how to accomplish that!
Create a Fork
Open up your GitHub repo and fork the create-react-app repo
Note: It is recommended that you fork from the latest stable branch. Master is unstable.
Inside the packages
directory, there is a folder called react-scripts
. The react-scripts
folder contains scripts for building, testing and starting your app. In fact, this is where we can tweak, configure and add new scripts and templates.
Tweak the Configuration
Clone the directory and open up the react-scripts/scripts/init.js
in your code editor. Let's add some few console messages like so:
......
......
console.log(chalk.red('VERY IMPORTANT:'));
console.log('Create a .env file at the root of your project with REACT_APP_EMPLOYEE_ID and REACT_APP_POSITION_ID');
console.log(' You can find these values in the company dashboard under application settings.');
console.log(' https://company.bamboohr.com/settings');
console.log();
.......
Now, Let's change templates
Open up react-scripts/template/src/App.js
and replace it with this:
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
getEnvValues() {
if (!process.env.REACT_APP_EMPLOYEE_ID || !process.env.REACT_APP_POSITION_ID) {
throw new Error('Please define `REACT_APP_EMPLOYEE_ID` and `REACT_APP_POSITION_ID` in your .env file');
}
const employeeID = process.env.REACT_APP_EMPLOYEE_ID
const position = process.env.REACT_APP_POSITION_ID;
return { employeeID, position };
}
render() {
const { employeeID, position } = this.getEnvValues();
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to Unicode Labs</h2>
</div>
<p className="App-intro">
<b> Employee ID: { employeeID } </b><br/><br/>
<b> Position: { position } </b>
</p>
</div>
);
}
}
export default App;
Now, go to react-scripts/template/public
directory. Open the index.html
file and change the value of the <title>
tag to Unicode Labs
.
You can also change the favicon to your company's favicon. You can change as many things as you want and add custom components that your team uses frequently.
Create an .env.example
in the react-scripts/template
directory that contains the following:
REACT_APP_EMPLOYEE_ID='44566'
REACT_APP_POSITION_ID='ENGR'
A user will have to rename it to .env
once the create-react-app
tool is done installing the react-scripts
. You should add this instruction to the README
file.
Note: CRA already includes support for custom env variables if you're open to prefixing their names with REACT_APP.
That's all we need!
Publish react-scripts to NPM
Before publishing to npm, we need to change the value of the name
key of the package.json
file in react-scripts
directory to unicodelabs-react-scripts
.
Change the value of the description
key to Unicodelabs Configuration and scripts for Create React App.
Also, point the value of the repository
key to the right location. In my case, it is unicodelabs/create-react-app
.
Now, cd
to the react-scripts
directory from your terminal like so:
You need to login to npm like so:
Go ahead and publish
Test Your Custom Script
Head over to your terminal and run:
create-react-app test-app --scripts-version unicodelabs-react-scripts
In your own case it would be yourname-react-scripts
, where yourname
is your company name or whatever name you choose to give it.
CRA would install it and then you will see a notice like so:
Remember, when we put this message in the code earlier? Awesome!
Now, cd
into the test-app
directory, rename the .env.example
to .env
and run npm start
command.
Your app will spin up with the new template like so:
Note: If you have yarn installed, then create-react-app would install your app using Yarn.
Aside: Securing React Apps with Auth0
As you will learn in this section, you can easily secure your React applications with Auth0, a global leader in Identity-as-a-Service (IDaaS) that provides thousands of enterprise customers with modern identity solutions. Alongside with the classic username and password authentication process, Auth0 allows you to add features like Social Login, Multifactor Authentication, Passwordless Login, and much more with just a few clicks.
To follow along the instruction describe here, you will need an Auth0 account. If you don't have one yet, now is a good time to sign up for a free Auth0 account.
Also, if you want to follow this section in a clean environment, you can easily create a new React application with just one command:
npx create-react-app react-auth0
Then, you can move into your new React app (which was created inside a new directory called react-auth0
by the create-react-app
tool), and start working as explained in the following subsections.
Setting Up an Auth0 Application
To represent your React application in your Auth0 account, you will need to create an Auth0 Application. So, head to the Applications section on your Auth0 dashboard and proceed as follows:
- click on the Create Application button;
- then define a Name to your new application (e.g., "React Demo");
- then select Single Page Web Applications as its type.
- and hit the Create button to end the process.
After creating your application, Auth0 will redirect you to its Quick Start tab. From there, you will have to click on the Settings tab to whitelist some URLs that Auth0 can call after the authentication process. This is a security measure implemented by Auth0 to avoid the leaking of sensitive data (like ID Tokens).
So, when you arrive at the Settings tab, search for the Allowed Callback URLs field and add http://localhost:3000/callback
into it. For this tutorial, this single URL will suffice.
That's it! From the Auth0 perspective, you are good to go and can start securing your React application.
Dependencies and Setup
To secure your React application with Auth0, there are only three dependencies that you will need to install:
auth0.js
: This is the default library to integrate web applications with Auth0.react-router
: This is the de-facto library when it comes to routing management in React.react-router-dom
: This is the extension to the previous library to web applications.
To install these dependencies, move into your project root and issue the following command:
npm install --save auth0-js react-router react-router-dom
Note: As you want the best security available, you are going to rely on the Auth0 login page. This method consists of redirecting users to a login page hosted by Auth0 that is easily customizable right from your Auth0 dashboard. If you want to learn why this is the best approach, check the Universal vs. Embedded Login article.
After installing all three libraries, you can create a service to handle the authentication process. You can call this service Auth
and create it in the src/Auth/
directory with the following code:
// src/Auth/Auth.js
import auth0 from 'auth0-js';
export default class Auth {
constructor() {
this.auth0 = new auth0.WebAuth({
// the following three lines MUST be updated
domain: '<AUTH0_DOMAIN>',
audience: 'https://<AUTH0_DOMAIN>/userinfo',
clientID: '<AUTH0_CLIENT_ID>',
redirectUri: 'http://localhost:3000/callback',
responseType: 'token id_token',
scope: 'openid profile',
});
this.getProfile = this.getProfile.bind(this);
this.handleAuthentication = this.handleAuthentication.bind(this);
this.isAuthenticated = this.isAuthenticated.bind(this);
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.setSession = this.setSession.bind(this);
}
getProfile() {
return this.profile;
}
handleAuthentication() {
return new Promise((resolve, reject) => {
this.auth0.parseHash((err, authResult) => {
if (err) return reject(err);
console.log(authResult);
if (!authResult || !authResult.idToken) {
return reject(err);
}
this.setSession(authResult);
resolve();
});
});
}
isAuthenticated() {
return new Date().getTime() < this.expiresAt;
}
login() {
this.auth0.authorize();
}
logout() {
// clear id token and expiration
this.idToken = null;
this.expiresAt = null;
}
setSession(authResult) {
this.idToken = authResult.idToken;
this.profile = authResult.idTokenPayload;
// set the time that the id token will expire at
this.expiresAt = authResult.expiresIn * 1000 + new Date().getTime();
}
}
The Auth
service that you just created contains functions to deal with different steps of the sign in/sign up process. The following list briefly summarizes these functions and what they do:
getProfile
: This function returns the profile of the logged-in user.handleAuthentication
: This function looks for the result of the authentication process in the URL hash. Then, the function processes the result with theparseHash
method fromauth0-js
.isAuthenticated
: This function checks whether the expiry time for the user's ID token has passed.login
: This function initiates the login process, redirecting users to the login page.logout
: This function removes the user's tokens and expiry time.setSession
: This function sets the user's ID token, profile, and expiry time.
Besides these functions, the class contains a field called auth0
that is initialized with values extracted from your Auth0 application. It is important to keep in mind that you have to replace the <AUTH0_DOMAIN>
and <AUTH0_CLIENT_ID>
placeholders that you are passing to the auth0
field.
Note: For the
<AUTH0_DOMAIN>
placeholders, you will have to replace them with something similar toyour-subdomain.auth0.com
, whereyour-subdomain
is the subdomain you chose while creating your Auth0 account (or your Auth0 tenant). For the<AUTH0_CLIENT_ID>
, you will have to replace it with the random string copied from the Client ID field of the Auth0 Application you created previously.
Since you are using the Auth0 login page, your users are taken away from the application. However, after they authenticate, users automatically return to the callback URL that you set up previously (i.e., http://localhost:3000/callback
). This means that you need to create a component responsible for this route.
So, create a new file called Callback.js
inside src/Callback
(i.e., you will need to create the Callback
directory) and insert the following code into it:
// src/Callback/Callback.js
import React from 'react';
import { withRouter } from 'react-router';
function Callback(props) {
props.auth.handleAuthentication().then(() => {
props.history.push('/');
});
return <div>Loading user profile.</div>;
}
export default withRouter(Callback);
This component, as you can see, is responsible for triggering the handleAuthentication
process and, when the process ends, for pushing users to your home page. While this component processes the authentication result, it simply shows a message saying that it is loading the user profile.
After creating the Auth
service and the Callback
component, you can refactor your App
component to integrate everything together:
// src/App.js
import React from 'react';
import { withRouter } from 'react-router';
import { Route } from 'react-router-dom';
import Callback from './Callback/Callback';
import './App.css';
function HomePage(props) {
const { authenticated } = props;
const logout = () => {
props.auth.logout();
props.history.push('/');
};
if (authenticated) {
const { name } = props.auth.getProfile();
return (
<div>
<h1>Howdy! Glad to see you back, {name}.</h1>
<button onClick={logout}>Log out</button>
</div>
);
}
return (
<div>
<h1>I don't know you. Please, log in.</h1>
<button onClick={props.auth.login}>Log in</button>
</div>
);
}
function App(props) {
const authenticated = props.auth.isAuthenticated();
return (
<div className="App">
<Route
exact
path="/callback"
render={() => <Callback auth={props.auth} />}
/>
<Route
exact
path="/"
render={() => (
<HomePage
authenticated={authenticated}
auth={props.auth}
history={props.history}
/>
)}
/>
</div>
);
}
export default withRouter(App);
In this case, you are actually defining two components inside the same file (just for the sake of simplicity). You are defining a HomePage
component that shows a message with the name of the logged-in user (that is, when the user is logged in, of course), and a message telling unauthenticated users to log in.
Also, this file is making the App
component responsible for deciding what component it must render. If the user is requesting the home page (i.e., the /
route), the HomePage
component is shown. If the user is requesting the callback page (i.e., /callback
), then the Callback
component is shown.
Note that you are using the Auth
service in all your components (App
, HomePage
, and Callback
) and also inside the Auth
service. As such, you need to have a global instance for this service, and you have to include it in your App
component.
So, to create this global Auth
instance and to wrap things up, you will need to update your index.js
file as shown here:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import Auth from './Auth/Auth';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
const auth = new Auth();
ReactDOM.render(
<BrowserRouter>
<App auth={auth} />
</BrowserRouter>,
document.getElementById('root'),
);
registerServiceWorker();
After that, you are done! You just finished securing your React application with Auth0. If you take your app for a spin now (npm start
), you will be able to authenticate yourself with the help of Auth0, and you will be able to see your React app show your name (that is, if your identity provider does provide a name).
If you are interested in learning more, please, refer to the official React Quick Start guide to see, step by step, how to properly secure a React application. Besides the steps shown in this section, the guide also shows:
Conclusion
Great programmers constantly sharpen their tools daily to increase productivity. CRA is a great tool for quickly building React Applications. In addition, having your own customized fork of react-scripts
helps you and your team easily add all the configurations you need. You'll need to maintain your fork, and make sure it is synced with the upstream to have all updates. Backstroke is a bot that can help you with this.
Have a very productive time hacking away!