Beyond Create React App: React Router, Redux Saga, and More

March 27, 2019 0 Comments

Beyond Create React App: React Router, Redux Saga, and More

 

 

Today, you will learn how to scaffold a React Single-Page Application (SPA) with some sensible and opinionated defaults. Probably, you heard of create-react-app before and, probably, you are even using this tool to scaffold your React apps. So, you may be wondering: "Why would I need to read this article and how does it help me?"

The answer to this question is simple: create-react-app is a great starting point but doesn't prepare the field to a real-world, production-ready SPA. As such, the goal of this article is to help you go through the steps needed to scaffold a React SPA that:


  • has a consistent code style (with the help of Prettier, a JavaScript formatter);

  • installs and configures some popular React libraries (like Redux and Redux Saga);

  • uses styled-components to help you manage the CSS of your React components;

  • configures React Bootstrap to give you a responsive, mobile-first application;

  • and that uses Auth0 to handle authentication easily.

After going through the steps here, you will have a SPA that:


  • is easy to extend;

  • multiple developers can contribute to (without ending with a spaghetti code);

  • has a nice user interface (based on one of the most popular React UI libraries out there);

  • can handle state and asynchronous tasks (like timeouts and AJAX requests) easily.

  • and that is secure.

If that sounds interesting, keep reading!

"Learn how to integrate Redux Saga, React Bootstrap, React Router, and more in your next React app."


Prerequisites

To follow along with this article, you will need Node.js and NPM installed in your machine. If you don't have these tools, please, check this resource before continuing. On most installations, Node.js ships with NPM.

Besides that, you will have to have some basic knowledge of React. You don't need years of experience with it to follow the article along with, but you do have to understand its basic principles (components, JSX, etc.). If you are completely new to React, please, read this article first. After reading it, you will be able to read this one without struggling.

What You Will Build

In this article, you will build a very simple to-do list manager application. Your app will consume a to-do list from an external server and will allow users to add new to-do items to the local list. However, the app will not update this external server with the new items that users insert. The goal of this article is not to build the application, but to teach you how to put everything together so you can build awesome React apps that rely on a mature architecture.

Scaffolding the React SPA

The first thing you will do to scaffold your new React SPA is to use create-react-app. This tool is incredible and, just by issuing one command, you can put together a React app. So, open a terminal, move into the directory where you usually save your projects, and issue the following command:

npx create-react-app react-todo

This command will make NPM (or Yarn if you have that available) download the latest version of create-react-app and execute it to create your project under a new directory called react-todo. After running this command, move into the new directory (cd react-todo), and run npm start to see your new app. If everything works as expected, NPM will open http://localhost:3000 for you in your default browser.

Note: Throughout this article, you will see npm commands everywhere. However, if you have Yarn installed in your machine, create-react-app will use it instead of NPM to scaffold your application. If that is the case, you will have to translate npm commands to yarn (e.g., instead of running npm start, you will need to execute yarn start), or you will have to remove the yarn-lock file and issue npm install to create the package-lock.json file for you. Feel free to choose whatever suits you better.

Scaffolding your new React single-page application.

Installing and Configuring Prettier

All software developers have their own preferences when it comes to code style. Some prefer using semicolons, and some prefer leaving them out. Some prefer indenting code with tabs, and some prefer using two spaces. However, what is really important is that they don't mix these different styles on a single code base.

To easily accomplish that, you will use Prettier. Prettier, as explained by their website is an opinionated code formatter that you can use to help you keep the code style of your project consistent. If you configure Prettier properly, any software developer will be able to jump right into your project and start coding without worrying about code format. Then, when they save their modifications (or when they commit them), Prettier will make sure the code is formatted correctly. Sounds cool, right?

So, to use this tool, the first thing you will have to do is to stop the development server (by hitting Ctrl + C on the terminal). Then, you will have to issue the following command:

npm install husky lint-staged prettier

This will make NPM install three libraries:


  • husky and lint-staged: Together, these libraries will allow you to register an NPM script as a githook (this way Prettier will run right before the developer commits a new code).

  • prettier: This is the JavaScript formatter you want to use.

After installing these libraries, add the following properties to the package.json file:

// ./package.json "husky": { "hooks": { "pre-commit": "lint-staged" }
},
"lint-staged": { "src/*/.{js,jsx,ts,tsx,json,css,scss,md}": [ "prettier --single-quote --write", "git add" ]
},
"prettier": { "singleQuote": true
}

The first property, husky, will make lint-staged run on Git's pre-commit phase. The second property, lint-staged, indicates what exactly NPM must run on this phase. The third property, prettier, changes the default configuration of Prettier to use singleQuote instead of double quotes.

With that in place, you might also be interested in integrating Prettier into your IDE. For that, you can check this resource. There, you will find that the community has built Prettier plugins for the most popular text editors and IDEs out there (e.g., WebStorm and VSCode)

For example, if you are using WebStorm, you will have to install this plugin. Then, after installing it, you can use the Reformat with Prettier action (Alt + Shift + Cmd + P on macOS or Alt + Shift + Ctrl + P on Windows and Linux) to format the selected code, a file, or a whole directory. Also, you might be interested in adding a WebStorm File Watcher to executes the Reformat with Prettier action on file modifications. If you are interested in this, please, check this resource.

Installing and Configuring React Bootstrap

After configuring Prettier into your project, the next thing you can do is to install and configure React Bootstrap. This library is a specialization of the Bootstrap toolkit. As Bootstrap depends on jQuery to run some components, the React community decided that it would be a good idea to remove this dependency and rebuild Bootstrap to integrate with React tightly. That's how React Bootstrap was born.

So, to install this library, issue the following command from the terminal:

npm install react-bootstrap bootstrap

After that, open the ./public/index.html file and, right after the title DOM element, import Bootstrap's CSS file:

<!-- ./public/index.html --> <!DOCTYPE html>
<html lang="en"> <head> <!-- ... title and other elements ... --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous" /> </head> <!-- ... body ... -->
</html>

As explained on the official React Bootstrap documentation:

This library doesn't depend on a specific version of Bootstrap. As such, the library doesn't ship with any CSS file on it. However, some stylesheet is required to use these components. How and which Bootstrap styles you include is up to you, but the simplest way is to include the latest styles from the CDN. - React Bootstrap Introduction

Now, to check if the configuration is working, open the ./src/App.js file and replace its code with this:

// ./src/App.js import React, { Component } from 'react';
import Container from 'react-bootstrap/Container';
import Button from 'react-bootstrap/Button';
import Col from 'react-bootstrap/Col';
import Row from 'react-bootstrap/Row'; class App extends Component { render() { return ( <Container> <Row className="row"> <Col xs={12}> <h1>My New React Bootstrap SPA</h1> <Button>Look, I'm a button!</Button> </Col> </Row> </Container> ); }
} export default App;

Then, back in your terminal, issue the following command to open your app in a web browser:

npm start

If everything works as expected, you will be able to see a page with a header (h1) and a Button that use Bootstrap's CSS rules.

Installing and configuring React Bootstrap

Note: You are not using two files anymore: ./src/App.css and ./src/logo.svg. As such, feel free to delete them.

Installing PropTypes

As a React developer you probably already know what PropTypes is, but if you don't, here is the definition from React's documentation:

PropTypes exports a range of validators that can be used to make sure the data you (your React components) receive is valid. - Typechecking With PropTypes

That is, PropTypes allows you to add some type checking capabilities to your project with ease. For example, if you have a component that outputs a (required) message, you can add type checking with PropTypes like so:

import React from 'react';
import PropTypes from 'prop-types'; const Header = ({ description }) => <h1>{description}</h1>; Header.propTypes = { description: PropTypes.string.isRequired
};

After that, whenever you use the Header component without passing a description to it, PropTypes will show a warning message in the JavaScript console. Note that this tool is there to help you in the development process. Also, for performance reasons, React only checks PropTypes in development mode.

So, before proceeding to more advanced topics, stop the development server (Ctrl + C) and issue the following command to install PropTypes:

npm install --save prop-types

Installing Redux and Integrating It with React

Next, you will install and integrate Redux in your React app. Redux, for those who don't know, is the most popular state management library among React developers. Redux itself is not tied to React but, most of the time, developers use them together.

If you don't know how Redux works, don't worry, you can still follow along with this article. In the end, you can read this practical tutorial on Redux to learn more about this library.

To integrate Redux with your React app, you will have to install two libraries:

npm install --save redux react-redux

The first one, redux, is Redux itself and the second one, react-redux, is a library that offers React bindings for Redux.

As you will build a simple to-do application in this article, the next thing you can do is to define the Redux actions that your app will handle. To do this, create a new directory called actions inside the src directory, then create a file called index.js inside it with the following code:

// ./actions/index.js export const ADDTODO = 'ADDTODO'; export function addToDo(title) { return { type: ADDTODO, toDoItem: { _id: (new Date().getTime()).toString(), title } };
}

Here you are defining that, for now, your app will handle a single type of action: ADDTODO. Actions of this type, as you can see, will carry a toDoItem object with two properties: an id and a title.

After defining the action type your app will handle, you can create a Redux reducer to process actions. In this case, you will create a directory called reducers (again inside the src directory), and you will create a file called index.js inside it with this code:

// ./reducers/index.js import { ADDTODO } from '../actions'; const initialState = { toDoList: []
}; export default function toDoApp(state = initialState, action) { switch (action.type) { case ADDTODO: let newToDoList = [ ...state.toDoList, { ...action.toDoItem } ]; return { ...state, toDoList: newToDoList }; default: return state; }
}

This file has two goals. The first one is to define the initialState state of your app (which is an empty toDoList). The second one is to define what the toDoApp will do when it receives an ADD_TODO action (which is to include the new to-do item to the toDoList).

With that in place, you can open the ./src/index.js file and replace its contents with this:

// ./src/index.js import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import toDoApp from './reducers';
import App from './App'; const store = createStore(toDoApp); render( <Provider store={store}> <App /> </Provider>, document.getElementById('root')
);

The new version of this file is using the createStore function (provided by redux) to create a single source of truth object about the state of the app (i.e., the store constant). Then, it uses this store to feed the app with state.

Integrating React Components with Redux

After defining these Redux elements (actions, reducers, and the store), the next thing you can do is to define the React components that will use these elements. First, you will create two new directories:


  • ./src/components: This is where you will create your Presentational Components (that is, components that are not aware of Redux).

  • ./src/containers: This is where you will create Container Components (that is, components that tightly integrate to Redux).

After that, you can create a file called AddToDo.js inside the ./src/containers directory with the following code:

// ./src/containers/AddToDo.js import React from 'react';
import { connect } from 'react-redux';
import { addToDo } from '../actions';
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
import InputGroup from 'react-bootstrap/InputGroup'; let AddToDo = ({ dispatch }) => { let input; return ( <Form onSubmit={e => { e.preventDefault(); if (!input.value.trim()) { return; } dispatch(addToDo(input.value)); input.value = ''; }} > <Form.Group controlId="formBasicEmail"> <InputGroup> <Form.Control type="text" placeholder="Enter an item" ref={node => { input = node; }} /> <InputGroup.Append> <Button type="submit">Add To-Do</Button> </InputGroup.Append> </InputGroup> </Form.Group> </Form> );
};
AddToDo = connect()(AddToDo); export default AddToDo;

This component will present a form to your users and will allow them to input (and submit) new to-do items. As you can see, when your users submit this form, the component will dispatch an action that the addToDo function creates. This is enough to feed your app with to-do items but is not enough to present the items to your users.

To be able to present the to-do items, you will create two Presentation Components: ToDo and ToDoList. As their names state, the first one will render a single to-do item, while the second one will render a list of to-do items.

To define the ToDo component, create a new file called ToDo.js inside ./src/components and add the following code to it:

// ./src/components/ToDo.js import React from 'react';
import PropTypes from 'prop-types';
import ListGroup from 'react-bootstrap/ListGroup'; const ToDo = ({ title }) => <ListGroup.Item>{title}</ListGroup.Item>; ToDo.propTypes = { title: PropTypes.string.isRequired
}; export default ToDo;

This component will receive the description of the to-do item in question and will use the ListGroup.Item Bootstrap component to render the item.

Now, to define the ToDoList component, create a new file called ToDoList.js inside ./src/components and add the following code to it:

// ./src/components/ToDoList.js import React from 'react';
import PropTypes from 'prop-types';
import ListGroup from 'react-bootstrap/ListGroup';
import Jumbotron from 'react-bootstrap/Jumbotron';
import ToDo from './ToDo'; const ToDoList = ({ toDoList }) => ( <Jumbotron> <ListGroup> {toDoList.map((toDo, index) => ( <ToDo key={index} {...toDo} /> ))} </ListGroup> </Jumbotron>
); ToDoList.propTypes = { toDoList: PropTypes.arrayOf( PropTypes.shape({ _id: PropTypes.string.isRequired, title: PropTypes.string.isRequired }).isRequired ).isRequired
}; export default ToDoList;

This component will receive the toDoList, iterate over it, and render (inside a Jumbotron Bootstrap component) a list of ToDo components.

After defining these two presentational components, you will have to map the state of the app to the props of the ToDoList component. To do this, create a file called ToDoListContainer.js inside the ./src/containers directory and add the following code to it:

// ./src/containers/ToDoListContainer.js import { connect } from 'react-redux';
import ToDoList from '../components/ToDoList'; const mapStateToProps = state => { return { toDoList: state.toDoList };
}; const ToDoListContainer = connect(mapStateToProps)(ToDoList); export default ToDoListContainer;

This will make sure Redux maps the toDoList object available on its store (the state object on the source code above) to the toDoList property that the ToDoList presentational component uses.

With that in place, the last thing you will have to do is to make the App component use both Redux containers. So, open the ./src/App.js file and replace its code with this:

// ./src/App.js import React, { Component } from 'react';
import Container from 'react-bootstrap/Container';
import Col from 'react-bootstrap/Col';
import Row from 'react-bootstrap/Row'; import AddToDo from './containers/AddToDo';
import ToDoListContainer from './containers/ToDoListContainer'; class App extends Component { render() { return ( <Container> <Row className="row"> <Col xs={12}> <h1>To Do List</h1> <AddToDo /> <ToDoListContainer /> </Col> </Row> </Container> ); }
} export default App;

Now, to see Redux in action, rerun npm start from the terminal. If everything works as expected, you will see your new to-do app up and running in your browser in a few seconds. There, you will be able to use the form to insert new to-do items.

React application using Redux as the single source of truth.

Managing Side Effects on React with Redux Saga

Cool, you now have an application that can rely on a single source of truth when it comes to state management. However, one big gap of Redux is that this library does not handle well side effects (like those that AJAX requests provoke). To be able to handle this kind of side effect, you can use Redux Saga.

The goal of this article is not to teach everything about Redux Saga. For that, you can check this tutorial. However, even if you are not acquainted with this tool, you can still follow the instructions here to put the whole thing together. Then, when you finish reading this article, you can dive into this useful (and excellent) topic.

For starters, to install Redux Saga, stop the development server (Ctrl + C), then issue the following command:

npm i redux-saga

This will install the redux-saga dependency in your React project.

After installing it, you can open the ./src/actions/index.js file and update it as follows:

// ./src/actions/index.js // ... ADDTODO ...
export const LOADTODOLIST = 'LOADTODOLIST';
export const RENDERTODOLIST = 'RENDERTODOLIST'; // ... addToDo ... export function loadToDoList() { return { type: LOADTODOLIST };
}

The new version of this file is defining two new action types:


  • LOADTODOLIST: This action type will make your React app load the to-do list from an external server.

  • RENDERTODOLIST: This action type will make your React app render the to-do list it just loaded.

Besides that, you are defining a function called loadToDoList to create an action with the LOADTODO_LIST type. In a few moments, you will make your React app use this function to dispatch an action of this type.

After defining these new action types, you can open the ./src/reducers/index.js file and update it as follows:

// ./src/reducers/index.js // ... other imports ... import { RENDERTODOLIST } from '../actions'; // ... initialState ... export default function toDoApp(state = initialState, action) { switch (action.type) { case RENDERTODOLIST: return { ...state, toDoList: action.toDoList }; // ... case ADDTODO, and default ... : }
}

Here, you are adding a new case statement to the switch command that will handle RENDERTODO_LIST actions. When your Redux receives an action with this type, it will read the toDoList payload and update the state of the app with the new list.

After this change, the next thing you will do is to create your first sagas. To do so, create a directory called sagas inside src, then create a file called index.js inside it with the following code:

// ./src/sagas/index.js import { all, call, put, takeEvery } from 'redux-saga/effects';
import { LOADTODOLIST, RENDERTODOLIST } from '../actions'; export function* fetchToDoList() { const endpoint = 'https://gist.githubusercontent.com/brunokrebs/f1cacbacd53be83940e1e85860b6c65b/raw/to-do-items.json'; const response = yield call(fetch, endpoint); const data = yield response.json(); yield put({ type: RENDERTODOLIST, toDoList: data });
} export function* loadToDoList() { yield takeEvery(LOADTODOLIST, fetchToDoList);
} export default function* rootSaga() { yield all([loadToDoList()]);
}

Note: Sagas are implemented as Generator functions (function*) that yield objects to the redux-saga middleware. The yielded objects are a kind of instruction to be interpreted by the middleware. When a Promise is yielded to the middleware, the middleware will suspend the Saga until the Promise completes. - Redux Saga Beginner Tutorial

Here you can see that you are creating two sagas:


  • fetchToDoList: A saga that issues a request to a backend API (a static JSON file in this case) to fetch a toDoList.

  • loadToDoList: A saga that listens to LOADTODOLIST actions to trigger the fetchToDoList saga.

When the fetchToDoList saga finishes loading the data from the API, it dispatches (through the put function) a RENDERTODOLIST action with the new list of to-do items. Then, the new version of your reducer captures this action and updates the state of the app accordingly.

After creating your sagas, the last thing you will have to do is to make your app dispatch a LOADTODOLIST action right after loading on users' browsers. To achieve this, open the ./src/index.js file and replace its code with this:

// ./src/index.js import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga'; import App from './App';
import { loadToDoList } from './actions';
import toDoApp from './reducers';
import rootSaga from './sagas'; const sagaMiddleware = createSagaMiddleware(); const store = createStore(toDoApp, applyMiddleware(sagaMiddleware)); sagaMiddleware.run(rootSaga); store.dispatch(loadToDoList()); render( <Provider store={store}> <App /> </Provider>, document.getElementById('root')
);

Here, you are creating a sagaMiddleware to add to your app's store and you are making your app use the loadToDoList action creator to dispatch an action.

After this change, you can rerun your development server:

npm start

If you get everything right, you will see that your React app now loads two to-do items from the remote server: one to remind you to "buy pizza" and another one to remind you to "watch Netflix".

Using Redux Saga in a React app to handle side effects.

"Redux Saga makes it really easy to manage side effects caused by process like async HTTP calls."


Handling Multiple Routes With React Router

Right now, your app is capable of:


  • rendering a nice user interface (with the help of React Bootstrap);

  • managing a single source of truth for its state (with the help of Redux);

  • and managing side effects that things like async HTTP requests cause (with the help of Redux Saga).

What you need now is to prepare your app to handle multiple routes. For that, you can use React Router, a declarative routing library for React.

To install this library, stop the development server (Ctrl + C), then run the following command:

npm i react-router-dom

After that, you have to import the BrowserRouter component and nest your App inside it. So, open the ./src/index.js file and update it as follows:

// ./src/index.js // ... other imports ...
import { BrowserRouter } from 'react-router-dom'; // ... saga and redux config ... render( <Provider store={store}> <BrowserRouter> <App /> </BrowserRouter> </Provider>, document.getElementById('root')
);

Then, before adding any routes to your app, you will create a Navigation component that will allow users to navigate between different routes. So, create a file called Navigation.js inside ./src/components and add the following code to it:

// ./src/components/Navigation.js import React from 'react';
import { Link } from 'react-router-dom'; export default () => ( <div> <Link className="btn btn-primary" to="/"> To-Do List </Link> <Link className="btn btn-secondary" to="/new-item"> + Add New </Link> </div>
);

As you can see, this component creates two instances of Link: one that allows users to navigate to your home page (i.e., /) and one that allows them to navigate to a route where they will insert to-do items (/new-item). For demonstration purposes, you will split the form from the to-do list.

Now, what you need to do is to use the Navigation component in your App and define the two routes. For that, open the ./src/App.js file and update it as follows:

// ./src/App.js // ... other imports ...
import { Route } from 'react-router-dom';
import Navigation from './components/Navigation'; class App extends Component { render() { return ( <Container> <Row className="row"> <Col xs={12}> <h1>To Do List</h1> <Navigation /> <Route exact path="/" component={ToDoListContainer} /> <Route exact path="/new-item" component={AddToDo} /> </Col> </Row> </Container> ); }
}
export default App;

Here, you are using the Route component to configure your app to render the ToDoListContainer component when users navigate to your home page, and to render the AddToDo component when they navigate to /new-item.

With that in place, you can rerun your application (npm start), and see the navigation feature working in your browser.

Configuring React Router.

Installing and Using Styled-Components

Your architecture is almost complete now. You have installed and configured some useful libraries that help you handle navigation, state, and the user interface. However, you haven't done anything related to facilitating the enhancement of this user interface. For example, what if you wanted to change the style of your buttons? Or if you want to add some margin between your Navigation component and the list of to-do items? You could, of course, write a simple CSS file and import it in your React app. But this is old school.

Instead, you are going to use style-components, a library that, among other benefits, allows you to adapt the styling of a component based on its props. To install this library, once more you will have to stop the server (Ctrl + C), then you will have to issue the following command:

npm i styled-components

Now, you can import styled-components in a file, and use it to change the style of any component or DOM element. For example, open the ./src/components/Navigation.js and replace its code with this:

// ./src/components/Navigation.js import React from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components'; const NavigationBar = styled.divmargin-bottom: 15px; background-color: lightgray;
; export default () => ( <NavigationBar> <Link className="btn btn-primary" to="/"> To-Do List </Link> <Link className="btn btn-secondary" to="/new-item"> + Add New </Link> </NavigationBar>
);

In the new version of this file, you are using styled-components to create a component called NavigationBar that is a div with some CSS styles. More specifically, you are defining that this div will have 15px of margin on its bottom and that it will have a lightgray background. As you can see, you use the new NavigationBar component just like other components (or DOM elements).

That's it! With that in place, you can rerun your app (npm start), and check the new layout in your browser.

Installing and configuring Styled Components.

Securing Your React Application

Lastly, you will have to think about securing your application. For that, you will use Auth0. You can't go too far without a good identity management system backing you up. You could, of course, build your own solution but, besides having to implement everything from the ground up (user registration, email confirmation, password reset, etc.), you will never have a solution that is as trustworthy as Auth0.

Auth0, a global leader in Identity-as-a-Service (IDaaS), provides thousands of customers in every market sector with the only identity solution they need for their web, mobile, IoT, and internal applications. Its extensible platform seamlessly authenticates and secures more than 2.5 billion logins per month, making it loved by developers and trusted by global enterprises. The company's U.S. headquarters in Bellevue, WA, and additional offices in Buenos Aires, London, Tokyo, and Sydney, support its global customers that are located in 70+ countries.

If you don't have one yet, you will have to create a free Auth0 account now. After creating it, go to the Applications section of your Auth0 dashboard and click on the Create Application button. Then, fill the form as follows:


  • Application Name: "React App"

  • Application Type: "Single Page Web App"

When you click on the Create button, Auth0 will redirect you to the Quick Start tab of your new application. From there, head to the Settings tab and make two changes:


  1. Add http://localhost:3000/callback to the Allowed Callback URLs field.

  2. Add http://localhost:3000/ to the Allowed Logout URLs.

For security reasons, after the login and logout processes, Auth0 will only redirect users to the URLs you register in these two fields.

After updating the configuration, scroll to the bottom of the page, and click Save Changes. For now, leave this page open.

Back in the terminal, stop the development server (Ctrl + C), and issue the following command:

npm i auth0-js

This will install Auth0's headless browser SDK in your app. After installing it, create a new file called Auth.js inside src and add the following code to it:

// ./src/Auth.js import auth0 from 'auth0-js'; const auth0Client = new auth0.WebAuth({ // the following three lines MUST be updated domain: '<YOURAUTH0DOMAIN>', audience: 'https://<YOURAUTH0DOMAIN>/userinfo', clientID: '<YOURAUTH0CLIENTID>', redirectUri: 'http://localhost:3000/callback', responseType: 'idtoken', scope: 'openid profile email'
}); export function handleAuthentication() { return new Promise((resolve, reject) => { auth0Client.parseHash((err, authResult) => { if (err) return reject(err); if (!authResult || !authResult.idToken) { return reject(err); } const idToken = authResult.idToken; const profile = authResult.idTokenPayload; // set the time that the id token will expire at const expiresAt = authResult.idTokenPayload.exp * 1000; resolve({ authenticated: true, idToken, profile, expiresAt }); }); });
} export function signIn() { auth0Client.authorize();
} export function signOut() { auth0Client.logout({ returnTo: 'http://localhost:3000', clientID: '<YOURAUTH0CLIENTID>' });
}

Note: In the code above, you will have to replace <YOURAUTH0DOMAIN> and <YOURAUTH0CLIENTID> (they both appear twice in the code) with the Domain and Client ID properties of your new Auth0 Application. You can get these properties from the page that you left open.

This file creates an auth0Client object with your Auth0 configuration and uses it to expose three functions:


  • handleAuthentication: You will call this function right after Auth0 redirects your users back to your app. At this moment, the function will fetch their idToken and profile (a.k.a., idTokenPayload) and send this information to whatever is listening to the promise that it returns.

  • signIn and signOut: These functions, as their names state, will initiate the login and logout processes.

After defining them, you will create two Redux actions that your app will need to interact with these functions. For that, open the ./src/actions/index.js file and update it as follows:

// ./src/actions/index.js // ... other constants ...
export const USERPROFILELOADED = 'USERPROFILELOADED';
export const HANDLEAUTHENTICATIONCALLBACK = 'HANDLEAUTHENTICATIONCALLBACK'; // ... addToDo and loadToDoList ... export function handleAuthenticationCallback() { return { type: HANDLEAUTHENTICATIONCALLBACK };
}

Next, you will create a component that handles the authentication callback. So, create a file called Callback.js inside src/containers/ and add the following code to it:

// ./src/containers/Callback.js import React from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router';
import { handleAuthenticationCallback } from '../actions'; const mapStateToProps = state => { return { user: state.user };
}; let Callback = ({ dispatch, user }) => { if (user) return <Redirect to="/" />; dispatch(handleAuthenticationCallback()); return <div className="text-center">Loading user profile.</div>;
};
Callback = connect(mapStateToProps)(Callback); export default Callback;

As you can see, when your app renders this component, it will check whether there is a user object available in the store or not (the component connects to Redux). If there is no user, it will use the handleAuthenticationCallback action creator to dispatch the HANDLEAUTHENTICATIONCALLBACK action. If there is a user, it will redirect them to your home page.

At this moment you have the Redux actions, the code (that Auth.js provides), and the Callback component necessary to handle the callback process. What you need now is to open the ./src/sagas/index.js file and update it as follows:

// ./src/sagas/index.js // ... other imports ...
import { takeLatest } from 'redux-saga/effects';
import { HANDLEAUTHENTICATIONCALLBACK, USERPROFILELOADED } from '../actions';
import { handleAuthentication } from '../Auth'; // ... fetchToDoList and loadToDoList ... export function* parseHash() { const user = yield call(handleAuthentication); yield put({ type: USERPROFILELOADED, user });
} export function* handleAuthenticationCallback() { yield takeLatest(HANDLEAUTHENTICATIONCALLBACK, parseHash);
} // replace the current rootSaga generator
export default function* rootSaga() { yield all([loadToDoList(), handleAuthenticationCallback()]);
}

Here you are defining two new sagas. The first one, parseHash, will call and wait for the result of the handleAuthentication function. Then it will put a USERPROFILELOADED action to let Redux know about the user who just signed in. The second one, handleAuthenticationCallback, is there to "listen" to HANDLEAUTHENTICATIONCALLBACK actions so it can trigger the first saga. Lastly, you are updating the rootSaga to make the handleAuthenticationCallback saga run when the app starts.

After creating your new sagas, you can open the ./src/reducers/index.js file and update it as follows:

// ./src/reducers/index.js // ... other imports ...
import { USERPROFILELOADED } from '../actions'; // ... initialState ... export default function toDoApp(state = initialState, action) { switch (action.type) { // ... RENDERTODOLIST and ADDTODO ... case USERPROFILELOADED: return { ...state, user: action.user }; default: return state; }
}

This new version is adding a case statement to handle USERPROFILE_LOADED actions. That is, when your saga informs Redux that the user logged in, the code in this statement will add the user object to your app's state.

These changes would suffice to integrate your app with Auth0. However, you are not consuming the user profile yet. To see the whole thing in action, you will make your Navigation component render information about the logged-in user. So, open the ./src/components/Navigation.js file and update it as follows:

// ./src/components/Navigation.js // ... other imports ...
import { Fragment } from 'react';
import Button from 'react-bootstrap/Button';
import { signIn, signOut } from '../Auth'; // ... NavigationBar ... const Profile = styled.spanmargin-left: 15px;
; const ProfilePicture = styled.imgborder-radius: 50%; max-width: 30px; margin-right: 5px;
; export default ({ user }) => ( <NavigationBar> <Link className="btn btn-primary" to="/"> To-Do List </Link> <Link className="btn btn-secondary" to="/new-item"> + Add New </Link> {!user && <Button onClick={signIn}>Login</Button>} {user && ( <Fragment> <Button onClick={signOut}>Logout</Button> <Profile> <ProfilePicture src={user.profile.picture} /> {user.profile.email} </Profile> </Fragment> )} </NavigationBar>
);

With this change, you are making the navigation bar aware of the state of the user. If there is a logged-in user, then the app will show a logout button, the user profile picture, and their email address. If the user is not logged-in, the app will show a login button.

Before wrapping things up, you still need to feed the new version of your Navigation component with the user. To do this, create a new file called NavigationContainer.js inside ./src/containers and add the following code to it:

// ./src/containers/NavigationContainer.js import { connect } from 'react-redux';
import Navigation from '../components/Navigation'; const mapStateToProps = state => { return { user: state.user };
}; const NavigationContainer = connect(mapStateToProps)(Navigation); export default NavigationContainer;

Lastly, you will need to update the App component to replace Navigation with NavigationContainer and to add a callback route (/callback). So, open the ./src/App.js file and update it as follows:

// ./src/App.js // ... other imports ...
import Callback from './containers/Callback';
import NavigationContainer from './containers/NavigationContainer'; class App extends Component { render() { return ( <Container> <Row className="row"> <Col xs={12}> <h1>To Do List</h1> <NavigationContainer /> <Route exact path="/" component={ToDoListContainer} /> <Route exact path="/new-item" component={AddToDo} /> <Route exact path="/callback" component={Callback} /> </Col> </Row> </Container> ); }
} export default App;

After this last change, if you rerun your application (npm start), you will be able to log in and log out from it with the help of Auth0. Easy, right?

Using Auth0 as the identity provider of your React application

"Scaffolding a React SPA with React Router, Redux, Redux Saga, and React Bootstrap is easy."


Recap

In this article, you learned how to create a robust architecture that will help you scale your next React application. You started by using create-react-app to generate a simple app, then you went through a few different steps to install and configure libraries that will help you:


  • make your code style consistent (Prettier);

  • make your UI look good (React Bootstrap);

  • perform some type checkings (PropTypes);

  • manage the state of the application (Redux);

  • manage side effects (Redux Saga);

  • handle CSS with ease (Styled-Components);

  • and handle authentication (Auth0).

After configuring these libraries, you can rest assured that your next React application will rely on a mature and battle-tested architecture that can grow indefinitely.


Tag cloud