Fullstack React: React Tutorial: Cloning Yelp

May 18, 2016 0 Comments react, tutorial, react router, full stack

Fullstack React: React Tutorial: Cloning Yelp

As we're writing the Fullstack React book, we get a lot of questions about how to build large applications with React and how to integrate external APIs.

tl;dr - This post will guide you through building a full React app, even with little to no experience in the framework. We're going build a Yelp clone in React

You can get the completed code here

Try the demo here

Let's build a lightweight clone of Yelp using React.

In this tutorial we'll talk about:

  • How to setup a new React project from scratch
  • How to create a basic React component
  • How to write modular CSS using postcss
  • How to setup testing
  • How route to different pages with react-router
  • How to integrate with Google Maps
  • How to write a Google Maps React component
  • How to write a five-star rating component

We'll be tying in a lot of different pieces of React together to build a full-sized React app. This post will guide you through building a full React app, even with little to no experience in the framework.

Let's buckle up and get building.

Table of Contents

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. We're 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 we're 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.

Really Quickstart

To skip the process of setting up the app, use the yeoman generator to build the entire setup process and skip directly to routing. Installation:

npm install -g yo generator-react-gen 

Run it with:

yo react-gen 

Throughout this process, we'll use some JavaScript features of ES6, inline css modules, async module loading, tests, and more. We'll use webpack for it's 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 you're not sure if you have npm available in $PATH,

This is the folder structure we'll be building towards:

Let's create a new node project. Open a terminal and create the beginning of our folder structure:

In the same directory, let's create our node project by using the npm init command and answering a few questions about the project. After this command finishes, we'll have a package.json in the same directory, which will allow us to define a repeatable process for building our app.

It doesn't 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.

We'll need a few dependencies to get started.

A Word on Dependencies

TLDR; Install the dependencies in each of the code sample sections.

Before we can start building our app, we'll need to set up our build chain. We'll 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.

Let's grab babel along with a few babel presets. In the same directory as the package.json, let's install our babel requirements:

$ npm install --save-dev babel-core babel-preset-es2015 babel-preset-react babel-preset-react-hmre babel-preset-stage-0 

We'll 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.

Let's include a few presets so we can use react as well as the react hot reloading features:

Babel allows us to configure different options for different operating environments using the env key in the babel configuration object. We'll include the babel-hmre preset only in our development environment (so our production bundle doesn't include the hot reloading JavaScript).

Let's even expand this even further by defining our full production/test environments as well:

webpack

Setting up webpack can be a bit painful, especially without having a previous template to follow. Not to worry, however! We'll 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.

Let's 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. Let's go ahead and install a few loaders we'll 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 url-loader file-loader 

In our webpack.config.js at the root directory, let's get our webpack module started. First, let's get some require statements out of the way:

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

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.

Personally, we'll usually create a few path variables to help us optimize our configuration when we start modifying it from it's default setup.

A development environment sets up a server without minification and accepts hot-reloading whereas a production one does not. Since we'll use the value of isDev later in our configuration, we'll recreate the default value in the same method. Alternatively, we can check to see if the NODE_ENV is 'development':

We'll come back to modifying the configuration file shortly as we get a bit further along and need some more configuration. For the time being, let's 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 we'll need to include the react dependency. Unlike the previous dependencies, we'll include react (and it's fellow react-dom) as an app dependency, rather than a development dependency.

$ npm install --save react react-dom 

We'll also install react router to handle some routing for us as we'll have multiple routes in our app, including a map place as well as details page for finding more details about each place we'll list.

$ npm install --save react-router 

A handy shortcut for installing and saving dependencies with the npm command:

The previous command could be rewritten as:

$ npm i -S react react-dom 

To install and save development dependencies, change the -S to -D, i.e.:

We can't start building our application without an entry file (as we added in the webpack configuration above). We'll come back to build our React app with a real app container, but let's make sure our server and build process are working up through this point.

Let's first start by setting up our app.js with a dummy react app. Create a file called src/app.js.

In this file, let's 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:

We'll need to mount the <App /> component on the page before we can see it working. In order to mount the application on the page, we'll need a DOM node reference to actually set it up, but where?

Let's grab a hold of the DOM node with the id of root and render our basic <App /> React component inside of it. Our complete src/app.js file should look like:

$ NODE_ENV=development ./node_modules/.bin/hjs-dev-server 

It's 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. We'll head to our browser (we'll use Google Chrome) and go to the address http://localhost:3000.


Although it's 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. Let's make it a tad easier by adding it as a script to our package.json.

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.

I.e.

All other scripts require the run command to be executed.

Let's finish off 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. Let's go ahead and install the autoprefixer package:

$ npm install --save-dev autoprefixer 

We'll use a few other postcss preprocessors in our postcss build chain to modify our CSS. The two we'll 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, we'll need to modify our webpack configuration so webpack knows we want to use them.

At it's 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. We'll modify the config object returned with any updates to the config object.

Each of the postcss plugins is exported as a function that returns a postcss processor, so we can have a chance to configure it.

We're not including any modification to the setup here, but it's possible.

For documentation on each one, check the documentation for each plugin:

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. Let's look at it in code.

For instance, let's say we have a css file that includes a single class definition of .container:

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 side-effects. CSS modules allow us to load the style alongside our JavaScript where the style applies and won't cause a conflict.

To use the .container class above in our <App /> container, we could import it and apply the style using it's name.

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.

We can apply the CSS class by adding it as a className in our React component as we would any other prop.

Demo: CSS Modules

In order to use CSS modules, we'll need to configure webpack to be aware of the fact we're using css modules. This part gets a little hairy, so let's tread a little slower here.

The css modules documentation page is a fantastic resource to use to get familiar with how they work and best practices in building CSS modules.

The postcss-loader gives us a few options we can use to configure css modules. We'll need to tell webpack how we want our modules named. In development, we'll want to set up our modules in a slighly nicer way than in production (where we won't do much debugging of classes).

In our webpack.config.js, let's create a dynamic naming scheme we'll set as the module names:

The hjs-webpack package makes it convenient to build our webpack configuration, but since we're modifying our css loading, we'll need to not only add a loader (to load our modules), but we'll need to modify the existing one.

Let's load the initial loader by finding it in the array of config.module.loaders using a simple regex:

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, let's create a new loader as well as modify the existing loader to support loading css modules:

In our new loader, we've modified the loading to only include css files in the src directory. For loading any other css files, such as font awesome, we'll include another css loader for webpack to load without modules support:

Credit for this (slightly modified) technique of loading css modules with webpack and hjs-webpack goes to lukekarrys

With our css loading devised in webpack, let's create a single global style in our app at src/app.css.

In our src/app.js, we can include these styles:

Starting up our server with npm start and refreshing our browser will reveal that global css loading works as expected.

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.


Configuring Multiple Environments

In our app, we're going to interface with the Google API. As it's never a good idea to hardcode our keys in a deployed application, we'll need a way to configure our app to include dynamic API keys based upon the environment.

One effective method for key handling is by using the environment variables of the system we're building against and bundling our key. Using a combination of the webpack.DefinePlugin() and dotenv, we can create a multi-environment build process using our environment variables.

First, let's install the dotenv package:

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.

Our .env file is generally a good spot to place global environment variables. To separate our environments, we'll create a mechanism to load those environment variables as well. Generally, we'll 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, let's add loading the second environment variables:

We can merge these two objects together to allow the environment-based [env].config.js file to overwrite the global one using Object.assign():

Our envVariables variable now contains all the environment variables and globally defined environment variables. In order to reference them in our app, we'll 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.

We can programmatically walk through our envVariables and replace each key in the conventional manner and stringifying their values.

We'll want to stringify the values we'll replace using the DefinePlugin() as they might contain characters that a browser's JavaScript parser won't recognize. Stringifying these values helps avoid this problem entirely.

In our webpack.config.js file, let's use the reduce() method to create an object that contains conventional values in our source with their stringified values:

Font Awesome

In our app, we'll use Font Awesome to display rating stars. We've already handled most of the work required to get font awesome working. We'll 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 we'll use this across components, we can require it in our main src/app.js:

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:

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

As we're 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.

In our source, instead of referencing our containers by relative path, we can simply call require('containers/SOME/APP').

Configuring Testing

React offers a wide range of methods of testing that our application is working as we expect it to work. We've been opening the browser and refreshing the page (although, hot-reloading is set up, so even refreshing the page isn't a requirement).

Although developing with such rapid feedback is great and offers 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 we'll implement the test first and then fill out the functionality of our components. Let's make sure that we can test our code.

Although the react team uses jest (and we cover it in-depth in fullstackreact), we'll 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

Let's start by installing our testing dependencies. We'll 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 

We'll 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. We'll need to install the json-loader to load json files along with our javascript files (hjs-webpack automatically configures the json loader for us, so we won't need to handle updating the webpack configuration manually):

$ npm i -D json-loader 

We'll be using karma to run our tests, so we'll need to install our karma dependencies. We'll use karma as it's 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.

Let's 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 

We'll be using PhantomJS to test our files so we don't 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 karma-phantomjs-launcher with karma-chrome-launcher and don't 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, we'll need to create two config files to both configure karma as well as the tests we'll have karma launch.

Let's set up our webpack testing environment through karma. The easiest way to get started with karma is by using the karma init command. First, let's install the karma cli and a few karma dependencies.

$ npm install -g karma-cli $ npm i -D karma karma-chai karma-mocha karma-webpack karma-phantomjs-launcher phantomjs-prebuilt phantomjs-polyfill 

And now we can run karma init to initialize the karma config file:

After we answer a few questions, it will spit out a karma.conf.js file. Since we're going to manipulate most of this file, it's 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:

With our karma.conf.js file generated, we'll need to give it a few configuration options, most of which are autogenerated or we have already set up.

First, the basics. We'll use some default options that karma has spit out for us automatically:

We'll need to tell karma that we want to use mocha and chai as the testing framework, instead of the default jasmine framework, so let's change the frameworks: [] option. We'll also need to add these to the plugins karma will use.

As we're using webpack to compile our files together, we'll also need to tell karma about our webpack configuration. Since we already have one, there is no need to recreate it, we'll just require our original one.

We'll also need to tell karma how to use webpack in it's confgiuration. We can do this by setting the karma-webpack plugin in it's plugin list.

First, let's install the spec reporter:

$ npm i -D karma-spec-reporter 

Back in our karma.conf.js file, let's add the plugins and change the browsers and plugins:

Finally, we need to tell karma where to find the files it will run as our tests. Instead of pointing it to the actual tests, we'll use a middleman, a webpack config to tell Karma where to find the tests and package them together.

Before we move on, we'll need to let karma know that it needs to run our tests.webpack.js file through the webpack preprocessor. We'll also ask it to run it through a sourcemap preprocessor to spit out usable sourcemaps (so we can debug our code effectively):

Let's 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:

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 we'll use in all of our tests.

Since we're going to be using a helper called chai enzyme, we can set our global configuration up here:

Up through this point, our complete karma conf file should look like this:

We've covered almost the entire karma setup, but we're missing two final pieces. Before we complete the karma setup, let's 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), let's place it in it's final spot. We're going to use the <App /> component we created earlier as a container for the rest of our app. We'll create a spec file in the containers/App/App.spec.js file.

$ mkdir src/containers/App && touch src/containers/App/App.spec.js 

In here, let's 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.

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, we'll 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 don't need to make our tests pass, initially, just get them running.

Let's 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 

Finally, we'll need to import the <App /> component from the right file in our src/app.js:

To execute our tests, we'll 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... don't 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. Webpack's approach to bundling is using a static file analyzer to find all the dependencies we're using in our app and to try to bundle those along with our source. As enzyme imports some dynamic files, this approach doesn't work.

Obviously we don't want to do this as we don't need to bundle tests with our production framework. We can tell webpack to ignore our testing framework and assume that it's available for us by setting it as an external dependency.

In our webpack.config.js file, let's set a few external dependencies that enzyme expects:

The second error we've encountered is that our testing framework is that a few of our production webpack plugins are mucking with our tests. We'll need to exclude a few plugins when we're running webpack under a testing environment. Since we're now handling two cases where testing with webpack differs from production or development, let's create a conditional application for our webpack testing environment.

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:

Now, if we run our tests again, using karma we'll see that our tests are running, they are just not passing yet.

$ NODE_ENV=test \ ./node_modules/karma/bin/karma start karma.conf.js 

Let's get our test passing!

First, let's wrap our long karma command into an npm script instead of running it at the command-line. In our package.json file, let's update the test script with our karma command.

Instead of passing the previous command, we can run our tests with npm test:

Let's also delete the contents of src/styles.module.css to get rid of the blue background.

We'll 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.

We'll use a command-line parser to add an npm script to tell karma to watch for any file changes. In our package.json file, let's add the test:watch command:

$ npm i -D yargs 

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.

Building Our Test Skeleton

Let's build the "infrastructure" of our app first. Our <App /> container element will contain the structure of the page, which essentially boils down to handling routes.

Being good software developers, let's build our test first to verify that we are able to ensure our assumptions about the component. Let's make sure that we have a <Router /> component loaded in our app.

This is also a good time for us to spend setting up our app testing structure.

In src/containers/App/App.spec.js, let's build the beginning of our jasmine spec. First, we'll need to include our libraries and the App component itself:

We'll use expect() to set up our expectations and shallow() (from enzyme) to render our elements into the test browser.

Our Testing Strategy

When testing any code in any language, the principles of testing are pretty much the same. We want to:

  1. define the focused functionality
  2. set an expectation of the output
  3. compare the executed code with the test code

In Jasmine, each of these steps are well-defined:

  1. Using describe()/it() defines the functionality
  2. We'll use expect() to set the expectation
  3. We'll use beforeEach() and matchers to confirm the output

To set up our our test, we'll need to pretend we're rendering our <App /> component in the browser. Enzyme makes this easy regardless of handling shallow or deep rendering (we'll look at the difference later).

In our test, we can shallow render our <App /> component into our browser and store the result (which will be our rendered DOM component). Since we'll want a "clean" version of our component every time, we need to do this in the beforeEach() block of our test:

Finally, we can run our tests by using our npm script we previously built:

Since we haven't implemented the <App /> component, our test will fail. Let's turn our test green.

Routing

Before we implement our routes, let's take a quick look at how we'll 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, we'll have a main header bar that we'll want to exist on every page. Underneath this main header, we'll switch out the content for each individual route.

We'll 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 we've been working with will simply become a container for route handling, rather than an element to hold/display content.

Although this approach sounds complex, it's an efficient 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, let's move on to setting up our main views.

In our src/containers/App/App.js, let's make sure we import the react-router library.

Next, in our usual style, let's build our React component (either using the createClass({}) method we used previously or using the class-based style, as we'll switch to here):

We like to include our content using the classical getter/setter method, but this is only a personal preference.

We'll 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.

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.

We're almost ready to place our routes on the page, we just have to pass in our custom routes (we'll make them shortly). We'll wrap our routes into this <App /> component:

In order to actually use our <App /> component, we'll need to pass through the two props the component itself expects to receive when we render the <App /> component:

  • history - we'll import the browserHistory object from react router and pass this export directly.
  • routes - we'll send JSX that defines our routes

Back in our src/app.js file, we'll pass through the history directly as we import it.

Lastly, we'll need to build some routes. For the time being, let's get some data in our browser. Let's show a single route just to get a route showing up. We'll revise this shortly.

To build our routes, we need access to the:

We can create our custom route by building a JSX instance of the routes using these two components:

Since we haven't yet defined the Home component above, the previous example fails, so we can create a really simple to prove it is working:

Finally, we can pass the routes object into our instance of <App /> and refreshing our browser. Provided we haven't made any major typos, we'll 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.

Building real routes

Up through this point, we've built our app using a demo routing scheme with a single route that doesn't do very much. Let's break out our routes to their own file both to keep our src/app.js clean and to separate concerns from the bootstrap script.

Let's create a src/routes.js file where we'll export our routes and we can consume them from the src/app.js file.

Let's confirm our <App /> is still running as we expect by using npm run test. If we don't make any typos, our app should still render in the browser.

Main page and nested routes

With our routing set up, let's move on to building our main view. This main view is designed to display our main map and the listing of restaurants. This is our main map page.

Since we'll be building a complex application, we like to separate our routes by themselves to be controlled by the component that will be using them. In other words, we'll be building our main view with the idea that it will define it's sub-routes as opposed to having one gigantic routing file, our nested components can define their own views.

Let's make a new directory in our root src directory we'll call views with a single directory in it with the name of the route we'll be building. For lack of a better name: Main/:

In this Main/ directory, let's create two files:

  • routes.js - a file for the Main/ view to define it's own routing
  • Container.js - the file that defines the container of the route itself

To get things started, let's 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.

When we import this routes file into our main routes file, we'll define some children elements, but for the time being, to confirm the set-up is working as we expect it, we'll work with a simple route container element. The container element can be as simple as the following:

With our containing element defined, let's flip back to our src/routes.js file to include our new sub-routes file. Since we exported a function, not an object, we'll need to make sure we display the return value of the function rather than the function itself.

Since we're defining sub-routes in our application, we won't 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.

Demo: Routing to Container Content

Refreshing the browser, we'll see our new content comes directly from the new container element.

Routing to Maps

Before we jump too far ahead, let's get our <Map /> component on the page. In a previous article, we built a <Map /> component from the ground-up, so we'll be using this npm module to generate our map. Check out this in-depth article at ReactMap.

Let's install this npm module called google-maps-react:

$ npm install --save google-maps-react 

In our <Container /> component, we'll 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 won't 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 we'll be making a list of places using the Google Places API, we'll place an invisible <Map /> component on screen.

For information on how to get a Google API key, check out the ReactMap article.

In our /.env file, let's set the GAPI_KEY to our key:

To use our GAPI_KEY, we'll reference it in our code surrounded by underscores (i.e.: __GAPI_KEY__).

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. Let's make sure by placing a <Map /> instance in our component:

Demo: Routing to a Map

With the google map displaying on our page, we can load up and start using the google maps service.

Getting a List of Places

With the hard work out of the way (displaying the map), let's get to displaying a list of places using the google api. When the <Map /> loads in the browser, it will call the prop function onReady() if it's passed in. We'll use the onReady() function to trigger a call to the google places API using the google script.

Let's modify our src/views/Main/Container.js file to define an onReady() function we can pass as a prop:

From here, we can use the google API as though we aren't using anything special. We'll create a helper function to run the google api command. Let's 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:

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.

Since we'll be storing a new state of the <Container /> so we can save the new results in our <Container />, let's set it to be stateful:

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:

Now, we can update our render() method by listing the places fetch we now have in our state:

Demo: A List of Places

With our listing of places in-hand, let's move on to actually turning our app into a closer to yelp-like component. In this section, we're 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, we're going to add some inline styling and use the natural React props flow.

First, let's install an npm module called classnames. The README.md is a fantastic resource for understanding how it works and how to use it. We're going to use it to combine classes together (this is an optional library), but useful, regardless.

$ npm install --save classnames 

Now, let's get to breaking out our app into components. First, let's build a <Header /> component to wrap around our app.

As we're 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. Let's create this directory and create the JS files that contain the component and tests:

Our <Header /> component can be pretty simple. All we'll use it for is to wrap the name of our app and possibly contain a menu (although we won't build this here). As we're building our test-first app, let's write the spec that reflects the <Header /> purpose first:

In the src/components/Header.spec.js file, let's create the specs:

The tests themselves are pretty simple. We'll simply expect for the text we expect:

Running our tests at this point will obviously fail because we haven't yet written the code to make them pass. Let's get our tests going green.

Since the full component itself is pretty straightforward, nearly the entire implementation (without styles) can be summed to:

Running the tests will now pass:

Inlining Styles

We can create a CSS module by naming a CSS stylesheet with the suffix: .module.css (based upon our webpack configuration). Let's create a stylesheet in the same directory as our Header.js:

$ touch src/components/Header/styles.module.css 

Let's create a single class we'll call topbar. Let's add a border around it so we can see the styles being applied in the browser:

We can use this style by importing it into our <Header /> component and applying the specific classname from the styles:

Let's go ahead and add a fixed style to make it look a little nicer:

Let's 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, let's add a few styles to help make our top bar look decent:

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.

Let's 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, let's add the content classes:

Setting flex: 2 sets our content box as the larger of the two elements (sidebar vs content). We'll 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, let's add a few styles around the elements on the page:

CSS Variables

Notice above how we have some hardcoded values, such as the height in our topbar class.

One of the valuable parts of using postcss with the postcss pipeline we've set up is that we can use variables in our css. There are a few different ways to handle using variables in CSS, but we'll 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:

Note, when creating a custom property (which in our case is a variable), we need to place it inside a CSS rule.

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:

Since we'll 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. Let's create a root directory for our CSS variables to live inside, such as src/styles/base.css:

$ touch src/styles/base.css 

Let's move our :root declaration into this base.css file:

To use this variable in our multiple css modules, we'll 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:

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, we'll show a map to start out with and when our user clicks on a pin, we'll change this to show details about the location the user has clicked on.

Let's get started with the sidebar component that shows a listing of the places. We've 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 we'll 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.

Let's take our listing from a simple list to a <Sidebar /> component. Since we'll create the component as a shared one, let's add it in the src/components/Sidebar directory. We'll also create a JavaScript file and the CSS module:

Let's also make sure we add the appropriate top positioning such that it isn't rendered underneath the topbar. Let's create the styles in src/components/Sidebar/styles.module.css and add some styles:

We could display the listing directly inside the <Sidebar /> component, but this would not be the React Way.

$ 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} 

Let's go ahead and fill out these tests. It's okay that they will ultimately fail when we run them at first (we haven't implemented either the <Listing /> or the <Item /> components yet). The final tests will look like:

With the <Listing /> component tests written, let's turn our sights on implementing the <Listing /> component. Similar to the <Sidebar /> component, the <Listing /> component serves basically as a wrapper around the <Item /> component with styles. At it's core, we simply need a wrapper component around a listing of <Item /> components.

Our entire component will be fairly short and simple.

Before we can depend upon the view changing with our updated <Listing /> component, we'll need to write up our <Item /> component.

The expectations we'll 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, let's stub out these assumptions as a Jasmine test:

Let's fill up these tests. The tests themselves are straightforward.

It may be obvious at this point, but we'll create another component we'll call <Rating /> which will encapsulate the ratings on a place given back to us through the Google API. For the time being, let's turn these tests green.

Now, back in our view, we'll see that the ratings are starting to take shape:


Rating Component

Let's get the ratings looking a little nicer. We'll use stars to handle the rating numbering (rather than the ugly decimal points -- ew). Since we're going to make a <Rating /> component, let's create it in the src/components directory. We'll 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 we'll 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:

Let's fill up these tests. We'll shallow-mount each of the components in each test and make sure that the CSS style that's attached matches our expectations. We'll use percentage to display the rating percentage (from 0 to 5).

Our tests will fail while we haven't actually implemented the <Rating /> component. Let's go ahead and turn our tests green. The <Rating /> component is straightforward in that we'll have two levels of Rating icons with some style splashed on to display the stars.

The <RatingIcon /> can be a stateless component as its output is not dependent upon the props. It simply shows a star (*).

Without any style, the <Rating /> component doesn't look quite like a rating.

Let's fix the styling to separate the top and the bottom part of the components.

Demo: Rating Stars

In src/styles/colors.css, let's set our colors:

With these colors set, let's convert our <Rating /> component's css module to use them (remembering to import the src/styles/colors.css file):

Now our <Sidebar /> with our <Listing /> component is complete.

Building the Main Pane

Now that we have our <Sidebar /> implemented, let's build up our main elements. That is, let's show our map and details. Since we'll want these pages to be linkable, that is we want our users to be able to copy and paste the URL and see the same URL as they expect, we'll set the main content to be defined by it's URL.

In order to set these elements up to be handled by their routes, we'll need to change our routes. Currently, our routes are set by a single route that shows the <Container /> component. Let's modify the route to show both a Map component and details.

Our src/views/Main/routes.js file currently only contains a single <Route /> component. As a reminder, it currently looks like:

Let's modify the makeMainRoutes() function to set the <Container /> component as a container (surprise) for the main routes.

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, let's create a simple <Map /> component to show in the Map area to confirm the route is working.

As we've done a few times already, let's create a JS file and the css module file in the src/views/Main/Map directory:

A really simple default <Map /> component with some dummy text is pretty simple to create

Heading back to the browser, we'll see that... wait, it's blank? Why? We haven't told the <Container /> component how or where to render it's child routes. Before we can go much further, we'll 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. We'll use the children prop to pass forward our child routes to be rendered within the container.

To use the children prop, let's modify the <Container /> component in src/views/Main/Container.js to pass them down inside the content block.

Now, when we load the view in the browser we'll see that the route for /map inside the content block.

With a handle to the children props, we can create a clone of them passing the new props down to the children, for instance:

That is, we can render <Marker /> components from inside the <Map /> component as children. In our <MapComponent /> component, we can use a similar process to the <Container /> element to render the child props. Since we'll want to display a <Marker /> for each place, we will iterate through the this.props.places array and instantiate a new <Marker /> instance for each.

Let's create the instances using a method we'll handle the children rendering using a helper function:

To create the Marker, let's import the <Marker /> component from the google-maps-react npm module:

Loading this in the browser, we'll see that our this.props.places is null and this method will throw an error (there are multiple ways to handle this, we'll use a simple check). We can avoid this check by returning null if there are no places at the beginning of the function:

Now, we have our <Marker /> component showing markers for each of the places in the map. With the <Map /> component set up, let's 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.

Since we'll be updating all the children in the <Map />, not just the Markers, this is a good time to "abstract" the children rendering function. Without rendering it's children, none of our additional child components will render.

For each of the <Marker /> components, we can listen for onClick events and run a function when it's 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.

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.

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.

When our user clicks on the marker, we'll want to send them to a different route, the details route.

To define this, we'll use the router in the <Container /> component's context to push the user's browser to another route. In order to get access to the context of the <Container /> component, we'll need to define the contextTypes. We'll be using a single context in our contextTypes. At the end of the src/views/Main/Container.js file, let's 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:

Now, clicking on a marker will push us to a new route, the /map/detail/${place.place_id} route.

To create the new route, let's modify the routes.js file so that we have this route. In our src/views/Main/routes.js file, let's add:

Let's create the Detail component and it's css module (just like we did previously for the <Map /> component):

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.

Let's create this API handler in our src/utils/googleApiHelpers.js (as we have done with the nearbySearch()). Let's add the following request at the end of the file:

Let's create the <Detail /> component, making sure to import our new helper function (which we'll use shortly). In the src/views/Main/Detail/Detail.js, let's create the component:

The <Detail /> component is a stateful component as we'll need to hold on to the result of an API fetch to the getDetails() request. In the constructor, we've 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, we'll 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.

With the placeId, we can call our helper method and store the result from the returned promise:

Although it looks like quite a bit, the method is straight-forward. We're 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, we'll update the state with the place/location and update the loading state.

We're storing the location as a custom state object to standardize the location, rather than creating the object in the render() function.

In the src/views/Main/Detail/Detail.js, let's add the componentDidMount() function:

With that, we'll have the place details in the this.state of the <Details /> component ready for rendering.

Let's show the place's name, which we now have in the this.state.place:

Before we move on, let's 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, let's wrap our <h2> element in the header class so we can modify the style and it's container:

Back in our src/views/Main/Detail/styles.module.css, we can add the .header{} CSS class definition to give it some definition. Let's increase the font-size and add some padding around the title to make it stand out more:

Although this style addition isn't 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. Let's get a photo panel showing the inside of the cafe (usually) that are handed back by API.

Demo: Explore Google Places JSON API

Rather than display the photos inline to handle the process, let's nest the photo rendering in a function in the <Detail /> component (we'll call it renderPhotos()):

An alternate way of handling photo rendering is by creating a component to take care of loading the photos. However, we'll nest them in the <Details /> component to contain the <Detail /> display to a single component.

Since some places don't have photos, we'll want to return back an empty virtual DOM node. Let's check for this case:

For each photo of the place's photo array, we'll pass back the generated URL (as we discussed above):

Checking our browser, we now have a beautiful photo spread... well, almost beautiful.

Annnnndddd let's get rid of that awful scrollbar at the bottom of the photoStrip by adding the ::-webkit-scrollbar CSS definition:

Finally, let's add a small margin between the photos so we can see they are different photos instead of a single one:

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?

Let's 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), let's move the menu to below the detail of a specific location. In order to handle this, we'll 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 we're printing by using the sample CSS media query:

In our postcss setup, we're 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, let's 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, we'll 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. We'll 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, we'll need to add the display: flex rule to the parent component of an element.

For us, this means we'll set the display: flex; rule on the wrapper of the entire page (since we're using flexbox on every element in our Main view). We set this in our wrapper class previously.

flex-direction

Refreshing our browser, we'll 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 doesn't quite work as well. Let's 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, let's 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, we'll use the @custom-media definition to define the screen for phones vs. those which are larger:

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, let's apply the media query to set the flex-direction back to column when we're on a larger screen.

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 we'll discuss in-depth here.

order

In order to set the sidebar to play nicely with the rest of our content box, let's apply the same principles of ordering and flex to the sidebar (except in reverse):

Back in the browser, refreshing the page, we'll see that the layout looks even at either mobile or larger.

Making the Map Page the Index

One final note before we end our app, when our user visits the page for the first time, they'll end up at a blank page. This is because we haven't defined an index route.

Refreshing the page and navigating to the root at http://localhost:3000, we'll 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.

We've placed the entire app on github at fullstackreact/yelp-clone.

If you're stuck, have further questions, feel free to reach out to us by:

Fullstack React: React Tutorial: Cloning Yelp


Tag cloud