TL;DR: MobX is one of the popular state management libraries out there. It is frequently used with JavaScript frameworks such as React and VueJs. Auth0, on the other hand, is an identity management solution for both web and mobile apps. In this article, you will learn how to build and secure MobX apps with Auth0. The fully functional app can be found on this GitHub Repository.
Prerequisites
You are expected to have prior knowledge of building web apps with React and state management with MobX. If you still need to learn a thing or two about that, here is a good resource to get you started. Asides that, you need Node.js and NPM installed on your machine. If you don't have that, follow the links to set it up on your machine.
Introduction
State management is one of the major talking points of software applications, and React apps are not exempt. It is one of the many functionalities that come packed with React by default. Little wonder React is even referred to as a state management library by some. Because of how important state management is, a lot of solutions have sprung up in an attempt to make it easier. MobX is one of three popular options out there to help manage your state in React apps. Other options include React Context API and Redux.
MobX uses a more reactive approach to state management. Some of the core concepts of MobX includes the following:
- Observable state: With this concept, you can make an object emit new changes when they are updated. They are usually represented with the
@observable
decorator. - Observers: The observers are notified of changes made on the observable state. And so the observable state and observers work hand in hand.
- Computed values: Computed values are used to derive values from an observable state. Let's say you have a function that works with an object marked as
@observable
. Anytime the object changes, the function will be automatically computed and the value derived. Computed values are usually represented with the@computed
decorator. - Actions: These are functions that modify state. Typically, you will use actions for functions that modify observables. There are represented with the
@action
decorator. - Reactions: MobX reactions are similar to computed values. But instead of producing a new value, a reaction simply triggers a side effect (side operation). There are three types of reaction functions -
when
,autorun
, andreaction
.
MobX is not just a React library; it is also compatible with other JavaScript libraries and frameworks that power the frontend of web apps. If you are a little short in the knowledge of MobX, you can make do with this resource and the official docs.
"MobX uses a more reactive approach to state management."
Tweet This
What You Will Build
In this tutorial, you will build a shopping cart. This is a common functionality you see in e-commerce apps where a user can add and subtract items to and from a cart. Implementing a cart involves keeping track of products a user wishes to buy, their respective quantities and prices. You will use MobX to store and manage the cart data in the app. The app will also need users to log in to access the shopping cart, and that's where Auth0 will come in. You will use Auth0 to handle authentication in the app. You can find the entire code used in this article on this repo.
Scaffolding Your React App
One of the fastest ways to bootstrap a React app is via the create-react-app package. You will use this to bootstrap your project. If you are using an old version of create-react-app
you would have to update it by following the instructions here.
Open a terminal window, move into a directory of your choice and run this command:
npx create-react-app auth0-mobx-app
This will generate a new project named auth0-mobx-app in the directory where you ran the command. This process will take a few minutes.
"One of the fastest ways to bootstrap a React app is via the create-react-app package."
Tweet This
Now that you have generated your project, the ideal thing to do next is to install dependencies that you need in the course of developing your app. If you are not in the directory of your MobX app yet, use this command to move into the directory:
cd auth0-mobx-app
After that, go ahead and install the dependencies by running this command:
npm install mobx mobx-react react-router react-router-dom @auth0/auth0-spa-js
This command will install the following dependencies:
mobx
: This is the main MobX library that powers state management.mobx-react
: This library contains React specific functions available through MobX.react-router-dom
andreact-router
: These libraries will be used to add page navigation to the app.auth0-spa-js
: This is the Auth0 client-side library.
Setting Up MobX
MobX uses decorators to handle its state management and React doesn't come with support for decorators by default. So you will install a babel plugin @babel/plugin-proposal-decorators
. Still, on your terminal, run the following commands:
npm install @babel/plugin-proposal-decorators
After installation, you have to eject the React app to be able to configure the babel plugin you just installed. To eject the React app, run the following command on your terminal. Ensure that you commit your code to git before running the command below:
npm run eject
This command ejects some scripts and configs initially inside the node_modules
to the project folder so you get a project structure as shown below:
After that, add the configuration for the babel plugin you just installed. Create a .babelrc
file in the project directory and add the following configuration to the file as shown below:
// .babelrc
{
"presets": ["react-app"],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}
To avoid conflict with the babel plugin configurations, you need to delete the existing babel configuration in package.json
. Open the package.json
file and remove this snippet from the file:
"babel": {
"presets": [
"react-app"
]
}
Now, you need to create a store for your application. Stores are usually compulsory in MobX apps. One of the main points of having them is that they help you move logic and state out of your components into a standalone testable unit that can be used in both frontend and backend JavaScript. This is usually a good idea if you want to write maintainable and testable apps.
Go ahead and create a Store.js
file in the src
folder. After creating the file, paste this snippet inside it:
// src/Store.js
class Store {
products = [
{
id: 1,
name: 'Tshirt sleeker',
description: 'Wonderfully fitted',
price: 50,
image: require('./img/image-1.jpg')
},
{
id: 2,
name: 'Tshirt sleeker',
description: 'Wonderfully fitted',
price: 350,
image: require('./img/image-2.jpg')
},
{
id: 3,
name: 'Tshirt sleeker',
description: 'Wonderfully fitted',
price: 250,
image: require('./img/image-3.jpg')
}
];
carts = [];
currentCart = [];
removeFromCart(id) {
this.carts = this.carts.filter(item => {
return item.product_id !== id;
});
this.getCart();
}
increaseQuantityInCart(id) {
this.carts.map(item => {
if (item.product_id === id) item.quantity += 1;
return item;
});
this.getCart();
}
decreaseQuantityInCart(id) {
this.carts.map(item => {
if (item.product_id === id && item.quantity > 1) item.quantity -= 1;
return item;
});
this.getCart();
}
addToCart(id) {
let found = false;
this.carts.map(item => {
if (item.product_id === id) {
item.quantity += 1;
found = true;
}
return item;
});
if (!found) {
this.carts.push({ product_id: id, quantity: 1 });
}
this.getCart();
}
getCart() {
let carts = this.carts;
carts.map(item => {
for (let i in this.products) {
if (item.product_id === this.products[i].id) {
item.image = this.products[i].image;
item.name = this.products[i].name;
item.description = this.products[i].description;
item.price = this.products[i].price * item.quantity;
}
}
return item;
});
this.currentCart = carts;
}
}
export default Store;
From the snippet above, you created a store that holds the state of the app. Here is a breakdown of what each state object does:
products
- This is an array that will store all the available products a user can add to the cart. Here, there are three hardcoded items in
products
.
- This is an array that will store all the available products a user can add to the cart. Here, there are three hardcoded items in
carts
- This is an array that will store items added to the cart by the user.
currentCart
- This is also an empty array and it is similar to the
carts
array. But this holds the initial state of the cart when the user enters the system or logs on to the system.
- This is also an empty array and it is similar to the
removeFromCart
- This function will be used to remove an item from the
carts
array.
- This function will be used to remove an item from the
increaseQuantityInCart
- This function will be used to increase the quantity of a particular item in the
carts
array.
- This function will be used to increase the quantity of a particular item in the
decreaseQuantityInCart
- This function will be used to decrease the quantity of a particular item in the
carts
array.
- This function will be used to decrease the quantity of a particular item in the
addToCart
- This function will be used to add a new item to the
carts
array.
- This function will be used to add a new item to the
getCart
- This function returns all the data in the
carts
array.
- This function returns all the data in the
Creating Your Components
In this section, you will build the various components for your app. Before you start building, you need to add Bootstrap and Font Awesome CDN links. You will use them both to style your app.
Open the public/index.html
file and add these links in the <head>
tag:
<!-- public/index.html -->
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
/>
<link
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
rel="stylesheet"
/>
Next, open the App.css
file and replace its content with this snippet:
/* src/App.css */
.formSection {
margin-top: 30px;
}
.formSection p {
font-weight: bold;
font-size: 20px;
}
.dashboardSection {
margin-top: 50px;
}
.reviewsWrapper {
margin-top: 50px;
}
.top-space {
margin-top: 30px;
}
.price {
padding: 7.5px;
border: 1.2px solid #ffc107;
color: #807e7b;
width: 100px;
font-size: 14px;
font-weight: bold;
padding-left: 15px;
padding-right: 15px;
border-radius: 3px;
}
.detail {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.product-cart-name {
font-size: 12px;
}
.control > .btn {
padding: 0px 5px;
font-size: 11px;
margin-right: 5px;
height: 20px;
}
.quantity {
font-size: 14px;
}
.cart {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 10px;
}
.cancel {
padding: 0px 3px;
font-size: 10px;
line-height: 10px;
height: 20px;
}
.login {
background-color: #353535;
padding: 40px;
margin-top: 100px;
text-align: center;
height: 350px;
display: flex;
align-items: center;
justify-content: center;
}
The above snippet is the custom styling applied throughout the app to make it look decent.
As mentioned earlier, this application is geared towards building a shopping cart, and so you need some mock data to play with. Here, you need to add some images to the project. Create a folder called img
in the src
folder and add three images, namely: image-1.jpg
, image-2.jpg
, and image-3.jpg
. Better still, you can use the images in the repository.
After setting up the app CSS style and adding images to your project, you will now proceed to build your components.
The Cart component
Here, you will create the Cart component. This component will be responsible for displaying data in the cart state variable, which you defined in the store. First, create a components
folder in the src
folder. Then create a new Cart.js
file in the src/components
directory and add the following snippet to it:
// src/components/Cart.js
import React, { Component } from 'react';
import { observer, inject } from 'mobx-react';
@inject('store')
@observer
class Cart extends Component {
render() {
return (
<div className='card'>
{this.props.store.currentCart.map((data, index) => (
<div key={index} className='cart'>
<button
onClick={() => this.props.store.removeFromCart(data.product_id)}
className='btn btn-default btn-xs cancel'
>
X
</button>
<img height={30} src={data.image} alt='Product stuff' />
<div className='product-cart-name'>{data.name}</div>
<div className='control'>
<button
onClick={() =>
this.props.store.increaseQuantityInCart(data.product_id)
}
className='btn btn-default btn-xs'
>
+
</button>
<button
onClick={() =>
this.props.store.decreaseQuantityInCart(data.product_id)
}
className='btn btn-default btn-xs'
>
-
</button>
</div>
<div className='quantity'>{data.quantity}</div>
<div className='quantity'>$ {data.price}</div>
</div>
))}
</div>
);
}
}
export default observer(Cart);
In this snippet, you coded the Cart component that will display items a user adds to the cart. For each item in the cart, you will display the image, name, description, price, and quantity. These items can be increased, reduced, or removed entirely by calling specific actions defined in the store.
The Product component
Next up, you will build the Product component. The Product component will be responsible for displaying all the items available for the user to add to cart and purchase. Typically, this will be populated by your backend API. But for the sake of this tutorial, they have been hardcoded for you in the store.
Create a new file Product.js
in the src/components
directory and add the following snippet to it:
// src/components/Product.js
import React, { Component } from 'react';
import { observer, inject } from 'mobx-react';
import Cart from './Cart';
@inject('store')
class Product extends Component {
addToCart(id) {
this.props.store.addToCart(id);
}
list(data, index) {
return (
<div key={index} className='col-md-4 top-space'>
<div className='card'>
<img
className='card-img-top'
height={200}
src={data.image}
alt='Product stuff'
/>
<div className='card-body'>
<h4 className='card-title'>{data.name}</h4>
<p className='card-text'>{data.description}</p>
<div className='detail'>
<div className='price text-center'>$ {data.price}</div>
<button
onClick={() => this.addToCart(data.id)}
className='btn btn-primary btn-sm'
>
Add to Cart
</button>
</div>
</div>
</div>
</div>
);
}
render() {
return (
<div className='row'>
<div className='col-md-8'>
<div className='row'>
{this.props.store.products.map((data, index) =>
this.list(data, index)
)}
</div>
</div>
<div className='col-md-4'>
<div className='top-space'>
<Cart />
</div>
</div>
</div>
);
}
}
export default observer(Product);
Here, you have the Product component that displays all the products available in the store. The Product component is wrapped with a higher-order component (i.e., observer) which automatically subscribes the component to an observable(in this case, products
) so that this component only re-renders when there is a change in the products
array.
You also imported and rendered the Cart component defined above to give the user a visual representation of the items in the cart when the user adds or removes an item.
Next, open the src/index.js
file and replace the default code with this snippet:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
The difference between this snippet and the default one is that now you're wrapping your entire component tree with the BrowserRouter
imported from the react-router-dom
package.
Next, replace the code in your src/App.js
file with this snippet:
// src/App.js
import React, { Component } from 'react';
import Product from './components/Product';
import { Provider } from 'mobx-react';
import { withRouter } from 'react-router';
import { Route } from 'react-router-dom';
import Store from './Store';
import { decorate, observable, action } from 'mobx';
import './App.css';
decorate(Store, {
products: observable,
addToCart: action,
increaseQuantityInCart: action,
decreaseQuantityInCart: action,
removeFromCart: action,
currentCart: observable
});
const shoppingStore = new Store();
class App extends Component {
render() {
return (
<Provider store={shoppingStore}>
<div className='container'>
<Route exact path='/' render={() => <Product />} />
</div>
</Provider>
);
}
}
export default withRouter(App);
In this snippet, you imported the Store
defined earlier. The decorate utility designates the products
and currentCart
as the entity that can be observed by components for changes. While the addToCart
, increaseQuantityInCart
, decreaseQuantityInCart
, and removeFromCart
functions are used to update the store by manipulating the cart.
Finally, you set the Product component to be the home(/
) route of the application.
At this point, you can test your app by running this command in the project terminal:
npm run start
You should have something like this:
Securing Your App With Auth0
Auth0 is a modern identity platform for rapidly integrating authentication and authorization for web, mobile, and legacy applications so you can focus on your core business.
To secure the app with Auth0, you need an Auth0 account. Create an account here or login to your account. When you get to your dashboard, click on the orange Create Application button.
You will be presented with a dialog like this:
Enter a suitable name for your app and select Single Page Web Applications as the type. After that, click the Create button.
When your application has been created, you will see a page like this once you click the "Settings" tab:
Go to the Settings tab, scroll to where you have Allowed Callback URLs, add this URL http://localhost:3000/callback
and click Save Changes at the bottom of the page.
Copy out your ClientID and Domain as you will need them soon.
Now that you have set up your Auth0 app, you will go ahead with the Auth0 implementation in your code. You will begin by adding some state objects in the store. Open the Store.js
file and add these:
// src/Store.js
class Store {
// ... Leave the other objects and functions untouched
loading = true;
auth0 = null;
authenticated = null;
setLoader(loading) {
this.loading = loading;
}
setAuth(token) {
this.authenticated = token;
}
initialize(auth0) {
this.auth0 = auth0;
}
}
Here is what the objects and functions you just added do:
loading
- This is used to control loaders which indicates whether a process has completed or not.
auth0
- This is the variable that will hold the instance of the initialized Auth0 client.
authenticated
- This is the variable that will hold the token generated after authenticating a user with
auth0
.
- This is the variable that will hold the token generated after authenticating a user with
setLoader
- This function will be used to alter the state of the loading variable.
setAuth
- This function will be used to set the token generated for a user after authentication with
auth0
.
- This function will be used to set the token generated for a user after authentication with
initialize
- This function will be used to update the initialized Auth0 client.
Next, create an Auth
folder in the src/components
directory. So, you'll have a directory like this src/components/Auth
, create an Auth.js
file there and paste this snippet:
// src/components/Auth/Auth.js
import createAuth0Client from '@auth0/auth0-spa-js';
import {Component} from 'react';
import {inject } from 'mobx-react';
@inject('store')
class Auth extends Component{
async componentWillMount(){
let auth0 = await createAuth0Client({
domain: 'AUTH0_DOMAIN',
client_id: 'AUTH0_CLIENT_ID',
redirect_uri: 'http://localhost:3000/callback',
responseType: 'token id_token',
scope: 'openid profile'
});
this.props.store.initialize(auth0);
this.props.store.setLoader(false);
}
render(){
return(null);
}
}
export default Auth;
This class is responsible for bootstrapping Auth0 in the app. It initializes the auth0
client with the credentials provided and updates it on the store.
Remember to replace the placeholders with your Auth0 credentials from your dashboard. The Auth component will handle all of the logic concerned with authenticating the user.
Building the Login component
Here, you will build a Login component. This component will be responsible for logging the user in. A user won't be allowed to view the Products component unless logged in.
Now create a new Login.js
file in the src/components/Auth
folder and paste this snippet in it:
// src/component/Auth/Login.js
import React, { Component } from 'react';
import { observer, inject } from 'mobx-react';
@inject('store')
@observer
class Login extends Component {
render() {
return (
<div className='row'>
<div className='col-md-4 offset-md-4'>
<div className='login'>
<button disabled={this.props.store.loading} className='btn btn-primary' onClick={()=> this.props.store.auth0.loginWithRedirect()}>
{this.props.store.loading ? <i className="fa fa-gear fa-spin"/>: null} Login
</button>
</div>
</div>
</div>
);
}
}
export default Login;
The Login component only displays a login button. Attached to the button is an onClick
listener that calls the loginWithRedirect()
method from the auth0
client. The login button is disabled and shows a loading icon until auth0
client initialization is complete.
Earlier in your Auth0 dashboard, you set a callback URL as http://localhost:3000/callback
. So, now you have to create a component that matches the /callback
path.
Create a new folder callback
in the src
folder. Then create a file Callback.js
in the src/callback
directory and paste this snippet:
// src/callback/Callback.js
import React, {Component} from 'react';
import { withRouter } from 'react-router';
import { observer, inject } from 'mobx-react';
@inject('store')
@observer
class Callback extends Component{
async componentWillUpdate(nextProps){
await this.props.store.auth0.handleRedirectCallback();
let token = await this.props.store.auth0.getTokenSilently();
this.props.store.setAuth(token);
this.props.history.push('/');
}
render(){
return
<div>Loading user profile. {this.props.store.loading ?
<i style={{color: 'black'}} className="fa fa-gear fa-spin"></i>
: null}
</div>;
}
}
export default withRouter(Callback);
In this snippet, you are using the loading
object from the store to track when the auth0
client has been initialized because of its asynchronous nature. Once the value of theloading
object changes (signaling the completion of the auth0
client initialization), the life cycle method componentWillUpdate
runs. This function handles the callback redirection, stores the token in the store, and redirects the user to the products page.
Now, open your src/App.js
file. First, add these to the import section:
// src/App.js
import Login from './components/Auth/Login';
import Auth from './components/Auth/Auth';
import Callback from './callback/Callback';
Here, you imported the Login, Auth, and Callback components.
In your App.js
file, add the loading
object as an observable like so:
// src/App.js
decorate(Store, {
// ... Other observables and actions
loading: observable
});
Then replace the main App
component with this:
// src/App.js
class App extends Component {
render() {
return (
<Provider store={shoppingStore}>
<Auth />
<div className='container'>
<Route
exact
path='/callback'
render={() => <Callback auth={this.props.auth} />}
/>
<Route
exact
path='/'
render={() => (
<Product
history={this.props.history}
/>
)}
/>
<Route
exact
path='/login'
render={() => <Login auth={this.props.auth} />}
/>
</div>
</Provider>
);
}
}
In this snippet, you rendered these components as routes passing in the auth
and history
prop where necessary. You made an update to the Product component by also passing in the auth
prop. Remember, the goal is to render the Product component only when the user is authenticated.
The Product component is not yet aware of the new prop you passed to it. To fix this open src/components/Product.js
and add this snippet just before the addToCart()
method:
// src/components/Product.js
componentWillMount() {
if (!this.props.store.authenticated) {
this.props.history.push("/login")
}
}
This snippet tells the Product component to redirect to the Login component if the user is not logged in.
Testing Your App
Now, run this command again to test your app:
npm run start
Then, this should open a browser tab with the URL - http://localhost:3000
. If you did everything correctly, you should be redirected to the Login component and prompted to log in:
When the login button is clicked, you'll be redirected to the Auth0 authentication page, which requests you to either login or sign up.
After a successful login, you'll be redirected to the products page.
Conclusion
In this article, you learned how to build a shopping cart with MobX for state management and securing the application with Auth0. MobX and Auth0 are very powerful and have a lot of features not covered in this article.
About Auth0
Auth0 by Okta takes a modern approach to customer identity and enables organizations to provide secure access to any application, for any user. Auth0 is a highly customizable platform that is as simple as development teams want, and as flexible as they need. Safeguarding billions of login transactions each month, Auth0 delivers convenience, privacy, and security so customers can focus on innovation. For more information, visit https://auth0.com.