• No results found

(Full Stack React)-BuildingYelp.pdf

N/A
N/A
Protected

Academic year: 2021

Share "(Full Stack React)-BuildingYelp.pdf"

Copied!
113
0
0

Loading.... (view fulltext now)

Full text

(1)

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

(2)

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

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

(3)

7. Building the Main Pane

01. Creating Markers On Our Map

02. Clicking on Markers

03. Adding More Details to Markers

04. Demo: Explore Google Places JSON API

05. Going Responsive

α. display: flex

β. flex-direction

γ. order

8. Making the Map Page the Index

9. Conclusion

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.

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,

(4)

Let’s 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, 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.

(5)

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 core preset-es2015 preset-react babel-preset-react-hmre babel-preset-stage-0

(6)

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.

$ touch .babelrc

Let’s 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 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).

{

"presets": ["es2015", "stage-0", "react"], "env": { "development": { "presets": ["react-hmre"] } } }

(7)

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-url-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:

(8)

const fs = require('fs'); const path = require('path'), join = path.join,

resolve = path.resolve;

const getConfig = require('hjs-webpack');

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, we’ll usually create a few path variables to help us optimize our configuration when we start modifying it from it’s default setup.

const root = resolve(__dirname); const src = join(root, 'src');

const modules = join(root, 'node_modules'); const dest = join(root, 'dist');

var config = getConfig({ in: join(src, 'app.js'), out: dest,

clearBeforeBuild: true })

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 we’re working in the development environment called isDev.

(9)

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’:

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 won’t 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;

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.

(10)

$ npm install --save react-router

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

$ npm i -S [dependencies]

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.:

$ npm i -D [dependencies]

Creating Our app.js

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.

(11)

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:

import React from 'react'

import ReactDOM from 'react-dom' const App = React.createClass({ render: function() {

return (<div>Text text text</div>); }

});

Demo: Basic app.js

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? The hjs-webpack package includes a minimal index.html it will generate if we don’t describe a custom one (using the html key in the configuration object). We won’t need to generate a custom html file here, so instead we’ll use the basic generated page. The default template includes a <div /> component with an id of root just for the purpose of setting up a Single Page App (SPA, for short). 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:

import React from 'react'

import ReactDOM from 'react-dom' const App = React.createClass({ render: function() {

return (<div>Text text text</div>) }

});

const mountNode = document.querySelector('#root'); ReactDOM.render(<App />, mountNode);

With the src/app.js in place, let’s 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

(12)

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.

(13)

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.

The package.json has an entry that allows us to add scripts, not surprisingly called the scripts key. Let’s 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

All other scripts require the run command to be executed.

postcss

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:

(14)

$ 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 precsspackage, 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.

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({ // ...

})

(15)

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.

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:

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. 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:

.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 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.

import React from 'react'

import ReactDOM from 'react-dom'

import styles from './styles.module.css' const App = React.createClass({

(16)

});

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> ); } }); // ...

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:

// ...

const cssModulesNames = `${isDev ? '[path][name]__[local]__' : ''}[hash:base64:5]`; // ...

(17)

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:

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, let’s 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, 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

(18)

config.module.loaders.push({ test: /\.css$/,

include: [modules], loader: 'style!css' })

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.

$ echo "body{ border: 1px solid red;}" > src/app.css

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

// ...

import './app.css' // ...

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

(19)

To confirm our css module loading works as expected, let’s 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.

(20)

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:

$ npm i -D dotenv && touch .env

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, let’s load up the .env file in our environment:

// ...

(21)

const dotEnvVars = dotenv.config();

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:

// ...

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, 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.

(22)

It’s 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.

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:

// ...

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

(23)

//...

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, we’ll 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, 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:

(24)

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:

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:

(25)

Demo: Environment

Webpack Tip: Relative requires

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.

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

(26)

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.

(27)

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:

$ karma init

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.

(28)

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, 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:

module.exports = function(config) { config.set({ // ... basePath: '', preprocessors: {}, port: 9876, colors: true, logLevel: config.LOG_INFO, browsers: ['Chrome'], concurrency: Infinity, plugins: [] }); }

(29)

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.

module.exports = function(config) { config.set({

frameworks: ['mocha', 'chai'], basePath: '', plugins: [ 'karma-mocha', 'karma-chai' ], // ... }); }

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.

var webpackConfig = require('./webpack.config'); module.exports = function(config) {

config.set({

frameworks: ['mocha', 'chai'], webpack: webpackConfig, webpackServer: { noInfo: true }, // ... }); }

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.

module.exports = function(config) { config.set({

frameworks: ['mocha', 'chai'], webpack: webpackConfig, webpackServer: { noInfo: true }, plugins: [ 'karma-mocha', 'karma-chai', 'karma-webpack'

(30)

], // ... }); }

Let’s 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, 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:

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, we’ll 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, 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):

(31)

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' ], // ... }); }

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:

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 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:

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);

(32)

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'] }) };

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.

(33)

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.

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, 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

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

(34)

import styles from './styles.module.css' const App = React.createClass({

render: function() { return (

<div className={styles.wrapper}> <h1>

<i className="fa fa-star"></i> Environment: {__NODE_ENV__}</h1> </div> ) } }); module.exports = App;

Finally, we’ll 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, we’ll use the karma command installed in our ./node_modules directory by our previous npm install:

$ NODE_ENV=test \

(35)

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:

// ./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 } // ...

(36)

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.

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, let’s 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; }) } // ...

(37)

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.

{ "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" },

// ... }

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

(38)

Our previous test is not passing because although we have defined a .wrapper{} class in our

src/containers/App/styles.module.css file, it’s empty so webpack just discards the class and styles.wrapper ends up undefined. In order to get our test passing, we’ll need to add a description to it.

.wrapper {

display: flex; }

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:

{

"name": "yelp", "version": "1.0.0", "description": "", "scripts": {

(39)

"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, we’ll launch our test watcher by using npm run test:watch. We’ll 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 it’s configuration object. We can set this using a command-line parser called yargs. Let’s 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.

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.

(40)

In src/contains/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:

import { expect } from 'chai' import { shallow } from 'enzyme' import App from './App'

describe('<App />', () => { // define our tests in here })

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:

// ...

describe('<App />', () => { // define our tests in here

let wrapper; // "dom" node wrapper element beforeEach(() => {

wrapper = shallow(<App />); })

(41)

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, we’ll 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

(42)

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.js, let’s make sure we import the react-router library.

import React, { PropTypes } from 'react'; import { Router } from 'react-router'; // ...

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):

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'

(43)

class App extends React.Component { // class getter content() { return (<Router />) } render() { return (

<div style=\{\{ height: '100%' \}\}> {this.content}

</div> )

} }

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.

There are multiple different 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.

We’ll 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 (

(44)

{this.content} </div>

) } }

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:

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, 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.

import React from 'react'

import ReactDOM from 'react-dom'

import 'font-awesome/css/font-awesome.css' import './app.css'

(45)

import {browserHistory} from 'react-router' import App from 'containers/App/App'

const mountNode = document.querySelector('#root'); ReactDOM.render(

<App history={browserHistory} />, mountNode);

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: <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>

) //...

(46)

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:

// ...

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 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.

(47)

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.

$ touch src/routes.js

In this src/routes.js file, let’s create and export a function to create and export the routes JSX instance rather. Let’s 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({

(48)

return (<div>Hello world</div>) }

})

export const makeRoutes = () => ( <Router>

<Route path="/" component={Home} /> <Redirect from="*" to="/" />

</Router> )

export default makeRoutes

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);

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.

(49)

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/:

$ mkdir -p src/views/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

$ touch src/views/Main/{Container,routes}

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.

import React from 'react'

import {Route} from 'react-router' import Container from './Container' export const makeMainRoutes = () => { return (

(50)

<Route path="" component={Container} /> )

}

export default makeMainRoutes;

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:

// 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, 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.

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

(51)

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 {{ name }}.

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

(52)

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.

Before we can use the <Map /> component, we’ll need to grab a google API key. In our webpack set up, we’re 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, let’s set the GAPI_KEY to our key:

GAPI_KEY=abc123

To use our GAPI_KEY, we’ll reference it in our code surrounded by underscores (i.e.: __GAPI_KEY__). Before we can start using the <Map /> component, we’ll 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.

(53)

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:

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

(54)

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:

export class Container extends React.Component { onReady(mapProps, map) {

// When the map is ready and mounted }

render() { return ( <div> <Map

(55)

onReady={this.onReady.bind(this)} google={this.props.google} /> </div> ) } }

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:

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

}

(56)

return ( <div> <Map onReady={this.onReady.bind(this)} google={this.props.google} /> </div> ) } }

Since we’ll be storing a new state of the <Container /> so we can stave the new results in our <Container />, let’s 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

}) } // ... }

(57)

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

References

Related documents

This training covers care manager fundamentals, key issues in geriatrics, and care and management of chronic disease. In particular, CM + care managers

In order to encourage the voluntary disclosure and payment of taxes owed to the State, the Legislature has authorized the Oklahoma Tax Commission to establish

– H.320 suite (ISDN) is being replaced by H.323 (IP) – SIP is emerging as an alternate signaling scheme.. • ISDN still makes up the majority of “formal”

In this article, we identified that small-sided games with a small number of players, bigger field dimensions, without goalkeepers, with touch limitations, and with coach

Full body, lush and complex with dried apple and toasted aromas, undertones of apple, pear, peach, nuts and Citrus

Magiera (2002) observed 11 co-taught middle school classes to document how students with dis- abilities spent their time and concluded that targeted stu- dents interacted more with

My vision is that we build a knowledge graph to represent scientific information, linking data that have been traditionally represented in text to open up research.. It would