Full-Stack Engine

How to create a project with Sails.js, webpack and React. Part 2

In the previous part of this guide, we made a starter project or template for new projects, integrating some of the most popular and powerful frameworks for web development in the Node.js realm: Sails.js, React and Webpack.

What this project includes

We'll continue this guide adding a new feature to our starter project, with the use of a client-side router, for which I've chosen react-router given its popularity and long release history. We'll learn how to tell react-router how to work along with the server side router provided by Sails.js.

On the other hand, we include bootstrap (version 4) as our styling frameworks, which is one of the most popular UI frameworks in the React world and has a lot of components and functionalities for our web applications.

Finally, unless not strictly necessary, we'll include reacstrap, a library with several ready-to-use react components with bootstrap 4 styles, which saves us time and reduces the amount of HTML needed code.

Let's recap the agenda for this guide:

  1. Part 1: Basic project setup with Sails.js v 1.0 (back-end), React 16 (front-end lib) and webpack 4 (as the module loader/bundler).
  2. (*) Part 2: Project setup adding bootstrap (4) for UI styling along with the use of reactstrap, react-router for client-side routing/history and jest/enzyme for basic testing.

You can find the complete project's template source here (part-2).

Before we start

We'll take the starter project created in part-1 as the start point and continue from there setting up our application.

As we're using webpack as the bundler for the client libraries, we can get rid of the folder tasks that comes with the sails-cli generator. This folder contains the tasks related to copying, uglifying, transpiling and others that rely upon default on grunt in a typical Sails.js new project. However, webpack replaces all of its functionalities, so is safe to delete it.

We can also clean our project structure a bit more, by renaming assets as client, I believe this a more appropriate name, as it contains the code for the client side of the project. After that, we can delete all folders inside the client, except js, as we won't need them for now. Of course, we can add them later as we need to include images, styles or other assets.

Without more delays, let's begin,

Add and configure React

After some cleaning, it's time to start adding the first library: React. We do this by running,

npm install --save react react-dom

We'll also need babel presets for react-16, to transpile es6 and jsx code to basic es5 (the most widely support javascript specification by browsers) properly,

npm install --save-dev babel-preset-env babel-preset-react babel-preset-react-hmre,

Then, modify .babelrc to set up these presets,

{
  "presets": ["es2015", "react"],
  "plugins": [
    "transform-object-rest-spread",
    "transform-class-properties"
  ],
  "env": {
    "development": {
      "presets": ["react-hmre"]
    }
  }
}

Webpack environments and webpack-dev-server

Now, it's time to configure webpack to bundle and load React js code, but we'll also make use of a neat feature called webpack-dev-server that reloads the application in real-time, when we make any change to the code, turning our development experience in a more satisfying and agile oriented one.

Let's add some libraries to support hot-reload feature along with the dev-server,

npm install --save-dev webpack-dev-server webpack-hot-middleware opn

As we only need webpack-dev-server in the development phase, let's add a webpack configuration file for dev only,

import webpack from 'webpack';

const HtmlWebpackPlugin = require('html-webpack-plugin');
const precss = require('precss');
const autoprefixer = require('autoprefixer');
const path = require('path');

module.exports = {
  devtool: 'source-map',
  entry: [
    './client/js/index.js',
    'webpack-hot-middleware/client?reload=true'
  ],
  output: {
    path: path.join(__dirname, '/.tmp/public'),
    filename: 'bundle.js',
    publicPath: '/'
  },
  module: {
    rules: [
      {
        use: 'babel-loader',
        test: /\.js$/,
        exclude: /node_modules/
      },
      {
        use: ['style-loader', 'css-loader'],
        test: /\.css$/
      },
      {
        test: /\.(scss)$/,
        use: [{
          loader: 'style-loader' // inject CSS to page
        }, {
          loader: 'css-loader' // translates CSS into CommonJS modules
        }, {
          loader: 'postcss-loader', // Run post css actions
          options: {
            plugins: () => // post css plugins, can be exported to postcss.config.js
              [
                precss,
                autoprefixer
              ]
          }
        }, {
          loader: 'sass-loader' // compiles Sass to CSS
        }]
      }
    ]
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: 'client/index.html'
    }),
    new webpack.HotModuleReplacementPlugin()
  ]
};

The main difference from the original webpack.config.js is the use webpack-hot-middleware to reload the server every time a change happens, and the line devtool: 'source-map', that let us inspect the source code, without being transpile, as we see it in our code editor. The following screenshot shows it in action,

devtool

Web Server

Maybe you're wondering that if webpack has its server, then what happens to Sails.js, isn't it supposed to work as the web server to accept requests from the client side?. Well, it's the server in the production environment. However, in development, we need a mean to get both working together, for this reason, we'll add a new folder called tools and inside it a file dev.js,

import opn from 'opn';
import path from 'path';
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';

import webpackConfig from '../webpack.config.dev';

const compiler = webpack(webpackConfig);

const serverConfig = {
  contentBase: path.resolve(__dirname, '../client'),
  hot: true,
  inline: true,
  stats: {
    colors: true
  },
  publicPath: webpackConfig.output.publicPath,
  historyApiFallback: true,
  proxy: {
    '/api/*': 'http://localhost:1337'
  },
  headers: {
    'Cache-Control': 'no-cache, no-store, must-revalidate',
    Pragma: 'no-cache',
    Expires: '-1',
    'X-Frame-Options': 'SAMEORIGIN'
  }
};

const server = new WebpackDevServer(compiler, serverConfig);

server.use(require('webpack-hot-middleware')(compiler));

const host = 'localhost';
const port = 8080;

server.listen(port, host, (err) => {
  if (err) {
    console.log('[webpack-dev-server] failed to start: ', err);
  } else {
    const protocol = 'http';
    const openHost = host;
    const suffix = webpackConfig.output.publicPath;
    const openURL = `${protocol}://${openHost}:${port}/webpack-dev-server${suffix}`;

    const openMsg = `[webpack-dev-server] started: opening the app: ${openURL}`;
    console.log(openMsg);
    opn(openURL);
  }
});

server.app.get('/reload', (req, res) => {
  // Tell connected browsers to reload.
  server.sockWrite(server.sockets, 'ok');
  res.sendStatus(200);
});

server.app.get('/invalid', (req, res) => {
  // Tell connected browsers some change is about to happen.
  server.sockWrite(server.sockets, 'invalid');
  res.sendStatus(200);
});

As you might know, Sails works on top of another framework, a pillar in the MEAN stack, Express.js. Here we make use of it and the webpack dev server API for Node.js. We take the configuration from webpack.config.dev.js, set a proxy for the web API listening at port 1334 (we'll see how this work in the next part) and then set the server listening for regular requests at port 8080. As the final step, we use the utility lib opn to launch the web page in the browser automatically for us.

Now, we can add a script in package.json to run this script inside dev.js in a single step, using babel-cli as follows,

"scripts": {
  ...
  "open:client": "babel-node tools/dev.js",
  ...
}

React & Sails Router

Getting back to the main app code, we're going now to set the routing configuration up in 2 steps, one in the client and the other on the server. Let's begin with the client. We first need to install React Router for the web, for what we use react-router-dom,

npm install --save react-router-dom,

Then, in the index of the app in client/js/index.js we wrap the whole component inside a <BrowserRouter> tag, so all the components bellow share a familiar context for the different routes we add.

Next, we define landing page (LandingPage.js, we wrap the whole component inside a <BrowserRouter> tag, so all the components bellow shares a familiar context for the different routes we add.
Thus, we implement the navigation feature with the buttons to go to the different sections of the page. We'll name them as Home, Contact and More.

import React from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import NavBar from './navbar/Navbar';
import Home from './home/Home';
import Contact from './contact/Contact';
import More from './more/More';

export default function LandingPage() {
  return (
    <Router>
      <div>
        <NavBar />
        <main>
          <Route exact path="/" component={Home} />
          <Route path="/home" component={Home} />
          <Route path="/contact" component={Contact} />
          <Route path="/more" component={More} />
        </main>
      </div>
    </Router>
  );
}

We'll define these components in the next section. However, before that, we need to tell Sails how to respond to requests that belong to routes in react and not for the server. For example, the user might enter http://my-react-app.com/home, where /home is not a resource in the server, but an in the client side, and its the task of react-router to respond rendering the corresponding JSX/HTML segment in the current page.

Sails Routes/Controller

First, go to config/routes.js and change the lines containing the following code:

  '/': {
    view: 'pages/homepage'
  },

to this:

  '/*': {
    controller: 'IndexController',
    action: 'render',
    skipAssets: true
  }

For all the requests coming from the browser, the server responds with the render action in the IndexController. So let's define the IndexController, we can quickly create a new one by typing into the command line,

sails generate controller Index

Then, go to api/controllers/IndexController.js and write the render action,

/**
 * IndexController
 *
 * @description :: Server-side actions for handling incoming requests.
 * @help        :: See https://sailsjs.com/docs/concepts/actions
 */

module.exports = {
  render: (req, res) => res.view('pages/homepage')
};

Now the server responds to any request from the browser with the same contents from homepage.ejs, the page where react-router lives. That's what we're looking for to get the navigation working as expected.

Bootstrap and Reactstrap

Last, but not least, it's time to define the visual components of our starter app. Let's begin with <Navbar>. Create a new folder in client/components/navbar and inside it a new file Navbar.js,

import React, { Component } from 'react';
import {
  Collapse,
  Navbar,
  NavbarToggler,
  NavbarBrand,
  Nav,
  NavItem
} from 'reactstrap';
import { Link } from 'react-router-dom';

export default class NavBar extends Component {
  constructor(props) {
    super(props);

    this.toggle = this.toggle.bind(this);
    this.state = {
      isOpen: false
    };
  }
  toggle() {
    this.setState({
      isOpen: !this.state.isOpen
    });
  }
  render() {
    return (
      <div>
        <Navbar color="light" light expand="md">
          <NavbarBrand href="/">Sails React starter</NavbarBrand>
          <NavbarToggler onClick={this.toggle} />
          <Collapse isOpen={this.state.isOpen} navbar>
            <Nav className="mr-auto" navbar>
              <NavItem>
                <Link className="nav-link" to="/home">Home</Link>
              </NavItem>
              <NavItem>
                <Link className="nav-link" to="/contact">Contact</Link>
              </NavItem>
              <NavItem>
              <Link className="nav-link" to="/more">More...</Link>
              </NavItem>
            </Nav>
          </Collapse>
        </Navbar>
      </div>
    );
  }
}

This component shows a nancy navbar with a toggle button for expanding menus on small screens. Moreover, also includes a key component to get the routes working as we want them to do. Instead of adding anchors with href properties, pointing directly to the route paths, we use a <Link> component from react-router-dom, with a property called to, that contains the route path.
If you inspect any of the <NavItem>'s in the browser, you'll see they render into a simple anchor with a href property pointing to the route path, however, internally there is a event handler that catch this request and sends it to react-router instead of sending it to the server. So there is no HTTP request/response and page refresh.

Finally, let's add the components for Home, Contact and More. We'll use almost the same JSX template for the three of them. So, for simplicity, I'll show the code only for <Home> (client/components/home/Home.js),

import React from 'react';
import { Jumbotron, Button } from 'reactstrap';

const Home = () => (
    <div>
      <Jumbotron>
        <h1 className="display-3">Hello, world!</h1>
        <p className="lead">
          This is a simple hero unit, a simple Jumbotron-style component for calling extra attention
          to featured content or information.
        </p>
        <hr className="my-2" />
        <p>
          It uses utility classes for typgraphy and spacing to space content out within the larger
          container.
        </p>
        <p className="lead">
          <Button color="primary">Learn More</Button>
        </p>
      </Jumbotron>
    </div>
);

export default Home;

That's it. We are ready to run the application!.

We can start running the project in development mode, with the command,

npm run start

You can find the complete list of scripts, including build, debug, clean and others in package.json.

Coming up next

Thank you so much for reading this article!. I would like to include all of the contents in this second part. However, I didn't want to make it longer than necessary for a pleasant reading experience. For this reason, I'll continue with Unit Testing using Jest and Enzyme, and manage the app's state with Redux in the next part.

Again, thank you for your interest in this blog. Take care and talk to you soon. Best regards!.

Author image
Costa Rica
Passionate Software Developer with full-stack development experience.