(Full Stack React) - BuildingYelp PDF
(Full Stack React) - BuildingYelp PDF
(Full Stack React) - BuildingYelp PDF
Table of Contents
1. Setup
01. A Word on Dependencies
02. Babel
03. webpack
04. React
05. Creating Our app.js
06. Demo: Basic app.js
07. postcss
08. CSS modules
09. Demo: CSS Modules
10. Configuring Multiple Environments
11. Font Awesome
12. Demo: Environment
13. Webpack Tip: Relative requires
2. Configuring Testing
01. Building Our Test Skeleton
02. Our Testing Strategy
3. Routing
01. Building real routes
02. Main page and nested routes
03. Demo: Routing to Container Content
04. Routing to Maps
05. Demo: Routing to a Map
4. Getting a List of Places
01. Demo: A List of Places
5. Creating a Sidebar
01. Inlining Styles
02. CSS Variables
6. Splitting Up Components
01. Business Listing Component
02. Item Component
03. Rating Component
04. Demo: Rating Stars
05. Demo: Sidebar with Listings
Setup
One of the most painful parts of building a React app is building the boilerplate. We have so many
choices we can make to start building our application, it can be overwhelming how to even get started.
Were going to be building our application using a few tools that we find useful as well and help us
build our production apps here at Fullstack.io.
Check out the final version of the package.json and the webpack.config.js on
github at github.com/fullstackreact/react-yelp-clone
While there are a ton of great boilerplates you can use, often times using a boilerplate can be more
confusing than setting things up yourself. In this post were going to install everything directly and so
this should give you a good idea about how to start a new project from scratch on your own.
Throughout this process, well use some JavaScript features of ES6, inline css modules, async module
loading, tests, and more. Well use webpack for its ease of babel implementation as well as a few
other convenient features it provides.
In order to follow along with this process, ensure you have node.js installed and have npm
available in your $PATH.
If youre not sure if you have npm available in $PATH,
This is the folder structure well be building towards:
Lets create a new node project. Open a terminal and create the beginning of our folder structure:
$ mkdir yelp && cd $_
$ mkdir -p src/{components,containers,styles,utils,views}\
&& touch webpack.config.js
In the same directory, lets create our node project by using the npm init command and answering a
few questions about the project. After this command finishes, well have a package.json in the same
directory, which will allow us to define a repeatable process for building our app.
It doesnt quite matter how we answer the questions at this point, we can always update
the package.json to reflect changes.
Additionally, instead of the command npm init, we can use npm init -y to accept all
the defaults and not answer any questions.
$ npm init
A Word on Dependencies
TLDR; Install the dependencies in each of the code sample sections.
Before we can start building our app, well need to set up our build chain. Well use a combination of
npm and some configuration files.
Babel
Babel is a JavaScript compiler that allows us to use the next generation JavaScript today. Since these
features are not only convenient, but they make the process of writing JavaScript more fun.
Lets grab babel along with a few babel presets. In the same directory as the package.json, lets
install our babel requirements:
$ npm install --save-dev babel-core babel-preset-es2015 babel-preset-react babelpreset-react-hmre babel-preset-stage-0
Well need to configure babel so our application will compile. Configuring babel is easy and can be set
up using a file called .babelrc at the root of the project (same place as our package.json) file.
$ touch .babelrc
Lets include a few presets so we can use react as well as the react hot reloading features:
{
"presets": ["es2015", "stage-0", "react"]
}
Babel allows us to configure dierent options for dierent operating environments using the env key in
the babel configuration object. Well include the babel-hmre preset only in our development
environment (so our production bundle doesnt include the hot reloading JavaScript).
{
"presets": ["es2015", "stage-0", "react"],
"env": {
"development": {
"presets": ["react-hmre"]
}
}
}
webpack
Setting up webpack can be a bit painful, especially without having a previous template to follow. Not to
worry, however! Well be building our webpack configuration with the help of a well-built webpack
starter tool called hjs-webpack.
The hjs-webpack build tool sets up common loaders for both development and production
environments, including hot reloading, minification, ES6 templates, etc.
Lets grab a few webpack dependencies, including the hjs-webpack package:
$ npm install --save-dev hjs-webpack webpack
Webpack is a tad useless without any loaders or any configuration set. Lets go ahead and install a few
loaders well need as we build our app, including the babel-loader, css/styles, as well as the the url and
file loaders (for font-loading, built-in to hjs-webpack):
$ npm install --save-dev babel-loader css-loader style-loader postcss-loader urlloader file-loader
In our webpack.config.js at the root directory, lets get our webpack module started. First, lets get
some require statements out of the way:
const webpack = require('webpack');
const fs
const path
join
resolve
=
=
=
=
require('fs');
require('path'),
path.join,
path.resolve;
The hjs-webpack package exports a single function that accepts a single argument, an object that
defines some simple configuration to define a required webpack configuration. There are only two
required keys in this object:
in - A single entry file
out - the path to a directory to generate files
var config = getConfig({
in: join(__dirname, 'src/app.js'),
out: join(__dirname, 'dist')
})
The hjs-webpack includes an option called clearBeforeBuild to blow away any previously built
files before it starts building new ones. We like to turn this on to clear away any strangling files from
previous builds.
var config = getConfig({
in: join(__dirname, 'src/app.js'),
out: join(__dirname, 'dist'),
clearBeforeBuild: true
})
Personally, well usually create a few path variables to help us optimize our configuration when we start
modifying it from its default setup.
const
const
const
const
root
src
modules
dest
=
=
=
=
resolve(__dirname);
join(root, 'src');
join(root, 'node_modules');
join(root, 'dist');
Now, the hjs-webpack package sets up the environment specific (dev vs prod) configuration using
the first argument value process.argv[1], but can also accept an option to define if were working
in the development environment called isDev.
A development environment sets up a server without minification and accepts hot-reloading whereas a
production one does not. Since well use the value of isDev later in our configuration, well recreate
the default value in the same method. Alternatively, we can check to see if the NODE_ENV is
development:
const NODE_ENV = process.env.NODE_ENV;
const isDev = NODE_ENV === 'development';
// alternatively, we can use process.argv[1]
// const isDev = (process.argv[1] || '')
//
.indexOf('hjs-dev-server') !== -1;
// ...
var config = getConfig({
isDev: isDev,
in: join(src, 'app.js'),
out: dest,
clearBeforeBuild: true
})
Webpack expects us to export a configuration object from the webpack.config.js file, otherwise it
wont have access to the config variable. We can export the config object by adding the
module.exports at the end of the file:
// ...
var config = getConfig({
// ...
})
module.exports = config;
Well come back to modifying the configuration file shortly as we get a bit further along and need some
more configuration. For the time being, lets get our build up and running.
Check out the final version of the webpack.config.js.
React
In order to actually build a react app well need to include the react dependency. Unlike the previous
dependencies, well include react (and its fellow react-dom) as an app dependency, rather than a
development dependency.
$ npm install --save react react-dom
Well also install react router to handle some routing for us as well have multiple routes in our app,
including a map place as well as details page for finding more details about each place well list.
A handy shortcut for installing and saving dependencies with the npm command:
$ npm i -S [dependencies]
In this file, lets create a simple React container to house a single component with some random text.
First, including the dependencies that webpack will bundle in our completed application bundle:
import React from 'react'
import ReactDOM from 'react-dom'
const App = React.createClass({
render: function() {
return (<div>Text text text</div>);
}
});
With the src/app.js in place, lets boot up the server. The hjs-webpack package installs one for us
by default in the ./node_modules directory. We can refer directly to it to start the server:
$ NODE_ENV=development ./node_modules/.bin/hjs-dev-server
Its usually a good idea to explicitly set the NODE_ENV, which we do here.
The server will print out a message about the url we can visit the app at. The default address is at
http://localhost:3000. Well head to our browser (well use Google Chrome) and go to the address
http://localhost:3000.
Although its not very impressive, we have our app booted and running along with our build process.
To stop the devServer, use Ctrl+C.
It can be a pain to remember how to start our development server. Lets make it a tad easier by adding
it as a script to our package.json.
The package.json has an entry that allows us to add scripts, not surprisingly called the scripts
key. Lets go ahead and add a start script in our package.json.
{
"name": "yelp",
"version": "1.0.0",
"description": "",
"scripts": {
"start": "NODE_ENV=development ./node_modules/.bin/hjs-dev-server",
"test": "echo \"Error: no test specified\" && exit 1"
},
/* ... */
}
With the start script configured in the scripts key, instead of using the binary directly we can call
npm run start to start the server.
The start and test scripts in a package.json file are special scripts and with either one of these
defined, we can leave out the run in the npm run start command.
I.e.
$ npm start
postcss
Lets finish o our configuration of our app process by setting up some styles configuration with
postcss and CSS modules.
PostCSS is a pre/post CSS processor. Similar to lesscss and sass, postcss presents a modular
interface for programmatically building CSS stylesheets. The community of plugins and preprocessors
is constantly growing and gives us a powerful interface for building styles.
Setting postcss in our webpack configuration already works and hjs-webpack will already include
one loader if we have it installed, the autoprefixer. Lets go ahead and install the autoprefixer package:
Well use a few other postcss preprocessors in our postcss build chain to modify our CSS. The two
well use is the precss package, which does a fantastic job at gluing a bunch of common postcss
plugins together and cssnano, which does the same for minification and production environments.
$ npm i -D precss cssnano
The hjs-webpack only automatically configures the autoprefixer package, not either one of ours, so in
order to use these two packages, well need to modify our webpack configuration so webpack knows
we want to use them.
At its core, the hjs-webpack tool creates a webpack configuration for us. If we want to extend it or
modify the config it generates, we can treate the return value as a webpack config object. Well modify
the config object returned with any updates to the config object.
The postcss-loader expects to find a postcss key in the webpack config object, so we can just
prepend and append our postcss modifiers to the config.postcss array.
// ...
var config = getConfig({
// ...
})
config.postcss = [].concat([
require('precss')({}),
require('autoprefixer')({}),
require('cssnano')({})
])
Each of the postcss plugins is exported as a function that returns a postcss processor, so
we can have a chance to configure it.
Were not including any modification to the setup here, but its possible.
For documentation on each one, check the documentation for each plugin:
precss
autoprefixer
cssnano
CSS modules
CSS modules are a way for us to interact with CSS definitions inside of JavaScript to avoid one of the
cascading/global styles errr biggest pains in CSS.
In CSS styles, our build script will take care of creating specific, unique names for each style and
modifying the actual name in the style itself. Lets look at it in code.
For instance, lets say we have a css file that includes a single class definition of .container:
.container {
border: 1px solid red;
}
The class of .container is a very generic name and without CSS modules, it would apply to every
DOM object with a class of container. This can lead to a lot of conflicts and unintended styling sideeects. CSS modules allow us to load the style alongside our JavaScript where the style applies and
wont cause a conflict.
To use the .container class above in our <App /> container, we could import it and apply the style
using its name.
import React from 'react'
import ReactDOM from 'react-dom'
import styles from './styles.module.css'
const App = React.createClass({
// ...
});
The styles object above exports an object with the name of the css class as the key and a unique
name for the CSS class as the value.
"container" = "src-App-module__container__2vYsV"
We can apply the CSS class by adding it as a className in our React component as we would any
other prop.
// ...
import styles from './styles.module.css'
const App = React.createClass({
render: function() {
return (
<div className={styles['container']}>
Text text text
</div>
);
}
});
// ...
The hjs-webpack package makes it convenient to build our webpack configuration, but since were
modifying our css loading, well need to not only add a loader (to load our modules), but well need to
modify the existing one.
Lets load the initial loader by finding it in the array of config.module.loaders using a simple
regex:
const matchCssLoaders = /(^|!)(css-loader)($|!)/;
const findLoader = (loaders, match) => {
const found = loaders.filter(l => l &&
l.loader && l.loader.match(match));
return found ? found[0] : null;
}
// existing css loader
const cssloader =
findLoader(config.module.loaders, matchCssLoaders);
With our loader found in the existing module.loaders list, we can create a clone of the loader and add a
new one that targets modules.
It can be convenient to use a global stylesheet. By adding and modifying the existing css
loader in webpack, we can retain the ability to import global styles as well as include css
modules.
Back in our webpack.config.js, lets create a new loader as well as modify the existing loader to
support loading css modules:
// ...
const newloader = Object.assign({}, cssloader, {
test: /\.module\.css$/,
include: [src],
loader: cssloader.loader
.replace(matchCssLoaders,
`$1$2?modules&localIdentName=${cssModulesNames}$3`)
})
config.module.loaders.push(newloader);
cssloader.test =
new RegExp(`[^module]${cssloader.test.source}`)
cssloader.loader = newloader.loader
// ...
In our new loader, weve modified the loading to only include css files in the src directory. For loading
any other css files, such as font awesome, well include another css loader for webpack to load
without modules support:
config.module.loaders.push({
test: /\.css$/,
include: [modules],
loader: 'style!css'
})
Credit for this (slightly modified) technique of loading css modules with webpack and hjswebpack goes to lukekarrys
With our css loading devised in webpack, lets create a single global style in our app at src/app.css.
$ echo "body{ border: 1px solid red;}" > src/app.css
Starting up our server with npm start and refreshing our browser will reveal that global css loading
works as expected.
To confirm our css module loading works as expected, lets create a styles.module.css in our
src/ directory with a single .wrapper css class (for now):
// In src/styles.module.css
.wrapper {
background: blue;
}
Loading our css module file in our src/app.js and applying the .wrapper class to our <div />
component is straightforward with the className prop:
// ...
import './app.css'
import styles from './styles.module.css'
const App = React.createClass({
render: function() {
return (
<div className={styles.wrapper}>
Text text text
</div>
)
}
});
// ...
Loading our server using npm start and refreshing our Chrome window, we see that our css module
style is set from the styles.wrapper class created by the css module.
In our .env file we created at the root of the project, we can set environment variables that we can
build into the project. The dotenv project allows us to load configuration scripts and gives us access to
these variables.
Loading the variables is a pretty simple process using dotenv. In our webpack.config.js file, lets
load up the .env file in our environment:
// ...
const dotenv = require('dotenv');
Our .env file is generally a good spot to place global environment variables. To separate our
environments, well create a mechanism to load those environment variables as well. Generally, well
keep these in a config/ directory as [env].config.js.
To load these files in our server, we can use the same function, except adding a few options to change
the source of the file. In our webpack.config.js file, lets add loading the second environment
variables:
// ...
const NODE_ENV = process.env.NODE_ENV;
const dotenv = require('dotenv');
// ...
const dotEnvVars = dotenv.config();
const environmentEnv = dotenv.config({
path: join(root, 'config', `${NODE_ENV}.config.js`),
silent: true,
});
We can merge these two objects together to allow the environment-based [env].config.js file to
overwrite the global one using Object.assign():
// ...
const dotEnvVars = dotenv.config();
const environmentEnv = dotenv.config({
path: join(root, 'config', `${NODE_ENV}.config.js`),
silent: true,
});
const envVariables =
Object.assign({}, dotEnvVars, environmentEnv);
Our envVariables variable now contains all the environment variables and globally defined
environment variables. In order to reference them in our app, well need to grant access to this
envVariables variable.
Webpack ships with a few common plugins including the DefinePlugin(). The DefinePlugin()
implements a regex that searches through our source and replaces variables defined in a key-value
object, where the keys are the names of variables and their value is replaced in the source before
shipping to the browser.
Its conventional to surround the replaced variable by two underscores (__) on either side of the
variable. For instance, access to the NODE_ENV variable in our source would be referenced by
__NODE_ENV__.
We can programmatically walk through our envVariables and replace each key in the conventional
manner and stringifying their values.
Well want to stringify the values well replace using the DefinePlugin() as they might
contain characters that a browsers JavaScript parser wont recognize. Stringifying these
values helps avoid this problem entirely.
In our webpack.config.js file, lets use the reduce() method to create an object that contains
conventional values in our source with their stringified values:
// ...
const envVariables =
Object.assign({}, dotEnvVars, environmentEnv);
const defines =
Object.keys(envVariables)
.reduce((memo, key) => {
const val = JSON.stringify(envVariables[key]);
memo[`__${key.toUpperCase()}__`] = val;
return memo;
}, {
__NODE_ENV__: JSON.stringify(NODE_ENV)
});
The defines object can now become the configuration object that the DefinePlugin() plugin
expects to use to replace variables. Well prepend the existing webpack plugin list with our
DefinePlugin():
// ...
const defines =
// ...
config.plugins = [
new webpack.DefinePlugin(defines)
].concat(config.plugins);
// ...
Checking to see if the replacement is working as we expect, we can set our <App /> component to
display these variables (as JavaScript strings). For instance, to see the environment using the
__NODE_ENV__ value, we can modify the render() function:
//...
const App = React.createClass({
render: function() {
return (
<div className={styles.wrapper}>
<h1>Environment: {__NODE_ENV__}</h1>
</div>
)
}
});
// ...
Kicking up our server using npm start and refreshing our browser, well see that the value has been
replaced by the string development (as set by NODE_ENV=development in our start script).
Font Awesome
In our app, well use Font Awesome to display rating stars. Weve already handled most of the work
required to get font awesome working. Well just need to install the font-awesome dependency and
require the css in our source.
Installing the dependency is straightforward using npm:
$ npm i -S font-awesome
To use the fonts in font-awesome, we just need to apply the proper classes as described in the font
awesome docs after we require the css in our source.
Requiring the font-awesome css in our source is pretty easy. Since well use this across components,
we can require it in our main src/app.js:
import React from 'react'
import ReactDOM from 'react-dom'
import 'font-awesome/css/font-awesome.css'
// ...
Using font-awesome in our react components is like using font-awesome outside of react, placing the
right css classes. To add a star to our <App /> component from font-awesome, we can modify our
render() function:
// ...
import 'font-awesome/css/font-awesome.css'
// ...
const App = React.createClass({
render: function() {
return (
<div className={styles.wrapper}>
<h1>
<i className="fa fa-star"></i>
Environment: {__NODE_ENV__}</h1>
</div>
)
}
});
Reloading our browser, we can see that the font-awesome css has loaded correctly and is displaying
the star icon from the font-awesome icon library:
Demo: Environment
Webpack Tip: Relative requires
As were using webpack to package our app, we can use it to make packaging our relative requires
simpler. Rather than requiring files relative to the directory that the current file is located in, we can
require them using an alias.
Lets add the webpack root to be both the node_modules/ directory as well as the src/ directory.
We can also set up a few aliases referencing the directories we previously created:
var config = getConfig({
// ...
})
// ...
config.resolve.root = [src, modules]
config.resolve.alias = {
'css': join(src, 'styles'),
'containers': join(src, 'containers'),
'components': join(src, 'components'),
'utils': join(src, 'utils')
}
In our source, instead of referencing our containers by relative path, we can simply call
require('containers/SOME/APP').
Configuring Testing
React oers a wide range of methods of testing that our application is working as we expect it to work.
Weve been opening the browser and refreshing the page (although, hot-reloading is set up, so even
refreshing the page isnt a requirement).
Although developing with such rapid feedback is great and oers convenience at development time,
writing tests to programmatically test our application is the quickest, most reliable way to ensure our
app works as we expect it to work.
Most of the code we will write in this section will be test-driven, meaning well implement the test first
and then fill out the functionality of our components. Lets make sure that we can test our code.
Although the react team uses jest (and we cover it in-depth in fullstackreact), well be using a
combination of tools:
karma is our test runner
chai is our expectation library
mocha as our test framework
enzyme as a react testing helper
sinon as a spy, stub, and moch framework
Lets start by installing our testing dependencies. Well install the usual suspects, plus a babel polyfill
so we can write our tests using ES6.
$ npm i -D mocha chai enzyme chai-enzyme expect sinon babel-register babel-polyfill
react-addons-test-utils
Well be using a library called enzyme to make testing our react components a bit easier and for fun to
write. In order to set it up properly, however, we will need to make a modification to our webpack
setup. Well need to install the json-loader to load json files along with our javascript files (hjswebpack automatically configures the json loader for us, so we wont need to handle updating the
webpack configuration manually):
$ npm i -D json-loader
Well be using karma to run our tests, so well need to install our karma dependencies. Well use karma
as its a good compliment to webpack, but it does require a bit of setup.
Karma has a fast testing iteration, it includes webpack compiling, runs our tests through babel, and
mounts our testing environment in a browser just the same as though we are testing it in our own
browser. Additionally, it is well supported and has a growing community working with karma. It makes
it a good candidate for us to use together with our webpack build pipeline.
Lets install the dependencies for karma:
$ npm i -D karma karma-chai karma-mocha karma-webpack karma-phantomjs-launcher
phantomjs-prebuilt phantomjs-polyfill
$ npm i -D karma-sourcemap-loader
Well be using PhantomJS to test our files so we dont actually need to launch a browser
with a window. PhantomJS is a headless, WebKit-driven, scriptable browser with a JS API
and allows us to run our tests in the background.
If you prefer to use Google Chrome to run the tests with a window, swap out karmaphantomjs-launcher with karma-chrome-launcher and dont update the config
below.
Grab a cup of tea to let these install (phantom can take a little while to install). Once they are ready,
well need to create two config files to both configure karma as well as the tests well have karma
launch.
Lets set up our webpack testing environment through karma. The easiest way to get started with
karma is by using the karma init command:
$ karma init
After we answer a few questions, it will spit out a karma.conf.js file. Since were going to
manipulate most of this file, its a good idea to just press enter on all of the questions to have it
generate the file for us.
Alternatively, we can touch the file as we have done with other files and recreate the file:
$ touch karma.conf.js
With our karma.conf.js file generated, well need to give it a few configuration options, most of
which are autogenerated or we have already set up.
First, the basics. Well use some default options that karma has spit out for us automatically:
module.exports = function(config) {
config.set({
// ...
basePath: '',
preprocessors: {},
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
browsers: ['Chrome'],
concurrency: Infinity,
plugins: []
});
}
Well need to tell karma that we want to use mocha and chai as the testing framework, instead of the
default jasmine framework, so lets change the frameworks: [] option. Well also need to add these
to the plugins karma will use.
module.exports = function(config) {
config.set({
frameworks: ['mocha', 'chai'],
basePath: '',
plugins: [
'karma-mocha',
'karma-chai'
],
// ...
});
}
As were using webpack to compile our files together, well also need to tell karma about our webpack
configuration. Since we already have one, there is no need to recreate it, well just require our original
one.
var webpackConfig = require('./webpack.config');
module.exports = function(config) {
config.set({
frameworks: ['mocha', 'chai'],
webpack: webpackConfig,
webpackServer: {
noInfo: true
},
// ...
});
}
Well also need to tell karma how to use webpack in its confgiuration. We can do this by setting the
karma-webpack plugin in its plugin list.
module.exports = function(config) {
config.set({
frameworks: ['mocha', 'chai'],
webpack: webpackConfig,
webpackServer: {
noInfo: true
},
plugins: [
'karma-mocha',
'karma-chai',
'karma-webpack'
],
// ...
});
}
Lets change the reporter to something a tad nicer (the spec-reporter, instead of the default
progress) reporter and change the browser from Chrome to PhantomJS (adding the proper plugins).
First, lets install the spec reporter:
$ npm i -D karma-spec-reporter
Back in our karma.conf.js file, lets add the plugins and change the browsers and plugins:
module.exports = function(config) {
config.set({
reporters: ['spec'],
plugins: [
'karma-mocha',
'karma-chai',
'karma-webpack',
'karma-phantomjs-launcher',
'karma-spec-reporter'
],
browsers: ['PhantomJS']
// ...
});
}
Finally, we need to test karma where to find the files it will run as our tests. Instead of pointing it to the
actual tests, well use a middleman, a webpack config to tell Karma where to find the tests and
package them together.
module.exports = function(config) {
config.set({
files: [
'tests.webpack.js'
],
// ...
})
}
Before we move on, well need to let karma know that it needs to run our tests.webpack.js file
through the webpack preprocessor. Well also ask it to run it through a sourcemap preprocessor to spit
out usable sourcemaps (so we can debug our code eectively):
module.exports = function(config) {
config.set({
files: [
'tests.webpack.js'
],
preprocessors: {
'tests.webpack.js': ['webpack', 'sourcemap']
},
plugins: [
'karma-mocha',
'karma-chai',
'karma-webpack',
'karma-phantomjs-launcher',
'karma-spec-reporter',
'karma-sourcemap-loader'
],
// ...
});
}
Lets create the tests.webpack.js file. This file will serve as middleware between karma and
webpack. Karma will use this file to load all of the spec files, compiled through webpack.
The file is fairly simple:
require('babel-polyfill');
var context = require.context('./src', true, /\.spec\.js$/);
context.keys().forEach(context);
When karma executes this file, it will look through our src/ directory for any files ending in .spec.js
and execute them as tests. Here, we can set up any helpers or global configuration well use in all of
our tests.
Since were going to be using a helper called chai enzyme, we can set our global configuration up
here:
require('babel-polyfill');
// some setup first
var chai = require('chai');
var chaiEnzyme = require('chai-enzyme');
chai.use(chaiEnzyme())
var context = require.context('./src', true, /\.spec\.js$/);
context.keys().forEach(context);
Up through this point, our complete karma conf file should look like this:
var path = require('path');
var webpackConfig = require('./webpack.config');
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['mocha', 'chai'],
files: [
'tests.webpack.js'
],
preprocessors: {
// add webpack as preprocessor
'tests.webpack.js': ['webpack', 'sourcemap'],
},
webpack: webpackConfig,
webpackServer: {
noInfo: true
},
plugins: [
'karma-mocha',
'karma-chai',
'karma-webpack',
'karma-phantomjs-launcher',
'karma-spec-reporter',
'karma-sourcemap-loader'
],
reporters: ['spec'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
browsers: ['PhantomJS']
})
};
Weve covered almost the entire karma setup, but were missing two final pieces. Before we complete
the karma setup, lets create a sample test file so we can verify our test setup is complete.
Instead of placing a sample file in the root (only to move it later), lets place it in its final spot. Were
going to use the <App /> component we created earlier as a container for the rest of our app. Well
create a spec file in the containers/App/App.spec.js file.
$ mkdir src/containers/App && touch src/containers/App.spec.js
In here, lets create a simple test that tests for the existence of an element with a custom wrapper style
class (from our CSS modules).
Without going in-depth to writing tests (yet), this simple test describes our intention using mocha and
chai.
import React from 'react'
import { expect } from 'chai'
import { shallow } from 'enzyme'
import App from './App'
import styles from './styles.module.css'
describe('<App />', function () {
let wrapper;
beforeEach(() => {
wrapper = shallow(<App />)
})
it('has a single wrapper element', () => {
expect(wrapper.find(`.${styles.wrapper}`))
.to.have.length(1);
});
});
We walk through testing our app later in this course. For the time being, feel free to copy
and paste the code into your own file to get us through setting up our build/test workflow.
To get this test running, well need to create two more files from the previous test. The
src/containers/App.js file along with the custom CSS module
src/containers/styles.module.css. We dont need to make our tests pass, initially, just get
them running.
Lets create the App.js file and move our original src/styles.module.css into the container
directory:
$ touch src/containers/App/App.js
$ mv src/styles.module.css \
src/containers/App/styles.module.css
Lets go ahead and move our <App /> definition from src/app.js into this new file at
src/containers/App/App.js:
import React from 'react'
import ReactDOM from 'react-dom'
Finally, well need to import the <App /> component from the right file in our src/app.js:
// ...
import './app.css'
import App from 'containers/App/App'
const mountNode = document.querySelector('#root');
ReactDOM.render(<App />, mountNode);
To execute our tests, well use the karma command installed in our ./node_modules directory by
our previous npm install:
$ NODE_ENV=test \
./node_modules/karma/bin/karma start karma.conf.js
Uh oh! We got an error. Do not worry, we expected this dont look behind the curtain
This error is telling us two things. The first is that webpack is trying to find our testing framework and
bundle it in with our tests. Webpacks approach to bundling is using a static file analyzer to find all the
dependencies were using in our app and to try to bundle those along with our source. As enzyme
imports some dynamic files, this approach doesnt work.
Obviously we dont want to do this as we dont need to bundle tests with our production framework.
We can tell webpack to ignore our testing framework and assume that its available for us by setting it
as an external dependency.
In our webpack.config.js file, lets set a few external dependencies that enzyme expects:
// ./webpack.config.js
// ...
var config = getConfig({
isDev,
in: join(src, 'app.js'),
out: dest,
clearBeforeBuild: true
});
config.externals = {
'react/lib/ReactContext': true,
'react/lib/ExecutionEnvironment': true
}
// ...
The second error weve encountered is that our testing framework is that a few of our production
webpack plugins are mucking with our tests. Well need to exclude a few plugins when were running
webpack under a testing environment. Since were now handling two cases where testing with
webpack diers from production or development, lets create a conditional application for our webpack
testing environment.
First, we can tell if we are in a testing environment by checking to see if the command we are issuing is
karma OR by checking the NODE_ENV is test. At the top of our webpack.config.js file, lets set
our variable isTest:
require('babel-register');
const NODE_ENV = process.env.NODE_ENV;
const isDev = NODE_ENV === 'development';
const isTest = NODE_ENV === 'test';
// ...
Later in our config file, we can manipulate our config under testing environments vs. dev/production
environments.
Moving our previous externals definition into this conditional statement and excluding our
production plugins, our updated webpack.config.js file:
// ./webpack.config.js
// ...
var config = getConfig({
// ...
});
if (isTest) {
config.externals = {
'react/lib/ReactContext': true,
'react/lib/ExecutionEnvironment': true
}
config.plugins = config.plugins.filter(p => {
const name = p.constructor.toString();
const fnName = name.match(/^function (.*)\((.*\))/)
const idx = [
'DedupePlugin',
'UglifyJsPlugin'
].indexOf(fnName[1]);
return idx < 0;
})
}
// ...
Now, if we run our tests again, using karma well see that our tests are running, they are just not
passing yet.
$ NODE_ENV=test \
./node_modules/karma/bin/karma start karma.conf.js
Instead of passing the previous command, we can run our tests with npm test:
$ npm test
Our previous test is not passing because although we have defined a .wrapper{} class in our
src/containers/App/styles.module.css file, its empty so webpack just discards the class and
styles.wrapper ends up undefined. In order to get our test passing, well need to add a description
to it.
.wrapper {
display: flex;
}
Well be using the flexbox layout in our app, so we can use display: flex; in our css description.
Running our tests again, using npm test this time, we can see that our test goes all green (i.e.
passes).
It can be a tad painful when flipping back and forth between our terminal and code windows. It would
be nice to have our tests constantly running and reporting any failures instead. Luckily karma handles
this easily and so can we.
Well use a command-line parser to add an npm script to tell karma to watch for any file changes. In
our package.json file, lets add the test:watch command:
{
"name": "yelp",
"version": "1.0.0",
"description": "",
"scripts": {
"start": "./node_modules/.bin/hjs-dev-server",
"test": "NODE_ENV=test ./node_modules/karma/bin/karma start karma.conf.js",
"test:watch": "npm run test -- --watch"
},
// ...
}
Instead of using npm test, well launch our test watcher by using npm run test:watch. Well also
need to tell karma (by using our karma.conf.js config file) that we want it to watch for any changes
to our files.
Karma handles this out of the box for us by using the singleRun key in its configuration object. We
can set this using a command-line parser called yargs. Lets install yargs as a development
dependency:
$ npm i -D yargs
In our karma.conf.js file, we can check for the --watch flag using yargs and set the singleRun
option based upon the existence of the --watch flag.
var argv = require('yargs').argv;
// ...
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['mocha', 'chai'],
// ...
singleRun: !argv.watch
});
}
Now, when we execute the npm run test:watch script and modify and save a file, our tests will be
executed, making for easy, fast test-driven development.
In src/contains/App.spec.js, lets build the beginning of our jasmine spec. First, well need to
include our libraries and the App component itself:
import { expect } from 'chai'
import { shallow } from 'enzyme'
import App from './App'
describe('<App />', () => {
// define our tests in here
})
Well use expect() to set up our expectations and shallow() (from enzyme) to render our elements
into the test browser.
With our test set up, we can test that our <App /> component contains a Router component by
using the find() method in our wrapper instance. To set the expectation in our component, well use
expect():
// ...
describe('<App />', () => {
// ...
it('has a Router component', () => {
expect(wrapper.find('Router'))
.to.have.length(1);
})
})
Finally, we can run our tests by using our npm script we previously built:
$ npm run test
Since we havent implemented the <App /> component, our test will fail. Lets turn our test green.
Routing
Before we implement our routes, lets take a quick look at how well set up our routing.
When we mount our react app on the page, we can control where the routes appear by using the
children to situate routes where we want them to appear. In our app, well have a main header bar
that well want to exist on every page. Underneath this main header, well switch out the content for
each individual route.
Well place a <Router /> component in our app as a child of the component with rules which
designate which children should be placed on the page at any given route. Thus, our <App />
component weve been working with will simply become a container for route handling, rather than an
element to hold/display content.
Although this approach sounds complex, its an ecient method for holding/generating routes on a
per-route basis. It also allows us to create custom data handlers/component generators which come in
handy for dealing with data layers, such as Redux.
With that being said, lets move on to setting up our main views.
In our src/containers/App.js, lets make sure we import the react-router library.
import React, { PropTypes } from 'react';
import { Router } from 'react-router';
// ...
Next, in our usual style, lets build our React component (either using the createClass({}) method
we used previously or using the class-based style, as well switch to here):
import React, { PropTypes } from 'react';
import { Router } from 'react-router';
class App extends React.Component {
render() {
return (<div>Content</div>)
}
}
We like to include our content using the classical getter/setter method, but this is only a personal
preference.
import React, { PropTypes } from 'react'
import { Router } from 'react-router'
Well use our app container to return an instance of the <Router /> component. The <Router />
component require us to pass a history object which tells the browser how to listen for the location
object on a document. The history tells our react component how to route.
There are multiple dierent types of history objects we can use, but the two most popular types are the
browserHistory and hashHistory types. The browserHistory object uses the native html5
react router to give us routes that appear to be server-based.
On the other hand, the hashHistory uses the # sign to manage navigation. Hash-based history, an
old trick for client-side routing is supported in all browsers.
Well use the browserHistory method here. We need to tell the <Router /> instance we want to
use the browserHistory by passing it as a prop in our routing instance:
import React, { PropTypes } from 'react';
import { Router } from 'react-router';
class App extends React.Component {
static propTypes = {
history: PropTypes.object.isRequired
}
// class getter
get content() {
return (
<Router history={this.props.history} />
)
}
render() {
return (
<div style=\{\{ height: '100%' \}\}>
{this.content}
</div>
)
}
}
Were almost ready to place our routes on the page, we just have to pass in our custom routes (well
make them shortly). Well wrap our routes into this <App /> component:
import React, { PropTypes } from 'react';
import { Router } from 'react-router';
class App extends React.Component {
static propTypes = {
routes: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
}
// class getter
get content() {
return (<Router
routes={this.props.routes}
history={this.props.history} />)
}
render() {
return (
<div style=\{\{ height: '100%' \}\}>
{this.content}
</div>
)
}
}
In order to actually use our <App /> component, well need to pass through the two props the
component itself expects to receive when we render the <App /> component:
history - well import the browserHistory object from react router and pass this export directly.
routes - well send JSX that defines our routes
Back in our src/app.js file, well pass through the history directly as we import it.
import React from 'react'
import ReactDOM from 'react-dom'
import 'font-awesome/css/font-awesome.css'
import './app.css'
Lastly, well need to build some routes. For the time being, lets get some data in our browser. Lets
show a single route just to get a route showing up. Well revise this shortly.
To build our routes, we need access to the:
<Router /> component
<Route /> component
Our custom route components.
The Router and Route component can be imported directly from react-router:
import React from 'react'
import ReactDOM from 'react-dom'
import 'font-awesome/css/font-awesome.css'
import './app.css'
import {browserHistory, Router, Route} from 'react-router'
import App from 'containers/App/App'
const mountNode = document.querySelector('#root');
ReactDOM.render(
<App history={browserHistory} />, mountNode);
We can create our custom route by building a JSX instance of the routes using these two components:
// ...
import './app.css'
import {browserHistory, Router, Route} from 'react-router'
const routes = (
<Router>
<Route path="/" component={Home} />
</Router>
)
//...
Since we havent yet defined the Home component above, the previous example fails, so we can create
a really simple to prove it is working:
// ...
import './app.css'
import {browserHistory, Router, Route} from 'react-router'
const Home = React.createClass({
render: function() {
return (<div>Hello world</div>)
}
})
const routes = (
<Router>
<Route path="/" component={Home} />
</Router>
)
//...
Finally, we can pass the routes object into our instance of <App /> and refreshing our browser.
Provided we havent made any major typos, well see that our route has resolved to the root route and
Hello world is rendered to the DOM.
We also see that our tests pass as the <App /> component now has a single <Router />
component being rendered as a child.
In this src/routes.js file, lets create and export a function to create and export the routes JSX
instance rather. Lets copy and remove the instances from the src/app.js file and into our new
routes.js file. Moving the contents from src/routes.js should leave the routes file as:
import React from 'react'
import {browserHistory, Router, Route, Redirect} from 'react-router'
const Home = React.createClass({
render: function() {
To use this in our src/app.js file, we can replace our routes definition and call the exported
function makeRoutes:
import React from 'react'
import ReactDOM from 'react-dom'
import 'font-awesome/css/font-awesome.css'
import './app.css'
import App from 'containers/App/App'
import {browserHistory} from 'react-router'
import makeRoutes from './routes'
const routes = makeRoutes()
const mountNode = document.querySelector('#root');
ReactDOM.render(
<App history={browserHistory}
routes={routes} />, mountNode);
Lets confirm our <App /> is still running as we expect by using npm run test. If we dont make any
typos, our app should still render in the browser.
Lets make a new directory in our root src directory well call views with a single directory in it with the
name of the route well be building. For lack of a better name: Main/:
$ mkdir -p src/views/Main
To get things started, lets add a single route for the Container in our src/views/Main/routes.js
file. The routes.js file can simply contain a route definition object just as though it is a top level
routes file.
import React from 'react'
import {Route} from 'react-router'
import Container from './Container'
export const makeMainRoutes = () => {
return (
When we import this routes file into our main routes file, well define some children elements, but for
the time being, to confirm the set-up is working as we expect it, well work with a simple route
container element. The container element can be as simple as the following:
// in src/views/Main/Container.js
import React from 'react'
export class Container extends React.Component {
render() {
return (
<div>
Hello from the container
</div>
)
}
}
export default Container
With our containing element defined, lets flip back to our src/routes.js file to include our new subroutes file. Since we exported a function, not an object, well need to make sure we display the return
value of the function rather than the function itself.
Modifying our initial src/routes.js file, to both remove the Home container definition and import our
new routes, our src/routes.js file can look more akin to the following:
import React from 'react'
import {browserHistory, Router, Route, Redirect} from 'react-router'
import makeMainRoutes from './views/Main/routes'
export const makeRoutes = () => {
const main = makeMainRoutes();
return (
<Route path=''>
{main}
</Route>
)
}
export default makeRoutes
Since were defining sub-routes in our application, we wont need to touch the main routes.js file
much for the rest of this application. We can follow the same steps to add a new top-level route.
Routing to Maps
Before we jump too far ahead, lets get our <Map /> component on the page. In a previous article, we
built a <Map /> component from the ground-up, so well be using this npm module to generate our
map. Check out this in-depth article at {{ name }}.
Lets install this npm module called google-maps-react:
$ npm install --save google-maps-react
In our <Container /> component, well place an invisible map on the page. The idea behind an
invisible map component is that our google map will load the Google APIs, create a Google Map
instance and will pass in on to our children components, but wont be shown in the view. This is good
for cases where we want to use the Google API, but not necessarily need to show a map at the same
time. Since well be making a list of places using the Google Places API, well place an invisible <Map
/> component on screen.
Before we can use the <Map /> component, well need to grab a google API key. In our webpack set
up, were using the WebpackDefinePlugin() to handle replacing variables in our source, so we can
set our google key as a variable in our .env file and it will just work.
For information on how to get a Google API key, check out the {{ name }} article.
In our /.env file, lets set the GAPI_KEY to our key:
GAPI_KEY=abc123
To use our GAPI_KEY, well reference it in our code surrounded by underscores (i.e.: __GAPI_KEY__).
Before we can start using the <Map /> component, well need to wrap our <Container />
component in the GoogleApiWrapper() higher-order component. This HOC gives us access to the
lazily-loaded google api and pass through a google prop which references the object loaded by the
google script.
import React from 'react'
import Map, {GoogleApiWrapper} from 'google-maps-react'
export class Container extends React.Component {
// ...
}
export default GoogleApiWrapper({
apiKey: __GAPI_KEY__
})(Container)
Now, when we load the <Container /> component on the page, the wrapper takes care of loading
the google api along with our apiKey.
With the google API loaded, we can place a <Map /> component in our <Container /> component
and it will just work. Lets make sure by placing a <Map /> instance in our component:
The only requirement the <Map /> component needs is the google prop, so we can add the <Map
/> component directly in our render code:
import React from 'react'
import Map, {GoogleApiWrapper} from 'google-maps-react'
export class Container extends React.Component {
render() {
return (
<div>
Hello from the container
<Map
google={this.props.google} />
</div>
)
}
}
onReady={this.onReady.bind(this)}
google={this.props.google} />
</div>
)
}
}
From here, we can use the google API as though we arent using anything special. Well create a helper
function to run the google api command. Lets create a new file in our src/utils directory called
googleApiHelpers.js. We can nest all our Google API functions in here to keep them in a common
place. We can return a promise from our function so we can use it regardless of the location:
export function searchNearby(google, map, request) {
return new Promise((resolve, reject) => {
const service = new google.maps.places.PlacesService(map);
service.nearbySearch(request, (results, status, pagination) => {
if (status == google.maps.places.PlacesServiceStatus.OK) {
resolve(results, pagination);
} else {
reject(results, status);
}
})
});
}
Now, within our container we can call this helper along with the maps and store the return from the
google request within the onReady() prop function for our <Map /> component.
import {searchNearby} from 'utils/googleApiHelpers'
export class Container extends React.Component {
onReady(mapProps, map) {
const {google} = this.props;
const opts = {
location: map.center,
radius: '500',
types: ['cafe']
}
searchNearby(google, map, opts)
.then((results, pagination) => {
// We got some results and a pagination object
}).catch((status, result) => {
// There was an error
})
}
render() {
return (
<div>
<Map
onReady={this.onReady.bind(this)}
google={this.props.google} />
</div>
)
}
}
Since well be storing a new state of the <Container /> so we can stave the new results in our
<Container />, lets set it to be stateful:
export class Container extends React.Component {
constructor(props) {
super(props);
this.state = {
places: [],
pagination: null
}
}
// ...
Now, when we fetch successful results, we can instead set some state on the local <Container />
to hold on to the results fetched from Google. Updating our onReady() function with setState:
export class Container extends React.Component {
onReady(mapProps, map) {
const {google} = this.props;
const opts = {
location: map.center,
radius: '500',
types: ['cafe']
}
searchNearby(google, map, opts)
.then((results, pagination) => {
this.setState({
places: results,
pagination
})
}).catch((status, result) => {
// There was an error
})
}
// ...
}
Now, we can update our render() method by listing the places fetch we now have in our state:
export class Container extends React.Component {
// ...
render() {
return (
<div>
Hello from the container
<Map
google={this.props.google}
onReady={this.onReady.bind(this)}
visible={false}>
{this.state.places.map(place => {
return (<div key={place.id}>{place.name}</div>)
})}
</Map>
</div>
)
}
}
Creating a Sidebar
With our listing of places in-hand, lets move on to actually turning our app into a closer to yelp-like
component. In this section, were going to use turn our app into something that looks a little stylish
and add some polish.
In order to build this part of the app, were going to add some inline styling and use the natural React
props flow.
First, lets install an npm module called classnames. The README.md is a fantastic resource for
understanding how it works and how to use it. Were going to use it to combine classes together (this
is an optional library), but useful, regardless.
$ npm install --save classnames
Now, lets get to breaking out our app into components. First, lets build a <Header /> component to
wrap around our app.
As were building a shared component (rather than one specific to one view), a natural place to build
the component would be the the src/components/Header directory. Lets create this directory and
create the JS files that contain the component and tests:
$ mkdir src/components/Header
$ touch src/components/Header/{Header.js,Header.spec.js}
Our <Header /> component can be pretty simple. All well use it for is to wrap the name of our app
and possibly contain a menu (although we wont build this here). As were building our test-first app,
lets write the spec that reflects the <Header /> purpose first:
In the src/components/Header.spec.js file, lets create the specs:
import React from 'react'
import { expect } from 'chai'
import { shallow } from 'enzyme'
import Header from './Header'
describe('<Header />', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<Header />)
});
// These show up as pending tests
it('contains a title component with yelp');
it('contains a section menu with the title');
})
The tests themselves are pretty simple. Well simply expect for the text we expect:
import React from 'react'
import { expect } from 'chai'
import { shallow } from 'enzyme'
import Header from './Header'
describe('<Header />', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<Header />)
});
it('contains a title component with yelp', () => {
expect(wrapper.find('h1').first().text())
.to.equal('Yelp')
});
it('contains a section menu with the title', () => {
expect(wrapper.find('section').first().text())
.to.equal('Fullstack.io')
});
})
Running our tests at this point will obviously fail because we havent yet written the code to make them
pass. Lets get our tests going green.
Since the full component itself is pretty straightforward, nearly the entire implementation (without
styles) can be summed to:
import React from 'react'
import {Link} from 'react-router'
export class Header extends React.Component {
render() {
return (
<div>
<Link to="/"><h1>Yelp</h1></Link>
<section>
Fullstack.io
</section>
</div>
)
}
}
export default Header
But loading the browser, the styles dont look quite right yet Lets see it in the browser by making
sure we import our new component into our main view and using in the render() function of our
main <Container /> component. In src/views/Main/Container.js:
// our webpack alias allows us to reference `components`
// relatively to the src/ directory
import Header from 'components/Header'
// ...
export class Container extends React.Component {
// ...
render() {
return (
<div>
<Map
visible={false}>
<Header />
{/* ... */}
</Map>
</div>
)
}
}
Inlining Styles
Adding styles to our <Header /> component is pretty straightforward. Without CSS modules, we can
create a specific CSS identifier, include a global CSS stylesheet, and applying it to the <Header />
component. Since were using CSS modules, lets create a CSS module to handle the header bar
component.
We can create a CSS module by naming a CSS stylesheet with the sux: .module.css (based upon
our webpack configuration). Lets create a stylesheet in the same directory as our Header.js:
$ touch src/components/Header/styles.module.css
Lets create a single class well call topbar. Lets add a border around it so we can see the styles
being applied in the browser:
/* In `src/components/Header/styles.module.css `*/
.topbar {
border: 1px solid red;
}
We can use this style by importing it into our <Header /> component and applying the specific
classname from the styles:
import React from 'react'
import {Link} from 'react-router'
import styles from './styles.module.css';
export class Header extends React.Component {
render() {
return (
<div className={styles.topbar}>
{/* ... */}
</div>
)
}
}
By setting the styles.topbar class to the <Header /> component, the styles will be applied to the
component. With our border in the styles.topbar class, well see we have a red border around our
topbar:
Lets go ahead and add a fixed style to make it look a little nicer:
/* In `src/components/Header/styles.module.css `*/
.topbar {
position: fixed;
z-index: 10;
top: 0;
left: 0;
background: #48b5e9;
width: 100%;
padding: 0 25px;
height: 80px;
line-height: 80px;
color: #fff;
a {
text-transform: uppercase;
text-decoration: none;
letter-spacing: 1px;
line-height: 40px;
h1 { font-size: 28px; }
}
section {
position: absolute;
top: 0px;
right: 25px;
}
}
Lets also add a few styles to the main app.css (some global styles to change the layout style and
remove global padding). Back in src/app.css, lets add a few styles to help make our top bar look
decent:
*,
*:after,
*:before {
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-size: 16px;
}
body {
color: #333333;
font-weight: lighter;
font: 400 15px/22px 'Open Sans', 'Helvetica Neue', Sans-serif;
font-smoothing: antialiased;
padding: 0;
margin: 0;
}
At this point, we can say that our header looks decent. However, with the way that we have it styled
now, the content runs through the topbar. We can fix this by adding some styles to our content.
Lets create a content css modules in our src/views/Main/styles.module.css directory that our
<Container /> component will use.
$ touch src/views/Main/styles.module.css
In here, we can wrap our entire container with a class and add a content class, etc. In our new
styles.module.css class, lets add the content classes:
.wrapper {
overflow-y: scroll;
display: flex;
margin: 0;
padding: 15px;
height: 100vh;
-webkit-box-orient: horizontal;
-o-box-orient: horizontal;
}
.content {
position: relative;
flex: 2;
top: 80px;
}
Setting flex: 2 sets our content box as the larger of the two elements (sidebar vs
content). Well come back and look at this in more depth at the end of this article.
In the same fashion as we did before, we can now import these container styles with the <Container
/> component. In our <Container /> component, lets add a few styles around the elements on the
page:
import styles from './styles.module.css'
export class Container extends React.Component {
// ...
render() {
return (
<Map
visible={false}
className={styles.wrapper}>
<Header />
<div className={styles.content}>
{/* contents */}
</div>
</Map>
</div>
)
}
}
CSS Variables
Notice above how we have some hardcoded values, such as the height in our topbar class.
/* In `src/components/Header/styles.module.css `*/
.topbar {
/* ... */
background: #48b5e9;
height: 80px;
line-height: 80px;
/* ... */
}
One of the valuable parts of using postcss with the postcss pipeline weve set up is that we can use
variables in our css. There are a few dierent ways to handle using variables in CSS, but well use the
method that is built into postcss syntax.
To add a custom property (variable), we can prefix the variable name with two dashes (-) inside of a
rule. For instance:
.topbar {
--height: 80px;
}
Note, when creating a custom property (which in our case is a variable), we need to place it
inside a CSS rule.
We can then use the --height property/variable in our css by using the var() notation. In our case,
we can then replace the height properties in our topbar class:
/* In `src/components/Header/styles.module.css `*/
.topbar {
--height: 80px;
/* ... */
background: #48b5e9;
height: var(--height);
line-height: var(--height);
/* ... */
}
Although this is all well and good, were limited to using this --height variable to the .topbar class
at this point. Instead, if we want to use this height in say, the content class, well need to put the
variable in a higher DOM selector.
We could place the variable in the wrapper class, for instance, but postcss has a more clever way of
handling variable notation. We can place the variable declaration at the root node of the CSS using the
:root selector:
/* In `src/components/Header/styles.module.css `*/
:root {
--height: 80px;
}
.topbar {
/* ... */
height: var(--height);
line-height: var(--height);
/* ... */
}
Since well likely want to use this within another css module, we can place this variable declaration in
another common css file that both modules can import. Lets create a root directory for our CSS
variables to live inside, such as src/styles/base.css:
$ touch src/styles/base.css
To use this variable in our multiple css modules, well import it at the top of our CSS module and use it
as though it has been declared at the top of the local file.
Back in our src/components/Header/styles.module.css, we can replace the :root definition
with the import line:
@import url("../../styles/base.css");
/* ... */
.topbar {
/* ... */
height: var(--height);
line-height: var(--height);
/* ... */
}
Now, we have one place to change the height of the topbar and still have the variable cascade across
our app.
Splitting Up Components
With our topbar built, we can move to building the routes of our main application. Our app contains a
sidebar and a changable content area that changes views upon changing context. That is, well show a
map to start out with and when our user clicks on a pin, well change this to show details about the
location the user has clicked on.
Lets get started with the sidebar component that shows a listing of the places. Weve done the
hardwork up through this point. The React Way dictates that we define a component that uses a
components to display the view (components all the way down weeee). What this literally entails is
that well build a <Listing /> component that lists individual listing items. This way we can focus on
the detail for each item listing rather than needing to worry about it from a high-level.
Lets take our listing from a simple list to a <Sidebar /> component. Since well create the
component as a shared one, lets add it in the src/components/Sidebar directory. Well also create
a JavaScript file and the CSS module:
$ mkdir src/components/Sidebar
$ touch src/components/Sidebar/{Sidebar.js,styles.module.css}
The <Sidebar /> component will be a fairly simple component that we can use to contain the listing
of places and a wrapper for a sticky sidebar. Lets place it in our <Container /> view initially, so we
can get it listed in the view. In our src/views/Main/Container.js file, lets import this new
<Sidebar /> component (yet to be built). Lets import it and place it in our render() function:
import Sidebar from 'components/Sidebar/Sidebar'
export class Container extends React.Component {
// ...
render() {
return (
<Map
visible={false}
className={styles.wrapper}>
<Header />
<Sidebar />
{/* contents */}
</Map>
</div>
)
}
}
Well also want to pass a few props into the <Sidebar /> component. Since it is responsible for
listing our places, well definitely need to pass in the list of places we fetch from the google api. As of
now, its stored in the state of the <Container />. Well come back to passing through a click
handler (that gets called when a list item is clicked). Modifying our implementation, lets pass through
these props:
import Sidebar from 'components/Sidebar/Sidebar'
export class Container extends React.Component {
// ...
render() {
return (
<Map
visible={false}
className={styles.wrapper}>
<Header />
<Sidebar
title={'Restaurants'}
places={this.state.places}
/>
{/* contents */}
</Map>
</div>
)
}
}
With our <Sidebar /> component in the <Container />, lets start building our <Sidebar />.
The component will mostly be used as a container to hold styles and dynamic data.
import React, { PropTypes as T } from 'react'
import styles from './styles.module.css'
export class Sidebar extends React.Component {
render() {
return (
<div className={styles.sidebar}>
<div className={styles.heading}>
<h1>{this.props.title}</h1>
</div>
</div>
)
}
}
export default Sidebar
Lets also make sure we add the appropriate top positioning such that it isnt rendered underneath the
topbar. Lets create the styles in src/components/Sidebar/styles.module.css and add some
styles:
@import url("../../styles/base.css");
.sidebar {
height: 100%;
top: var(--topbar-height);
left: 0;
overflow: hidden;
position: relative;
flex: 1;
z-index: 0;
.heading {
flex: 1;
background: #fff;
border-bottom: 1px solid #eee;
padding: 0 10px;
h1 {
font-size: 1.8em;
}
}
}
Our <Sidebar /> component so far is straightforward and currently only renders the title prop we
passed it above. Currently, we dont have much interesting going on with our <Sidebar />
component. Lets pass through our details to a new <Listing /> component.
We could display the listing directly inside the <Sidebar /> component, but this would
not be the React Way.
Lets create a <Listing /> component to list each place. Well use an additional component called
<Item /> to handle listing each individual item. That being said, lets build the <Listing />
component. Well make the components, a css module, and the tests to go along with it:
$ mkdir src/components/Listing
$ touch src/components/Listing/{Listing.js,Item.js,styles.module.css}
$ touch src/components/Listing/{Listing.spec.js,Item.spec.js}
Since were doing some test driven development, lets get our Listing.spec.js updated. The
assumptions well make about our <Listing /> component is that it has the styles we expect and
that it lists an <Item /> component for each place in our places prop. The test can be stubbed as
follows:
import React from 'react'
import { expect } from 'chai'
import { shallow } from 'enzyme'
Lets go ahead and fill out these tests. Its okay that they will ultimately fail when we run them at first
(we havent implemented either the <Listing /> or the <Item /> components yet). The final tests
will look like:
import React from 'react'
import { expect } from 'chai'
import { shallow } from 'enzyme'
import Listing from './Listing'
import styles from './styles.module.css'
describe('<Listing />', () => {
let wrapper;
const places = [{
name: 'Chicago'
}, {
name: "San Francisco"
}];
beforeEach(() => {
wrapper = shallow(<Listing title={'Cafes'}
places={places} />)
});
it('wraps the component in a listing css class', () => {
expect(wrapper.find(`.${styles.container}`))
.to.be.defined;
})
it('has an item for each place in the places prop', () => {
expect(wrapper.find('Item').length)
.to.equal(places.length);
})
})
Lets create the container style inside the css module for the <Listing /> component. In our
src/components/Listing/styles.module.css, lets make sure we have a CSS container that
removes some basic styling.
.container {
height: 100%;
overflow: auto;
padding-bottom: 60px;
margin: 0;
}
Before we can depend upon the view changing with our updated <Listing /> component, well
need to write up our <Item /> component.
Item Component
The expectations well make with our <Item /> component will be that it shows the name of the
place, that it is styled with the appropriate class, and that it shows a rating (given by the Google API).
In our src/components/Listing/Item.spec.js, lets stub out these assumptions as a Jasmine
test:
import React from 'react'
import { expect } from 'chai'
import { shallow } from 'enzyme'
import Item from './Item'
import styles from './styles.module.css'
describe('<Item />', () => {
let wrapper;
const place = {
name: 'San Francisco'
}
beforeEach(() => {
wrapper = shallow(<Item place={place} />)
});
it('contains a title component with yelp')
it('wraps the component with an .item css class')
it('contains a rating')
})
It may be obvious at this point, but well create another component well call <Rating /> which will
encapsulate the ratings on a place given back to us through the Google API. For the time being, lets
turn these tests green.
In our src/components/Listing/Item.js. Our <Item /> component wont need to worry about
fetching a place or dealing with the API, since well be passing through the place object when we
mount it. Well display the name of the place along with its rating. Before we get to building our
<Rating /> component, lets build up our <Item /> component to display the name and its rating
by number.
import React, { PropTypes as T } from 'react'
import classnames from 'classnames'
import Rating from 'components/Rating/Rating';
import styles from './styles.module.css'
export class Item extends React.Component {
render() {
const {place} = this.props;
return (
<div
className={styles.item}>
<h1 className={classnames(styles.title)}>{place.name}</h1>
<span>{place.rating/5}</span>
</div>
)
}
}
export default Item
In order to style the <Item /> component, lets create the .item class in the css module at
src/components/Listing/styles.module.css. Well use flexbox to design our <Item />
listing which contains the two elements:
/* ... */
.item {
display: flex;
flex-direction: row;
border-bottom: 1px solid #eeeeee;
padding: 10px;
text-decoration: none;
h1 {
flex: 2;
&:hover {
color: $highlight;
}
}
.rating {
text-align: right;
flex: 1;
}
&:last-child {
border-bottom: none;
}
}
Now, back in our view, well see that the ratings are starting to take shape:
Rating Component
Lets get the ratings looking a little nicer. Well use stars to handle the rating numbering (rather than the
ugly decimal points ew). Since were going to make a <Rating /> component, lets create it in the
src/components directory. Well create the module, the css module, and the tests:
$ mkdir src/components/Rating
$ touch src/components/Rating/{Rating.js,styles.module.css}
$ touch src/components/Rating/Rating.spec.js
The assumptions well make with the <Rating /> component is that we will have two layers of CSS
elements and that the first one will fill up the stars at a percentage of the rating.
With these expectations, the test skeleton will look akin to:
import React from 'react'
import {expect} from 'chai'
import {shallow} from 'enzyme'
Lets fill up these tests. Well shallow-mount each of the components in each test and make sure that
the CSS style thats attached matches our expectations:
// ...
it('fills the percentage as style', () => {
let wrapper = shallow(<Rating percentage={0.10} />)
expect(wrapper.find(`.${styles.top}`))
.to.have.style('width', '10%');
wrapper = shallow(<Rating percentage={0.99} />)
expect(wrapper.find(`.${styles.top}`))
.to.have.style('width', '99%')
});
it('renders bottom and top star meters', () => {
let wrapper = shallow(<Rating percentage={0.99} />)
expect(wrapper.find(`.${styles.top}`)).to.be.present;
expect(wrapper.find(`.${styles.bottom}`)).to.be.present;
})
Our tests will fail while we havent actually implemented the <Rating /> component. Lets go ahead
and turn our tests green. The <Rating /> component is straightforward in that well have two levels
of Rating icons with some style splashed on to display the stars.
In our src/components/Rating/Rating.js file, lets create a stateless <RatingIcon />
component along with our <Rating /> component:
import React, { PropTypes as T } from 'react'
import styles from './styles.module.css';
const RatingIcon = (props) => (<span></span>)
export class Rating extends React.Component {
render() {
// ...
}
}
export default Rating
The <RatingIcon /> can be a stateless component as its output is not dependent upon
the props. It simply shows a star (*).
We can use this <RatingIcon /> component inside our <Rating /> component. Well use inline
style to set the width of the top colored rating icons. In our src/components/Rating/Rating.js,
lets update the <Rating /> component to use the <RatingIcon /> component:
export class Rating extends React.Component {
render() {
const {percentage} = this.props;
const style = {
width: `${(percentage || 0) * 100}%`
}
return (
<div className={styles.sprite}>
<div className={styles.top} style={style}>
<RatingIcon />
<RatingIcon />
<RatingIcon />
<RatingIcon />
<RatingIcon />
</div>
<div className={styles.bottom}>
<RatingIcon />
<RatingIcon />
<RatingIcon />
<RatingIcon />
<RatingIcon />
</div>
</div>
)
}
}
Without any style, the <Rating /> component doesnt look quite like a rating.
Lets fix the styling to separate the top and the bottom part of the components.
.sprite {
unicode-bidi: bidi-override;
color: #404040;
font-size: 25px;
height: 25px;
width: 100px;
margin: 0 auto;
position: relative;
padding: 0;
text-shadow: 0px 1px 0 $light-gray;
}
.top {
color: #48b5e9;
padding: 0;
position: absolute;
z-index: 1;
display: block;
top: 0;
left: 0;
overflow: hidden;
}
.bottom {
padding: 0;
display: block;
z-index: 0;
color: #a2a2a2;
}
With these colors set, lets convert our <Rating /> components css module to use them
(remembering to import the src/styles/colors.css file):
@import "../../styles/colors.css";
.sprite {
unicode-bidi: bidi-override;
color: var(--dark);
font-size: 25px;
height: 25px;
width: 100px;
margin: 0 auto;
position: relative;
padding: 0;
text-shadow: 0px 1px 0 var(--light-gray);
}
.top {
color: var(--highlight);
padding: 0;
position: absolute;
z-index: 1;
display: block;
top: 0;
left: 0;
overflow: hidden;
}
.bottom {
padding: 0;
display: block;
z-index: 0;
color: var(--light-gray);
}
Now our <Sidebar /> with our <Listing /> component is complete.
In order to set these elements up to be handled by their routes, well need to change our routes.
Currently, our routes are set by a single route that shows the <Container /> component. Lets
modify the route to show both a Map component and details.
Our src/views/routes.js file currently only contains a single <Route /> component. As a
reminder, it currently looks like:
import React from 'react'
import {Route} from 'react-router'
import Container from './Container'
export const makeMainRoutes = () => {
return (
<Route path="" component={Container} />
)
}
export default makeMainRoutes;
Lets modify the makeMainRoutes() function to set the <Container /> component as a container
(surprise) for the main routes.
export const makeMainRoutes = () => {
return (
<Route path="" component={Container}>
{/* child routes in here */}
</Route>
)
}
Now, lets build the <Map /> container we will surface in this area. Well build this <Map />
component in the src/views/Main/Map directory, so lets import the exported component in our
makeMainRoutes() file and list it as a child:
import Map from './Map/Map'
export const makeMainRoutes = () => {
return (
<Route path="" component={Container}>
<Route path="map" component={Map} />
</Route>
)
}
Loading our new routes in the browser will show nothing until we navigate to the /map route AND we
build our component. For the time being, lets create a simple <Map /> component to show in the
Map area to confirm the route is working.
As weve done a few times already, lets create a JS file and the css module file in the
src/views/Main/Map directory:
$ mkdir src/views/Main/Map
$ touch src/views/Main/Map/{styles.module.css,Map.js}
A really simple default <Map /> component with some dummy text is pretty simple to create
import React, { PropTypes as T } from 'react'
import classnames from 'classnames'
import styles from './styles.module.css'
export class MapComponent extends React.Component {
render() {
return (
<div className={styles.map}>
MAP!
</div>
)
}
}
export default MapComponent
Heading back to the browser, well see that wait, its blank? Why? We havent told the <Container
/> component how or where to render its child routes. Before we can go much further, well need to
share our expectations with the component.
The React Way to handle this is by using the children prop of a component. The
this.props.children prop is handed to the React component when it mounts for any nodes that
are rendered as a child of a React component. Well use the children prop to pass forward our child
routes to be rendered within the container.
To use the children prop, lets modify the <Container /> component in
src/views/Main/Container.js to pass them down inside the content block.
export class Container extends React.Component {
// ...
render() {
return (
<Map
visible={false}
className={styles.wrapper}>
<Header />
<Sidebar />
<div className={styles.content}>
{/* Setting children routes to be rendered*/}
{this.props.children}
</div>
</Map>
</div>
)
}
}
Now, when we load the view in the browser well see that the route for /map inside the content block.
Since we are already loading the <Map /> container in the outer <Container /> component, we will
already have a handle to the google reference as well as a reference to a created map instance. We
can pass the reference down as a prop to the child elements by cloning children and creating a new
instance to handle passing down data.
React makes this process easy to handle by using the React.cloneElement() function. In our
render() method of the <Container /> component, lets get a handle to the children props
outside of the <Map /> component:
With a handle to the children props, we can create a clone of them passing the new props down to the
children, for instance:
export class Container extends React.Component {
// ...
render() {
let children = null;
if (this.props.children) {
// We have children in the Container component
children = React.cloneElement(
this.props.children,
{
google: this.props.google
});
}
return (
{/* shortened for simplicity */}
<div className={styles.content}>
{/* Setting children routes to be rendered*/}
{children}
</div>
)
}
}
Now, the children of the <Container /> component will receive the google prop. Of course, we
also will have to pass the places from the state of the <Container /> component. We can pass
any other data we want to send o within the React.cloneElement() function. Well pass as much
data down as we need to here, which well take advantage of a bit later.
Now, we can update our <Map /> component (in src/views/Main/Map/Map.js) to show an actual
<Map /> from our GoogleApiComponent() container using the google props:
import React, { PropTypes as T } from 'react'
import classnames from 'classnames'
import Map from 'google-maps-react'
import styles from './styles.module.css'
export class MapComponent extends React.Component {
render() {
return (
<Map google={this.props.google}
className={styles.map}
>
</Map>
)
}
}
export default MapComponent
The <MapComponent /> here will work as expected as we are already creating the google
component (in fact, we can also pass the map prop down and the GoogleApiComponent npm
module will handle it correctly without creating a new map instance).
To create the Marker, lets import the <Marker /> component from the google-maps-react npm
module:
import Map, { Marker } from 'google-maps-react'
Now, inside the renderMarkers() function we can iterate over the this.props.places prop and
create a <Marker /> instance:
export class MapComponent extends React.Component {
renderMarkers() {
return this.props.places.map(place =>{
return <Marker key={place.id}
name={place.id}
place={place}
position={place.geometry.location}
/>
})
}
// ...
}
Loading this in the browser, well see that our this.props.places is null and this method will throw
an error (there are multiple ways to handle this, well use a simple check). We can avoid this check by
returning null if there are no places at the beginning of the function:
export class MapComponent extends React.Component {
renderMarkers() {
if (!this.props.places) { return null; }
return this.props.places.map(place =>{
return <Marker key={place.id}
name={place.id}
position={place.geometry.location}
/>
})
}
// ...
}
Now, we have our <Marker /> component showing markers for each of the places in the map. With
the <Map /> component set up, lets move on to handling settling the screen that shows more details
about each place after we click on the marker that corresponds to the place.
Clicking on Markers
For each of the <Marker /> components, we can listen for onClick events and run a function when
its clicked. We can use this functionality to route the user to a new path that is designed to show
details specifically about a single place from the Google API.
Since were handling most of the business logic in the <Container /> and we are mostly using the
<MapComponent /> as a non-stateful component, well put this logic inside the containing
<Container /> component, as opposed to the <MapComponent />.
Containing the entire routing logic in the <Container /> component is a simple way to
keep the business logic of the application in a single spot and not clutter up other
components. Plus, it makes testing the components way simpler.
Just like we can pass through data in the props of a component, we can pass down function
references as well. Lets take the call to React.cloneElement() and pass through a reference to a
function that will get called when one is clicked. Lets call it onMarkerClick() (obvious name for the
function, eh?).
Lets open up our <Container /> element in src/views/Main/Container.js and add the
function as well as the prop to the children:
export class Container extends React.Component {
// ...
onMarkerClick(item) {
}
render() {
let children = null;
if (this.props.children) {
// We have children in the Container component
children = React.cloneElement(
this.props.children,
{
google: this.props.google,
places: this.state.places,
loaded: this.props.loaded,
onMarkerClick: this.onMarkerClick.bind(this)
});
}
// ...
}
To use our new onMarkerClick prop, we can pass it through to the onClick() method in the
<Marker /> component. The <Marker /> component accepts a click handler through the onClick
prop. We can pass through the prop directly to the <Marker /> component.
Back in the <MapComponent /> (at src/views/Main/Map/Map.js), lets update the <Marker />
component with the onClick prop to call back to our onMarkerClick prop (that is a mouthful):
export class MapComponent extends React.Component {
renderMarkers() {
return this.props.places.map(place =>{
return <Marker key={place.id}
name={place.id}
place={place}
onClick={this.props.onMarkerClick.bind(this)}
position={place.geometry.location}
/>
})
}
// ...
}
Now, when a <Marker /> is clicked, it will call our <Container /> components
onMarkerClick() function. The callback it executes is called with the instance of the <Marker />
component, along with the map and google instance. Well only need to work with the <Marker />
instance as we have all the information passed through in props.
Since we pass the place object in the <Marker /> component, we can reference the entire place
object in the callback.
export class Container extends React.Component {
// ...
onMarkerClick(item) {
const {place} = item; // place prop
}
// ...
}
When our user clicks on the marker, well want to send them to a dierent route, the details route.
To define this, well use the router in the <Container /> components context to push the users
browser to another route. In order to get access to the context of the <Container /> component,
well need to define the contextTypes. Well be using a single context in our contextTypes. At the
end of the src/views/Main/Container.js file, lets set the router to the type of object:
Now, inside the onMarkerClick() function we can get access to the push() method from the router
context and call it with the destination route:
export class Container extends React.Component {
// ...
onMarkerClick(item) {
const {place} = item; // place prop
const {push} = this.context.router;
push(`/map/detail/${place.place_id}`)
}
// ...
}
React router takes the final part of the URL and treats it as a variable route. That is to say if the user
visits a url such as: /detail/abc123, it will match our new route and the :placeId will be passed
through as props to our Detail component as params. Well come back to this in a moment.
Lets create the Detail component and its css module (just like we did previously for the <Map />
component):
$ mkdir src/views/Main/Detail
$ touch src/views/Main/Detail/{styles.module.css,Detail.js}
The <Detail /> component is a single component that is responsible for showing the data
associated with a place. In order to handle finding more details about the place, we can call another
Google API that directly gives us details about one specific place.
Lets create this API handler in our src/utils/googleApiHelpers.js (as we have done with the
nearbySearch()). Lets add the following request at the end of the file:
export function getDetails(google, map, placeId) {
return new Promise((resolve, reject) => {
const service = new google.maps.places.PlacesService(map);
const request = {placeId}
service.getDetails(request, (place, status) => {
if (status !== google.maps.places.PlacesServiceStatus.OK) {
return reject(status);
} else {
resolve(place);
}
})
})
}
Lets create the <Detail /> component, making sure to import our new helper function (which well
use shortly). In the src/views/Main/Detail/Detail.js, lets create the component:
import React, { PropTypes as T } from 'react'
import {getDetails} from 'utils/googleApiHelpers'
import styles from './styles.module.css'
export class Detail extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
loading: true,
place: {},
location: {}
}
}
getDetails(map) {}
// ...
render() {
return (
<div className={styles.details}></div>
)
}
}
export default Detail
The <Detail /> component is a stateful component as well need to hold on to the result of an API
fetch to the getDetails() request. In the constructor, weve set the state to hold on to a few values,
including the loading state of the request.
We have to handle two cases for when the <Detail /> component mounts or updates in the view.
1. The first case is when the <Detail /> mounts initially, well want to make a request to fetch more
details about the place identified by the :placeId.
2. The second is when the map component updates or the placeId changes.
In either case, well need a common method for getting details. Lets update our getDetails()
method inside the <Detail /> component that well call our helper method of the same name. First,
lets get the placeId from the URL (passed to our component through this.props.params):
import {getDetails} from 'utils/googleApiHelpers'
export class Detail extends React.Component {
getDetails(map) {
// the placeId comes from the URL, passed into
// this component through params
const {google, params} = this.props;
const {placeId} = params;
}
// ...
}
With the placeId, we can call our helper method and store the result from the returned promise:
import {getDetails} from 'utils/googleApiHelpers'
export class Detail extends React.Component {
getDetails(map) {
// the placeId comes from the URL, passed into
Although it looks like quite a bit, the method is straight-forward. Were setting the state as loading (so
we can show the loading state in the view) and then calling the method. When it comes back
successfully, well update the state with the place/location and update the loading state.
Were storing the location as a custom state object to standardize the location, rather
than creating the object in the render() function.
Back to the two states, the first one is easy to handle. When the componentDidMount(), we can
check to see if we have a map available in the props (we wont have a map prop if the <Map /> has
yet to load its asynchronous, after all) and call our getDetails() method.
In the src/views/Main/Detail/Detail.js, lets add the componentDidMount() function:
// ...
export class Detail extends React.Component {
componentDidMount() {
if (this.props.map) {
this.getDetails(this.props.map);
}
}
getDetails(map) {
// ...
}
}
The second case is relatively straight-forward as well. The component will update when the props
update, which in our case might happen when the map is loading or the placeId changes in the URL.
If either one of those are true, well call through to our getDetails() method:
// ...
export class Detail extends React.Component {
componentDidUpdate(prevProps) {
if (this.props.map && // make sure we have a map
(prevProps.map !== this.props.map ||
prevProps.params.placeId !== this.props.params.placeId)) {
this.getDetails(this.props.map);
}
}
getDetails(map) {
// ...
}
}
With that, well have the place details in the this.state of the <Details /> component ready for
rendering.
If were in a loading state (i.e. this.state.loading is true), well show the user that were loading
the page. Well set this up simply in the render() function in our <Details /> component:
export class Detail extends React.Component {
// ...
render() {
if (this.state.loading) {
return (<div className={styles.wrapper})>
Loading...
</div>);
}
// We're no longer loading when we get here
}
}
Lets show the places name, which we now have in the this.state.place:
export class Detail extends React.Component {
// ...
render() {
if (this.state.loading) {
return (<div className={styles.wrapper})>
Loading...
</div>);
}
// We're no longer loading when we get here
Before we move on, lets add a small bit of style to the <Detail /> component. Mostly for
demonstration purposes as well as making our app responsive. Before we get there, lets wrap our
<h2> element in the header class so we can modify the style and its container:
export class Detail extends React.Component {
// ...
render() {
if (this.state.loading) {
return (<div className={styles.wrapper})>
Loading...
</div>);
}
// We're no longer loading when we get here
const {place} = this.state;
return (
<div className={styles.wrapper}>
<div className={styles.header}>
<h2>{place.name}</h2>
</div>
</div>
)
}
}
Remember, we can use the same padding across the entire app for consistency, so rather than hardcode the padding in the header, lets use the globally defined variable --padding. First, well need to
import the base style and then use the var() syntax to apply the --padding:
@import url("../../../styles/base.css");
.header {
padding: 0 var(--padding);
h2 {
font-size: 1.5em;
}
}
Although this style addition isnt incredibly impressive, we now have confirmed the css module is
hooked up to our <Detail /> component.
The Google Places API gives us back an interesting object with all sorts of fun goodies included, such
as photos. Lets get a photo panel showing the inside of the cafe (usually) that are handed back by
API.
// ...
const {place} = this.state;
return (
<div className={styles.wrapper}>
<div className={styles.header}>
<h2>{place.name}</h2>
</div>
<div className={styles.details}>
{this.renderPhotos(place)}
</div>
</div>
)
}
}
For each photo of the places photo array, well pass back the generated URL (as we discussed
above):
export class Detail extends React.Component {
// ...
renderPhotos(place) {
if (!place.photos || place.photos.length == 0) return;
const cfg = {maxWidth: 100, maxHeight: 100}
return (<div className={styles.photoStrip}>
{place.photos.map(p => {
const url = `${p.getUrl(cfg)}.png`
return (<img key={url} src={url} />)
})}
</div>)
}
}
Checking our browser, we now have a beautiful photo spread well, almost beautiful.
Lets add some style to our already classNamed photoStrip CSS class in our modules. First, lets
use flexbox to make our photoStrip be a single, horizontally-scrollable line of photos. In the same CSS
file (src/views/Main/Detail/styles.module.css), lets add the photoStrip CSS definition
with flex:
.details {
/* a little bit of padding */
padding: 0 var(--padding);
.photoStrip {
flex: 1;
display: flex;
overflow: auto;
}
}
Annnnndddd lets get rid of that awful scrollbar at the bottom of the photoStrip by adding the ::webkit-scrollbar CSS definition:
.details {
padding: 0 var(--padding);
::-webkit-scrollbar {
width: 0px;
background: transparent;
}
/* ... */
}
Finally, lets add a small margin between the photos so we can see they are dierent photos instead of
a single one:
.details {
/* a little bit of padding */
padding: 0 var(--padding);
.photoStrip {
flex: 1;
display: flex;
overflow: auto;
img {
margin: 0 1px;
}
}
}
Going Responsive
Our app looks great at a medium-sized to larger screen, but what if our user is using a mobile device to
view our yelp-competitor?
Lets fix this right now. We will still want our list of other close-by restaurants showing, but perhaps the
location makes sense below the details of the currently interesting one. It makes more sense for the list
of restaurants to be below the details about a particular one or as a pull-out menu from the side.
To avoid adding extra JS work (and focus on the CSS responsiveness), lets move the menu to below
the detail of a specific location. In order to handle this, well use some media queries.
Definition of Media query from w3c schools:
Media query is a CSS technique introduced in CSS3.
It uses the @media rule to include a block of CSS properties only if a certain condition is
true.
We can use media queries to ask the browser to only display a CSS rule when a browser condition is
true. For instance, we can use a media query to only set a font-size to 12px from 18px when were
printing by using the sample CSS media query:
body { font-size: 18px; }
/* Media query for printing */
@media (print) {
font-size: 12px;
}
/* ... */
In our postcss setup, were using the precss postcss (mouthful, right?) processor, which conveniently
adds the postcss-custom-media plugin to our code. The postcss-custom-media plugin allows us to
define media queries as variables and use these variables as the definitions for our media queries.
In a mobile-first only world, it makes sense for us to write our CSS to look correct on mobile first and
then add media queries to style larger screens.
1. Design/write mobile-friendly CSS (first)
2. Add styles for larger screen (second)
With this in mind, lets design our main screen to show the content block as the first visual component
and the sidebar to come second.
In order to set up our app to use the flexbox approach, well need to look at 3 aspects of flexbox. To
learn more about flexbox, what it is and how to use it, Chris Coyier has a fantastic article on using it.
Well spend just enough time on it to setup our app.
display: flex
To tell the browser we want a component to use the flexbox approach, well need to add the
display: flex rule to the parent component of an element.
For us, this means well set the display: flex; rule on the wrapper of the entire page (since were
using flexbox on every element in our Main view). We set this in our wrapper class previously.
flex-direction
The flex-direction rule tells the browser where to set the axis of the layout. We can either lay
elements out horizontally (column) or vertically (row). By setting the flex-direction: column rule,
the browser will lay elements one on top of the other. Since we want our page to lay out our elements
horizontally for mobile, lets set the flex-direction to column:
/* In src/views/Main/styles.module.css */
.wrapper {
overflow-y: scroll;
display: flex;
margin: 0;
padding: 15px;
height: 100vh;
-webkit-box-orient: horizontal;
-o-box-orient: horizontal;
/* Added these rules */
flex-direction: column;
flex: 1;
}
/* ... */
Refreshing our browser, well see that our layout has completely switched from horizontal to vertical.
Adding a flex: 1 to the content container balances out the sizing of the app in the mobile view.
Now, if we expand the view back to desktop size, the vertical layout looks out of place and doesnt
quite work as well. Lets add our first media query to fix this view on a larger screen.
First, we like to define our media queries by themselves. We could add our custom media queries into
the base.css file, but it can clutter the styles as it grows larger. Instead, lets create a new file in
src/styles/queries.css to contain all of our media query definitions.
$ touch src/styles/queries.css
In this new file, well use the @custom-media definition to define the screen for phones vs. those
which are larger:
@custom-media --screen-phone (width <= 35.5em);
@custom-media --screen-phone-lg (width > 35.5em);
Now, any screen that is relatively small can be targeted using the --screen-phone media query and
any larger screen can be targeted using the --screen-phone-lg rule.
Back in our main styles css module, lets apply the media query to set the flex-direction back to
column when were on a larger screen.
/* In src/views/Main/styles.module.css */
.wrapper {
overflow-y: scroll;
display: flex;
margin: 0;
padding: 15px;
height: 100vh;
-webkit-box-orient: horizontal;
-o-box-orient: horizontal;
flex-direction: column;
flex: 1;
@media (--screen-phone-lg) {
/* applied when the screen is larger */
flex-direction: column;
}
}
/* ... */
Now, both the content block and the sidebar are set side-by-side, but are both the same size. We can
fix the ordering using the last rule well discuss in-depth here.
order
The flexbox styling allows us to set the order of content blocks as well in CSS. Currently, our content
block and the sidebar block are both set to be ordered first, which defaults to showing the content
block that is defined first initially. To modify the content block so that it is defined first on mobile and
last on desktop, well apply the CSS rule of order.
/* In src/views/Main/styles.module.css */
.wrapper {
overflow-y: scroll;
display: flex;
margin: 0;
padding: 15px;
height: 100vh;
-webkit-box-orient: horizontal;
-o-box-orient: horizontal;
flex-direction: column;
flex: 1;
@media (--screen-phone-lg) {
In order to set the sidebar to play nicely with the rest of our content box, lets apply the same
principles of ordering and flex to the sidebar (except in reverse):
@import url("../../styles/base.css");
@import url("../../styles/queries.css");
.sidebar {
/* ... */
flex-direction: column;
order: 2;
flex: 1;
@media (--screen-phone-lg) {
display: block;
flex-direction: row;
order: 1;
}
.heading {
/* ... */
}
}
Back in the browser, refreshing the page, well see that the layout looks even at either mobile or larger.
One final note before we end our app, when our user visits the page for the first time, theyll end up at
a blank page. This is because we havent defined an index route.
We can handle an index route in a few dierent ways. Well set our Map component as the index route
using react routers <IndexRoute /> component. Back in src/views/Main/routes.js, lets set
the index route:
import {Route, IndexRoute} from 'react-router'
// ...
export const makeMainRoutes = () => {
return (
<Route path="" component={Container}>
<Route path="map" component={Map} />
<Route path="detail/:placeId"
component={Detail} />
<IndexRoute component={Map} />
</Route>
)
}
Refreshing the page and navigating to the root at http://localhost:3000, well see we no longer
have a blank page, but the main route of our app with the map and sidebar showing pages.
Conclusion
We walked through a complete app with a large number of routes, complex interactions, concepts, and
even added responsiveness to the app.
Weve placed the entire app on github at fullstackreact/yelp-clone.
If youre stuck, have further questions, feel free to reach out to us by:
Commenting on this post at the end of the article
Emailing us at [email protected]
Filling an issue on the Github repo
Tweet at us at @fullstackreact