developers

Building Ambitious Apps with MDX

Learn how MDX works and create a blazing fast app in no time.

Oct 25, 201819 min read


TL;DR: MDX is used to load, parse and render JSX in Markdown documents. In this tutorial, you'll learn how to use Markdown with React to build a project documentation app. Check out this GitHub repository if you'd like to dive straight into the code.

MDX combines the readability of Markdown files with the expressiveness of JSX.

Tweet This

What is MDX?

MDX combines the readability of Markdown files with the expressiveness of JSX. It is a format that lets you seamlessly use JSX in your Markdown documents. MDX is fast, has no runtime compilation, provides customizable layouts, easily pluggable to existing projects and can import components, like interactive charts, and export metadata.

What We'll Build

There are several frameworks and projects for setting up documentation websites for open or closed-source software. Instead of setting up any of these projects for our documentation, we'll use MDX to build a documentation app for a fictional JavaScript framework called

JollofJS
. Once the app is feature-complete, we'll secure it with Auth0.

Getting Started

First, create a React app. To make this easy, use the

create-react-app
open-source software by Facebook.

# Install it globally
npm install -g create-react-app

Once you are done installing

create-react-app
on your machine, create a React app like so:

create-react-app jollofjs-doc

Another option is to create a new React app via

npx
:

npx create-react-app jollofjs-doc

Note: Make sure you have node version >= v8.5 installed on your system and

npx
comes with
npm 5.2+
and higher.

Go ahead to change and the new working directory,

jollofjs-doc
:

cd jollofjs-doc

Now, install

react-app-rewired
:

npm install react-app-rewired --save-dev

react-app-rewired
allows you tweak the
create-react-app
webpack config(s) without using
eject
and without creating a fork of the react-scripts.

Create a

config-overrides.js
file in the root directory and add the code below to it:

const { getBabelLoader } = require('react-app-rewired');
module.exports = (config, env) => {
    const babelLoader = getBabelLoader(config.module.rules);
    config.module.rules.map(rule => {
        if (typeof rule.test !== 'undefined' || typeof rule.oneOf === 'undefined') {
            return rule
        }

        rule.oneOf.unshift({
            test: /\.mdx$/,
            use: [
                {
                    loader: babelLoader.loader,
                    options: babelLoader.options
                },
                '@mdx-js/loader'
            ]
        });

        return rule
    });
    return config
};

Don't worry too much about the contents of this file. It's a file that utilizes Babel to load several packages and plugin such as the

mdx-js/loader
and
.mdx
files.

Go ahead and install the following packages:

npm install node-sass-chokidar @mdx-js/loader @mdx-js/mdx npm-run-all react-router react-router-dom --save
  • @mdx-js/loader: Webpack loader for MDX.
  • @mdx-js/mdx: MDX Implementation for parsing the
    mdx
    files.
  • node-sass-chokidar: For watching and converting scss to css files.
  • npm-run-all: A CLI tool to run multiple npm-scripts in parallel or sequential.
  • react-router and react-router-dom: Packages for routing within the app.

Once the installation is complete, open

package.json
in the root directory and change the existing calls to
react-scripts
in
npm
scripts

...
"scripts": {
  "build-css": "node-sass-chokidar src/ -o src/",
  "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive",
  "start-js": "react-app-rewired start",
  "start": "npm-run-all -p watch-css start-js",
  "build-js": "react-app-rewired build",
  "build": "npm-run-all build-css build-js",
}

Now, run the app for the first time using

npm start
to be sure the packages got installed properly and the config works. Let's create our components.

Creating Our React Components

Components allow us to split the UI into reusable, independent pieces. Now, let's go ahead and create the following directories:

mkdir src/Page src/Header src/Nav src/GuardedRoute src/markdown

The

markdown
directory will contain all the markdown files that contain our documentation pages.

To get the

.mdx
files easily, follow this process:

  • Clone the mdx-tutorial repo.
  • Unzip the folder.
  • Copy the files from
    src/markdown
    in the downloaded folder.
  • Paste the copied files into your project's
    src/markdown
    folder.

Header Component

In the

src/Header
directory, create
Header.js
file and add the following:

src/Header/Header.js

import React from 'react';
import {Link, withRouter} from 'react-router-dom';

function Header(props) {

  return (
    <nav className="navbar navbar-dark bg-primary fixed-top">
      <Link className="navbar-brand" to="/">
        JollofJS Documentation App
      </Link>
      <button className="btn btn-dark">Sign In</button>
    </nav>
  );
}

export default withRouter(Header);

The

Header
component contains the
Sign In
button and will house the
Sign out
button when we add authentication later on in the tutorial.

Let's create the

Page
component. This is the major component that handles the processing of the markdown files.

Page Component

In the

src/Page
directory, create two files,
Page.js
and
style.scss
file and add code to them like so:

src/Page/Page.js

import React, { Component } from 'react';
import Nav from "../Nav/Nav";
import './style.css';

class Page extends Component {

    constructor(props){
      super(props);

      this.state = {
        Component: '',
        page: this.props.match.params.page
      }
    }

    async addComponent(name){
      import(`../markdown/${name}.mdx`)
        .then(component =>
          this.setState({
            Component: component.default
          })
        )
        .catch(() => {
          this.setState({
            Component: ''
          })
        });
    }

    static capitalize(text){
      return text.substr(0, 1).toUpperCase() + text.substr(1);
    }

    async componentDidMount() {
      await this.addComponent(Page.capitalize(this.state.page));
    }

    async componentDidUpdate(prevProps, prevState) {
      if(prevState.page !== this.state.page) {
         await this.addComponent(Page.capitalize(this.state.page));
      }
    }

    static getDerivedStateFromProps(nextProps, prevState) {
      if(nextProps.match.params.page !== prevState.page) {
        return {
          page: nextProps.match.params.page
        }
      }

      else return null;
    }

    render() {
        const { Component } = this.state;

        return (
            <div className="container">
                <div>
                    <Nav />
                    <div className="content">
                        { Component ? <Component /> : null }
                    </div>
                </div>
            </div>
        );
    }
}

export default Page;

Create

style.scss
file and copy the code from this file here to it.

Let's analyze this code properly.

  • addComponent: The
    addComponent
    function dynamically imports
    .mdx
    files from the
    markdown
    directory and stores the content in the app's state.
  • capitalize: The
    capitalize
    function simply capitalizes the name of the route to match the name of the file in the
    markdown
    directory.
  • componentDidMount: This is a lifecycle method that's invoked once the
    Page
    component loads. This method calls the
    addComponent
    method when it is invoked.
  • getDerivedStateFromProps: This is a static method which is invoked after a component is instantiated as well as when it receives new props. It receives two params
    nextProps
    and
    prevState
    . We compared the prop stored in the state with the next prop. If it is different, it returns the new prop as a new state else it returns null.
  • componentDidUpdate: This is a new lifecycle method that's fired when state changes and we called the
    addComponent
    method to load the content of the new markdown file.

This is the backbone of our application. This component is able to load the content of the

mdx
files because of the incredible
@mdx-js/loader
and
@mdx-js/mdx
tools.

Sidebar Navigation Component

In the

src/Nav
directory, create
Nav.js
file and add the following:

src/Nav/Nav.js

import React, { Component } from 'react';
import { NavLink } from "react-router-dom";
import './style.css';

class Nav extends Component{
  constructor(props){
    super(props);
  }

  render(){
    return (
      <div className="sidenav">
        <div className="sidenavbar">
          <NavLink to='/page/home' activeClassName="active">Home </NavLink>
          <NavLink to='/page/intro' activeClassName="active">Get Started </NavLink>
          <NavLink to='/page/concepts' activeClassName="active">Main Concepts </NavLink>
          <NavLink to='/page/reference' activeClassName="active">API Reference </NavLink>
          <NavLink to='/page/libraries' activeClassName="active">Libraries </NavLink>
          <NavLink to='/page/contributors' activeClassName="active">Contributors </NavLink>
        </div>
      </div>
    )
  }
}

export default Nav;

Create

style.scss
file and copy the code from this file into it.

The

Nav
component will be our sidebar navigation system. Without it, users won't be able to go through the various documentation pages.

Welcome Component

This is the component that makes up the landing page. Create a

src/Welcome
directory and add a
Welcome.js
file to it. Now add code to it like so:

src/Welcome/Welcome.js

import React, {Component} from 'react';

class Welcome extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <div className="container">
        <div className="row">
          <div className="jumbotron col-12">
            <h1 className="display-3">Welcome to the Jollof JS Framework</h1>
          </div>
        </div>
      </div>
    )
  }
}

export default Welcome;

Set Up Routing

Open up

index.js
and make sure it is set up to handle the routing like so:

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

ReactDOM.render(
  <BrowserRouter>
    <App/>
  </BrowserRouter>,
  document.getElementById('root')
);
registerServiceWorker();

In the code above, we can see clearly that

App
is imported. So, let's set up
App.js
properly with the routes.

src/App.js

import React, { Component } from 'react';
import {Route, withRouter} from 'react-router-dom';
import Header from './Header/Header';
import Welcome from './Welcome/Welcome';
import Page from "./Page/Page";

class App extends Component {

  render() {
    return (
      <div>
        <Header />
        <Route exact path='/' component={Welcome} />
        <Route exact path='/page/:page' component={Page} />
      </div>
    );
  }
}

export default withRouter(App);

One more thing. Set up bootstrap in the

public/index.html
file like so:

<!DOCTYPE html>
<html lang="en">
  <head>
    ...
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <link rel="stylesheet" href="https://bootswatch.com/4/flatly/bootstrap.min.css">
    ...
  </head>
  <body>
    ...
  </body>
</html>

If you need to, run your app again, open

http://localhost:3000
and you'll see the Welcome screen as follows:

Welcome localhost JollofJS home

Now, go to the

/page/home
route and use the sidebar navigation to check the documentation for each section!

Documentation App navigation - Main concepts Main concepts

Adding Authentication to Our React App

Auth0 allows us to easily add authentication to applications. If you don't have an Auth0 account yet, you can sign up for a free one here. Login to your Auth0 management dashboard and create a new application. In the dialog shown, enter the name and select Single Page Application as its type:

Select Application within Auth0 Dashboard Select application

Then, click on the

Create
button. Go ahead and click on the
Settings
tab.

Auth0 Dashboard Project App Settings Grab client id

In the Settings tab, add

http://localhost:3000/callback
in the Allowed Callback URLs and
http://localhost:3000
to the Allowed Origins (CORS).

Click on the

SAVE CHANGES
button once you are done modifying the settings with the information above.

Create the Auth Service

We'll create an authentication service to handle everything about authentication in our app. Go ahead and create an

Auth.js
file under the
src
directory.

Before we add code, you need to install

auth0-js
package like so:

npm install auth0-js --save

Open up the

Auth.js
file and add code to it like so:

src/Auth.js

import auth0 from 'auth0-js';

class Auth {
  constructor() {
    this.auth0 = new auth0.WebAuth({
      // the following three lines MUST be updated
      domain: '<YOUR_AUTH0_DOMAIN>',
      clientID: '<YOUR_AUTH0_CLIENT_ID>',
      redirectUri: 'http://localhost:3000/callback',
      audience: 'https://<YOUR_AUTH0_DOMAIN>/userinfo',
      responseType: 'token id_token',
      scope: 'openid email'
    });

    this.handleAuthentication = this.handleAuthentication.bind(this);
    this.isAuthenticated = this.isAuthenticated.bind(this);
    this.signIn = this.signIn.bind(this);
    this.signOut = this.signOut.bind(this);
  }

  getIdToken() {
    return this.idToken;
  }

  handleAuthentication() {
    return new Promise((resolve, reject) => {
      this.auth0.parseHash((err, authResult) => {
        if (err) return reject(err);
        if (!authResult || !authResult.idToken) {
          return reject(err);
        }
        this.setSession(authResult);
        resolve();
      });
    })
  }

  isAuthenticated() {
    return new Date().getTime() < this.expiresAt;
  }

  setSession(authResult) {
    this.idToken = authResult.idToken;
    // set the time that the id token will expire at
    this.expiresAt = authResult.expiresIn * 1000 + new Date().getTime();
  }

  signIn() {
    this.auth0.authorize();
  }

  signOut() {
    // clear id token, and expiration
    this.idToken = null;
    this.expiresAt = null;
  }

  silentAuth() {
    return new Promise((resolve, reject) => {
      this.auth0.checkSession({}, (err, authResult) => {
        if (err) return reject(err);
        this.setSession(authResult);
        resolve();
      });
    });
  }
}

const auth = new Auth();

export default auth;

Note: Replace the

<YOUR_AUTH0_CLIENT_ID>
and
<YOUR_AUTH0_DOMAIN>
with the values from your Auth0 application settings.

  • YOURAUTH0CLIENT_ID:
    Client ID
    from the Auth0 application setting we created earlier.
  • YOURAUTH0DOMAIN:
    Domain
    from the Auth0 application setting we created earlier.

Let's analyze what's going on in the authentication code above:

  • constructor: An instance of
    auth0.WebAuth
    is created and initialized with your Auth0 values and define some other important configurations. For example, you are defining that Auth0 will redirect users (redirectUri) to the
    http://localhost:3000/callback
    URL (the same one you inserted in the Allowed Callback URLs field previously).
  • getIdToken: This method returns the
    idToken
    generated by Auth0 for the current user.
  • handleAuthentication: This is the method that your app will call right after the user is redirected from Auth0. This method simply reads the hash segment of the URL to fetch the user details and the id token.
  • isAuthenticated: This method returns whether there is an authenticated user or not.
  • signIn: This method initializes the authentication process. This method sends your users to the Auth0 login page.
  • signOut: This method signs a user out.

The configuration parameters of

auth0.WebAuth
are as follows:

  • domain: Your Auth0 domain such as
    example.auth0.com
    .
  • clientID: The
    Client ID
    found on your Application settings page.
  • redirectUri: The URL where Auth0 will call back to with the result of a successful or failed authentication. It must be whitelisted in the Allowed Callback URLs in your Auth0 Application's settings.
  • audience: The default audience, used if requesting access to an API.
  • responseType: Response type for all authentication requests. It can be any space separated list of the values code,
    token
    ,
    id_token
    . If you don't provide a global responseType, you will have to provide a responseType for each method that you use.
  • scope: The default scope used for all authorization requests.

Let's update the

Header
component to hide/show the
login
and
logout
buttons based on the user's authentication status.

Now, your

Header
component should be refactored to look like this:

src/Header/Header.js

import React from 'react';
import {Link, withRouter} from 'react-router-dom';
import auth from '../Auth';

function Header(props) {
  const signOut = () => {
    auth.signOut();
    props.history.replace('/');
  };

  return (
    <nav className="navbar navbar-dark bg-primary fixed-top">
      <Link className="navbar-brand" to="/">
        JollofJS Documentation App
      </Link>
      {
        !auth.isAuthenticated() &&
        <button className="btn btn-dark" onClick={auth.signIn}>Sign In</button>
      }
      {
        auth.isAuthenticated() &&
        <div>
          <button className="btn btn-dark" onClick={() => {signOut()}}>Sign Out</button>
        </div>
      }
    </nav>
  );
}

export default withRouter(Header);

Add A Callback Component

We will create a new component in the

src
directory and call it
Callback.js
. This component will be activated when the
localhost:3000/callback
route is called and it will process the redirect from Auth0 and ensure we received the right data back after a successful authentication.

src/Callback.js

import React, {Component} from 'react';
import {withRouter} from 'react-router-dom';
import auth from './Auth';

class Callback extends Component {
  async componentDidMount() {
    await auth.handleAuthentication();
    this.props.history.replace('/page/home');
  }

  render() {
    return (
      <p>Loading profile...</p>
    );
  }
}

export default withRouter(Callback);

Once a user is authenticated, Auth0 will redirect back to our application and call the

/callback
route. Auth0 will also append the
id_token
to this request, and our Callback component will make sure to properly process and store the token in the in-app memory. Then, the app will be redirected back to the
/
page and will be in a logged-in state.

Now, go ahead and add the

/callback
route in
App.js
.

src/App.js

...
import Callback from './Callback';
...
 class App extends Component {

   render() {
    return (
      <div>
        <Header />
        <Route exact path='/' component={Welcome} />
        <Route exact path='/page/:page' component={Page} />
        <Route exact path='/callback' component={Callback} />
      </div>
    );
  }
}

export default withRouter(App);

Now, try to run the app again.

App localhost Welcome Page Welcome

Click on the

Sign In
button.

Localhost App User Authentication Sign In modal Sign In

Keeping Users Signed In

Right now, once you reload the application, the users automatically get logged out because the user's credentials are stored in the app's memory. Let's keep the users logged in!

Note: The routes are not guarded yet, so you can still navigate the app. We'll add a guard to secure the page route later on in this article.

We'll use the Silent Authentication provided by Auth0. Whenever your application is loaded, it will send a silent request to Auth0 to check if the current user (actually the browser) has a valid session. If they do, Auth0 will send back to you an

idToken
and an
idTokenPayload,
just like it does on the authentication callback.

Head over to the Applications section of your Auth0 dashboard. Click on the MDX app from the list provided, then head over to the

Settings
tab of the app and update the following:

  • Allowed Web Origins: Add
    localhost:3000
    to the field. Without this value there, Auth0 would deny any AJAX request coming from your app.
  • Social Connections: Auth0 auto-configure all new accounts to use development keys registered at Google for the social login. We expect developers to replace these keys with theirs once they start using Auth0 in more capacity. Furthermore, every time an app tries to perform a silent authentication, and that app is still using the development keys, Auth0 returns that there is no session active (even though this is not true). Head over to the Social connections on your dashboard, and update the Client ID and Client Secret fields with the new keys gotten from Connect your app to Google documentation provided by Auth0..

Note: If you don't want to use your Google keys, you can deactivate this social connection and rely only on users that sign up to your app through Auth0's Username and Password Authentication.

Check out the

silentAuth
function in the
Auth.js
file:

src/Auth.js

...
silentAuth() {
  return new Promise((resolve, reject) => {
    this.auth0.checkSession({}, (err, authResult) => {
      if (err) return reject(err);
      this.setSession(authResult);
      resolve();
    });
  });
}
...

This method is responsible for performing the Silent Authentication. It makes a request to AuthO's

checkSession
function and sets a new session.

Let's refactor the

src/Auth.js
file to ensure the new authentication works properly.

src/Auth.js

import auth0 from 'auth0-js';

class Auth {
  constructor() {
    ...
    this.authFlag = 'isLoggedIn';
    ...
  }

  ...

  isAuthenticated() {
    return JSON.parse(localStorage.getItem(this.authFlag));
  }

  setSession(authResult) {
    this.idToken = authResult.idToken;
    localStorage.setItem(this.authFlag, JSON.stringify(true));
  }

  signIn() {
    this.auth0.authorize();
  }

  signOut() {
    localStorage.setItem(this.authFlag, JSON.stringify(false));
    this.auth0.logout({
      returnTo: 'http://localhost:3000',
      clientID: '<YOUR_AUTH0_CLIENT_ID>',
    });
  }

  silentAuth() {
    if(this.isAuthenticated()) {
      return new Promise((resolve, reject) => {
        this.auth0.checkSession({}, (err, authResult) => {
          if (err) {
            localStorage.removeItem(this.authFlag);
            return reject(err);
          }
          this.setSession(authResult);
          resolve();
        });
      });
    }
  }
}

const auth = new Auth();

export default auth;

Note: Quickly head over to the Applications section of your Auth0 dashboard, click on the MDX app from the list provided, then head over to the

Settings
tab of the app and update the Allowed Logout URLs section with
http://localhost:3000
.

Let's analyze the change above. The

authFlag
constant is a flag that we store in local storage to indicate whether a user is logged in or not. It is also an indicator for renewing tokens with the Auth0 server after a full-page refresh.

Once the user is authenticated and redirected back to the app from the Auth0 server, the

authFlag
is set to true and stored in local storage so that if the user returns to the app later, we can check whether to ask the authorization server for a fresh token. The
silentAuth
method checks if the user is indeed authorized via the Auth0
checkSession
method if the
authFlag
is true. If the user is authorized, new authentication data is returned and the
setSession
method is called. If the user is not authorized, the
authFlag
is deleted from local storage and logged out.

One more thing. Head over to

App.js
and update it like so:

src/App.js

import React, { Component } from 'react';
import {Route, withRouter} from 'react-router-dom';
import Header from './Header/Header';
import Welcome from './Welcome/Welcome';
import Callback from './Callback';
import Page from "./Page/Page";
import auth from './Auth';

class App extends Component {

  async componentDidMount() {
    if (this.props.location.pathname === '/callback') return;
    try {
      await auth.silentAuth();
      this.forceUpdate();
    } catch (err) {
      if (err.error === 'login_required') return;
    }
  }


  render() {
    return (
      <div>
        <Header />
        <Route exact path='/' component={Welcome} />
        <Route exact path='/page/:page' component={Page} />
        <Route exact path='/callback' component={Callback} />
      </div>
    );
  }
}

export default withRouter(App);

If the requested route is

/callback
, the app does nothing. This is the correct behavior because, when users are requesting for the
/callback
route, they do so because they are getting redirected by Auth0 after the authentication process. In this case, you can leave the Callback component to handle the process.

If the requested route is anything else, the app wants to try a

silentAuth
. Then, if no error occurs, the app calls
forceUpdate
so the user can see its name and that they are signed in.

If there is an error on the

silentAuth
, the app checks if the error is
login_required
. If this is the case, the app does nothing because it means the user is not signed in (or that you are using development keys, which you shouldn't). If there is an error that is not
login_required
, the error is simply logged to the console. Actually, in this case, it would be better to notify someone about the error so they could check what is happening.

Try to authenticate your app. Then, refresh your browser and you'll discover that you won't be logged out on a page refresh.

One more thing. Let's make sure the page routes can't be accessed if the user is not authenticated!

Secure Page Route

Create a

GuardedRoute.js
file in the
src/GuardedRoute
directory. Now add code to the
GuardedRoute.js
file:

src/GuardedRoute/GuardedRoute.js

import React from 'react';
import {Route} from 'react-router-dom';
import auth from '../Auth';

function GuardedRoute(props) {
  const { component: Component, path} = props;
  return (
    <Route exact path={path} render={(props) => {
     if (!auth.isAuthenticated()) return auth.signIn();
      return <Component {...props} />
    }} />
  );
}

export default GuardedRoute;

In the code above, we created a functional component that checks if a user is authenticated before rendering a component. Let's apply this recently created functionality to our

Page
route.

Open up

App.js
. Make sure the
GuardedRoute
is imported at the top of the file like so:

src/App.js

...
import GuardedRoute from './GuardedRoute/GuardedRoute';
...

Now, pass the

Page
component to the
GuardedRoute
and replace
<Route exact path='/page/:page' component={Page}>
with
<GuardedRoute ... />
like so:

src/App.js

...
import GuardedRoute from './GuardedRoute/GuardedRoute';
...

class App extends Component {

  ...

  render() {
    return (
      <div>
        <Header />
        ...
        <GuardedRoute path='/page/:page' component={Page} />
      </div>
    );
  }
}

export default withRouter(App);

Once you try to access any

/page
route when you are not logged in, you'll be redirected to the login page.

Conclusion

There's a lot to learn about MDX. I just scratched the surface of what's possible in this tutorial. As a developer, MDX gives you enough flexibility and power to easily build awesome documentation software for your closed and open source projects while Auth0 takes care of securing the app.

MDX gives you enough flexibility and power to easily build awesome documentation software for your closed and open source projects

Tweet This

I'm learning more about MDX and its various use cases. Have you used MDX? Let me know in the comments section!