This movie catalog app illustrates the usage of react-engine to build and run an universal/isomorphic app.
- express - 4.x on the server side
- react-engine - 4.x as the express view render engine
- react - 16.x for building the UI
- react-router - 3.x for UI routing
- webpack - 1.x as the client side module loader
- babel - 6.x for compiling the ES6/JSX code
# cd `into_this_dir`
$ npm install
$ npm start # or `npm run dev`
$ open http://localhost:3000 # let us start by installing the dependencies for our app
# create a npm manifest
# (fill out the needed information like name, author, etc..)
$ npm init
# install express, react, react-router (optional) & react-engine
$ npm install express@4 react-engine@4 react@16 react-dom@16 react-router@3 --save
# install the rest of the dependencies
$ npm install babel-register@6 babel-preset-react@6 webpack@1 --save
# we are going to use a static json file that contains
# an array of movie information as the data source for
# our movie catalog app
$ touch movies.json
# copy the contents for this file from http://bit.ly/2BbcpIb # next, let us build the client side of our app
# create a directory called public and inside that
# create the client side index file
$ mkdir public
$ touch public/index.js
# create a directory called views to hold all the view files
# also create a client side routes file to hold the react-router routes
$ mkdir public/views
$ touch public/routes.jsx
# setup the client side react-engine inside public/index.js
# instructions: https://github.com/paypal/react-engine#usage-on-client-side-mounting
# public/index.js code for our app can be found here - http://bit.ly/2DLbIIp // since we are building a movie catalog app, let us plan to have two UI pages.
// 1. list page - to list the catalog of movies
// 2. detail page - to show the detailed description of a movie
// lets start building the react-router route file (public/routes.jsx)
// keeping in mind the above requirements
const Layout = require('./views/layout.jsx');
const ListPage = require('./views/list.jsx');
const DetailPage = require('./views/detail.jsx');
const routes = module.exports = (
<Router>
<Route path='/' component={Layout}>
<IndexRoute component={ListPage} />
<Route path='/movie/:id' component={DetailPage} />
<Redirect from='/gohome' to='/' />
</Route>
</Router>
); # next, lets build the actual UI inside the pages
# create the below three files inside the public/views directory
$ touch public/views/layout.jsx
$ touch public/views/list.jsx
$ touch public/views/detail.jsx // public/views/layout.jsx file contains the main parts of the app
// such as html, body and script tags.
module.exports = (props) => {
return (
<html>
<head>
<meta charSet='utf-8' />
<title>React Engine Example App</title>
<link rel='stylesheet' href='/styles.css'></link>
</head>
<body>
<div>
{/* Router now automatically populates props.children of your components based on the active route. https://github.com/rackt/react-router/blob/latest/CHANGES.md#routehandler */}
{props.children}
</div>
<script src='/bundle.js'></script>
</body>
</html>
);
};
// public/views/list.jsx file contains the catalog view elements of our app.
// we iterate through the array of movies and display them on this page.
module.exports = (props) => {
return (
<div id='list'>
<h1>Movies</h1>
<h6>Click on a movie to see the details</h6>
<ul>
{props.movies.map((movie) => {
return (
<li key={movie.id}>
<Router.Link to={`/movie/${movie.id}`}>
<img src={movie.image} alt={movie.title} />
</Router.Link>
</li>
);
})}
</ul>
</div>
);
};
// public/views/detail.jsx file contains the markup to
// display the detail information of a movie
module.exports = (props) => {
const movieId = props.params.id;
const movie = props.movies.find((_movie) => _movie.id === movieId);
return (
<div id='detail'>
<h1>{movie.title}</h1>
<img src={movie.image} alt={movie.title} />
<a href={movie.url} target='_blank'>more info</a>
</div>
);
}; # next, lets add the server side file
$ touch index.js // start by configuring Babel at the top
// this takes care of parsing JSX files and also ES6 code
require('babel-register')({
presets: ['react']
});
// next, lets create the express app
const express = require('express');
const renderer = require('react-engine');
const app = express();
// then create the view engine for our express app
const reactRoutesFilePath = path.join(__dirname + '/public/routes.jsx');
const engine = renderer.server.create({
routes: require(reactRoutesFilePath),
routesFilePath: reactRoutesFilePath
});
// then configure our express app with the view engine that we created
// set the engine
app.engine('.jsx', engine);
// set the view directory
app.set('views', path.join(__dirname, '/public/views'));
// set jsx as the view engine
app.set('view engine', 'jsx');
// finally, set the custom view
app.set('view', renderer.expressView);
// next, lets configure the routes for the express app
// expose public folder as static assets (JS/CSS)
app.use(express.static(path.join(__dirname, '/public')));
// add the our app routes
// we open a free pass to all GET requests to our app and use react-engine to render them
app.get('*', (req, res) => {
res.render(req.url, {
movies: require('./movies.json')
});
});
// add the error handler middleware
app.use((err, req, res, next) => {
console.error(err);
// http://expressjs.com/en/guide/error-handling.html
if (res.headersSent) {
return next(err);
}
if (err._type && err._type === ReactEngine.reactRouterServerErrors.MATCH_REDIRECT) {
return res.redirect(302, err.redirectLocation);
}
else if (err._type && err._type === ReactEngine.reactRouterServerErrors.MATCH_NOT_FOUND) {
return res.status(404).send('Route Not Found!');
}
else {
// for ReactEngine.reactRouterServerErrors.MATCH_INTERNAL_ERROR or
// any other error we just send the error message back
return res.status(500).send(err.message);
}
});
// the last step in the server side is to configure the express app to listen on port 3000
app.listen(3000, () => {
console.log('Example app listening at http://localhost:%s', PORT);
});
// the consolidated full code for this file can be
// found here: http://bit.ly/1MdzR5c # finally, lets configure webpack, our client side module loader
# we need two webpack loaders for our app
# 1. babel-loader for webpack to load jsx and es6 code
# 2. json-loader for webpack to load json files
$ npm install babel-loader@6 json-loader@0.5 --save
# next, add a webpack configuration file
$ touch webpack.config.js
# configure webpack to build a bundle.js file using public/index.js as the main file
# module.exports = {
#
# entry: __dirname + '/public/index.js',
#
# output: {
# path: __dirname + '/public',
# filename: 'bundle.js'
# },
#
# module: {
# loaders: [
# {
# test: /\.jsx?$/,
# exclude: /node_modules/,
# loader: 'babel?presets[]=react'
# },
# {
# test: /\.json$/,
# loader: 'json-loader'
# }
# ]
# },
#
# resolve: {
# extensions: ['', '.js', '.jsx', '.json']
# }
# }; # modify the public/views/layout.jsx file to add the bundle.js into it
# <script src='/bundle.js'></script>
# also lets add a start script to our package.json to build our client code using webpack and then start the app
# "scripts": {
# "start": "webpack && node index.js"
# }
# now that we are done with the app,
# lets start the app and launch http://localhost:3000 in a browser
$ npm start # to beautify our movie catalog app we are going to add some css
# modify the public/views/layout.jsx file to add the styles.css into it
# <link rel='stylesheet' href='/styles.css'></link>
$ touch public/styles.css
# copy the contents for this file from http://bit.ly/2mQoU7n