├── Chapter01 ├── 1-1-learn-react │ ├── .npmrc │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ └── index.js └── 1-1-react-router │ ├── .babelrc │ ├── .npmrc │ ├── package.json │ ├── public │ ├── index.ejs │ └── index.html │ ├── server.js │ └── src │ ├── components │ ├── Index.js │ └── List.js │ ├── index.js │ └── routes.js ├── Chapter02 ├── 2-2-basics │ ├── .npmrc │ ├── components │ │ ├── Btn.js │ │ ├── Nav.css │ │ └── Nav.js │ ├── data │ │ └── posts.js │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _document.js │ │ ├── index.js │ │ └── second.js │ ├── server.js │ └── static │ │ ├── js.jpg │ │ └── js.png └── 2-3-configuration │ ├── .npmrc │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _document.js │ ├── index.sass │ └── index.tsx │ └── tsconfig.json ├── Chapter04 ├── 4-4-graphql │ ├── .babelrc │ ├── .gitignore │ ├── .graphqlconfig │ ├── .npmrc │ ├── components │ │ └── environment.js │ ├── package.json │ └── pages │ │ └── index.js └── 4-5-apollo │ ├── .npmrc │ ├── components │ └── withData.js │ ├── package.json │ └── pages │ └── index.js ├── Chapter05 ├── 5-1-auth │ ├── .npmrc │ ├── lib │ │ └── redux.js │ ├── next.config.js │ ├── package.json │ ├── pages │ │ └── index.js │ ├── server.js │ └── users.js ├── 5-2-rbac │ ├── .npmrc │ ├── lib │ │ ├── pages.js │ │ ├── rbac.js │ │ ├── redux.js │ │ └── withRbac.js │ ├── package.json │ ├── pages │ │ └── index.js │ └── server.js ├── 5-3-rnp │ ├── .npmrc │ ├── lib │ │ ├── bl.js │ │ ├── pages.js │ │ ├── rbac.js │ │ ├── redux.js │ │ └── withRbac.js │ ├── package.json │ ├── pages │ │ └── index.js │ └── server.js ├── 5-4-i18n │ ├── .npmrc │ ├── components │ │ ├── Common.js │ │ └── Other.js │ ├── lib │ │ └── withI18n.js │ ├── package.json │ ├── pages │ │ ├── index.js │ │ └── other.js │ └── static │ │ └── locales │ │ ├── en │ │ ├── common.json │ │ └── other.json │ │ └── es │ │ ├── common.json │ │ └── other.json ├── 5-5-error │ ├── .npmrc │ ├── package.json │ └── pages │ │ ├── _error.js │ │ ├── boundary.js │ │ ├── index.js │ │ └── unhandledError.js ├── 5-6-caching │ ├── .npmrc │ ├── lib │ │ ├── redux.js │ │ └── withPersistGate.js │ ├── package.json │ └── pages │ │ ├── gated.js │ │ └── index.js └── 5-7-analytics │ ├── .npmrc │ ├── lib │ └── withGA.js │ ├── package.json │ └── pages │ ├── index.js │ └── that.js ├── Chapter06 ├── 6-2-unit-tests │ ├── .babelrc │ ├── .npmrc │ ├── jest.config.js │ ├── jest.setup.js │ ├── lib │ │ ├── index.js │ │ └── index.test.js │ ├── package.json │ └── pages │ │ ├── __snapshots__ │ │ └── index.test.js.snap │ │ ├── index.js │ │ └── index.test.js ├── 6-3-e2e-tests │ ├── .gitignore │ ├── .npmrc │ ├── jest-puppeteer.config.js │ ├── jest.config.js │ ├── package.json │ └── pages │ │ ├── index.js │ │ └── index.test.js ├── 6-4-ci │ ├── .gitignore │ ├── .gitlab-ci.yml │ ├── .npmrc │ ├── .travis.yml │ ├── jest-puppeteer.config.js │ ├── jest.config.js │ ├── package.json │ └── pages │ │ ├── index.js │ │ └── index.test.js ├── 6-5-coveralls │ ├── .babelrc │ ├── .gitignore │ ├── .npmrc │ ├── jest.config.js │ ├── package.json │ └── pages │ │ ├── index.js │ │ └── index.test.js └── 6-6-hooks │ ├── .babelrc │ ├── .gitignore │ ├── .npmrc │ ├── jest.config.js │ ├── package.json │ └── pages │ ├── index.js │ └── index.test.js ├── Chapter07 ├── 7-2-docker │ ├── ssr │ │ ├── .npmrc │ │ ├── Dockerfile │ │ ├── package.json │ │ └── pages │ │ │ └── index.js │ └── static │ │ ├── .npmrc │ │ ├── Dockerfile │ │ ├── next.config.js │ │ ├── package.json │ │ └── pages │ │ └── index.js ├── 7-3-heroku │ ├── .npmrc │ ├── .travis.yml │ ├── Procfile │ ├── jest.config.js │ ├── package.json │ └── pages │ │ ├── index.js │ │ └── index.test.js └── 7-4-now │ ├── .npmrc │ ├── .travis.yml │ ├── jest.config.js │ ├── package.json │ └── pages │ ├── index.js │ └── index.test.js ├── LICENSE ├── README.md └── book ├── 1-introduction.md ├── 2-basics.md ├── 3-configuration.md ├── 4-data-flow.md ├── 5-app-lifecycle-and-business-logic.md ├── 6-ci.md └── 7-deployment.md /Chapter01/1-1-learn-react/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter01/1-1-learn-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "1-1-learn-react", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "react-scripts start" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "react": "16.2.0", 13 | "react-dom": "16.2.0" 14 | }, 15 | "devDependencies": { 16 | "react-scripts": "1.1.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Chapter01/1-1-learn-react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Learn React 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /Chapter01/1-1-learn-react/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {render} from "react-dom"; 3 | 4 | const App = () => (
It works!
); 5 | 6 | render(, document.getElementById('app')); 7 | -------------------------------------------------------------------------------- /Chapter01/1-1-react-router/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react-app" 5 | ] 6 | } -------------------------------------------------------------------------------- /Chapter01/1-1-react-router/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter01/1-1-react-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "1-1-react-router", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "react-scripts start", 8 | "server": "NODE_ENV=development babel-node server.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "react": "16.2.0", 14 | "react-dom": "16.2.0", 15 | "react-router-config": "1.0.0-beta.4", 16 | "react-router-dom": "4.2.2" 17 | }, 18 | "devDependencies": { 19 | "babel-cli": "6.26.0", 20 | "ejs": "2.5.7", 21 | "express": "4.16.2", 22 | "node-fetch": "2.0.0", 23 | "react-scripts": "1.1.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Chapter01/1-1-react-router/public/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%-title%> 6 | 7 | 8 |
<%-content%>
9 | 10 | -------------------------------------------------------------------------------- /Chapter01/1-1-react-router/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Learn React 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /Chapter01/1-1-react-router/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import fetch from 'node-fetch'; 3 | 4 | import React from 'react'; 5 | import {renderToString} from 'react-dom/server'; 6 | 7 | import StaticRouter from 'react-router-dom/StaticRouter'; 8 | import {matchRoutes, renderRoutes} from 'react-router-config'; 9 | 10 | import routes from './src/routes'; 11 | 12 | global.fetch = fetch; 13 | 14 | const app = express(); 15 | const port = 3000; 16 | 17 | app.set('view engine', 'ejs'); 18 | app.set('views', process.cwd() + '/public'); 19 | 20 | app.get('*', (req, res) => { 21 | 22 | const {url} = req; 23 | const matches = matchRoutes(routes, url); 24 | const context = {}; 25 | 26 | const promises = matches.map(({route}) => { 27 | const getInitialProps = route.component.getInitialProps; 28 | return getInitialProps ? getInitialProps(context) : Promise.resolve(null) 29 | }); 30 | 31 | return Promise.all(promises).then(() => { 32 | 33 | console.log('Context', context); 34 | 35 | const content = renderToString( 36 | 37 | {renderRoutes(routes)} 38 | 39 | ); 40 | 41 | res.render('index', {title: 'SSR', content}); 42 | 43 | }); 44 | 45 | }); 46 | 47 | app.listen(port, listen); 48 | 49 | function listen(err) { 50 | if (err) throw err; 51 | console.log('Listening %s', port); 52 | } 53 | 54 | -------------------------------------------------------------------------------- /Chapter01/1-1-react-router/src/components/Index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default ({children}) => ( 4 |
Index
5 | ); -------------------------------------------------------------------------------- /Chapter01/1-1-react-router/src/components/List.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const getText = async () => (await (await fetch('https://api.github.com/users/octocat')).text()); 4 | 5 | export default class List extends React.Component { 6 | 7 | state = {text: ''}; 8 | 9 | static async getInitialProps(context) { 10 | context.text = await getText(); 11 | } 12 | 13 | async componentWillMount() { 14 | const text = await getText(); 15 | this.setState({text}) 16 | } 17 | 18 | render() { 19 | 20 | const {staticContext} = this.props; 21 | let {text} = this.state; 22 | 23 | if (staticContext && !text) text = staticContext.text; 24 | 25 | return ( 26 |
Text: {text}
27 | ); 28 | 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /Chapter01/1-1-react-router/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | 4 | import BrowserRouter from 'react-router-dom/BrowserRouter'; 5 | import {renderRoutes} from 'react-router-config'; 6 | 7 | import routes from './routes'; 8 | 9 | const Router = () => { 10 | return ( 11 | 12 | {renderRoutes(routes)} 13 | 14 | ) 15 | }; 16 | 17 | render(, document.getElementById('app')); -------------------------------------------------------------------------------- /Chapter01/1-1-react-router/src/routes.js: -------------------------------------------------------------------------------- 1 | import Index from "./components/Index"; 2 | import List from "./components/List"; 3 | 4 | const routes = [ 5 | { 6 | path: '/', 7 | exact: true, 8 | component: Index 9 | }, 10 | { 11 | path: '/list', 12 | component: List 13 | } 14 | ]; 15 | 16 | export default routes; -------------------------------------------------------------------------------- /Chapter02/2-2-basics/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter02/2-2-basics/components/Btn.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {withRouter} from 'next/router'; 3 | 4 | export default withRouter(({href, onClick, children, router}) => ( 5 | 6 | 7 | 22 | 23 | )); -------------------------------------------------------------------------------- /Chapter02/2-2-basics/components/Nav.css: -------------------------------------------------------------------------------- 1 | nav { 2 | background: #f6f6f6; 3 | } 4 | 5 | .logo-css { 6 | background: url(/static/js.jpg) no-repeat center center; 7 | background-size: cover; 8 | } 9 | 10 | .logo { 11 | display: inline-block; 12 | width: 32px; 13 | height: 32px; 14 | } -------------------------------------------------------------------------------- /Chapter02/2-2-basics/components/Nav.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Btn from "./Btn"; 3 | import Link from 'next/link'; 4 | import PNG from '../static/js.png'; 5 | import './Nav.css'; 6 | 7 | export default () => ( 8 | 14 | ); -------------------------------------------------------------------------------- /Chapter02/2-2-basics/data/posts.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | {title: 'Foo'}, 3 | {title: 'Bar'}, 4 | {title: 'Baz'}, 5 | {title: 'Qux'} 6 | ] -------------------------------------------------------------------------------- /Chapter02/2-2-basics/next.config.js: -------------------------------------------------------------------------------- 1 | const withCss = require('@zeit/next-css'); 2 | const withImages = require('next-images'); 3 | 4 | module.exports = withCss(withImages({})); -------------------------------------------------------------------------------- /Chapter02/2-2-basics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "2-2-basics", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@zeit/next-css": "0.1.2", 13 | "express": "4.16.2", 14 | "next": "^5.0.0", 15 | "next-images": "0.9.2" 16 | }, 17 | "dependencies": { 18 | "react": "^16.2.0", 19 | "react-dom": "^16.2.0", 20 | "react-vis": "1.8.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Chapter02/2-2-basics/pages/_document.js: -------------------------------------------------------------------------------- 1 | // ./pages/_document.js 2 | import Document, {Head, Main, NextScript} from 'next/document'; 3 | 4 | export default class MyDocument extends Document { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | NextJS Condensed 11 | 12 | 13 |
14 | 15 | 16 | 17 | ) 18 | } 19 | } -------------------------------------------------------------------------------- /Chapter02/2-2-basics/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from "next/link"; 3 | import Nav from "../components/Nav"; 4 | import posts from "../data/posts"; 5 | import {HorizontalGridLines, LineSeries, XAxis, XYPlot, YAxis} from 'react-vis'; 6 | import "react-vis/dist/style.css"; 7 | 8 | //@import "./node_modules/react-vis/dist/styles/legends"; 9 | 10 | export default () => ( 11 |
12 | 13 |
43 | ); -------------------------------------------------------------------------------- /Chapter02/2-2-basics/pages/second.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Error from 'next/error'; 3 | import Nav from "../components/Nav"; 4 | import posts from "../data/posts"; 5 | 6 | export default ({url: {query: {id}}}) => ( 7 | (posts[id]) ? ( 8 |
9 |
13 | ) : ( 14 | 15 | ) 16 | ); -------------------------------------------------------------------------------- /Chapter02/2-2-basics/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const next = require('next'); 3 | 4 | const port = 3000; 5 | const dev = process.env.NODE_ENV !== 'production'; // use default NodeJS environment variable to figure out dev mode 6 | const app = next({dev}); 7 | const handle = app.getRequestHandler(); 8 | const server = express(); 9 | 10 | server.get('/post/:id', (req, res) => { 11 | const actualPage = '/second'; 12 | const queryParams = {id: req.params.id}; 13 | app.render(req, res, actualPage, queryParams); 14 | }); 15 | 16 | server.get('*', (req, res) => { // pass through everything to NextJS 17 | return handle(req, res) 18 | }); 19 | 20 | app.prepare().then(() => { 21 | 22 | server.listen(port, (err) => { 23 | if (err) throw err; 24 | console.log('NextJS is ready on http://localhost:' + port); 25 | }); 26 | 27 | }).catch(e => { 28 | 29 | console.error(e.stack); 30 | process.exit(1); 31 | 32 | }); -------------------------------------------------------------------------------- /Chapter02/2-2-basics/static/js.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Next.js-Quick-Start-Guide/fb5a0549e8b39752f89db19600b610311ecd535d/Chapter02/2-2-basics/static/js.jpg -------------------------------------------------------------------------------- /Chapter02/2-2-basics/static/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Next.js-Quick-Start-Guide/fb5a0549e8b39752f89db19600b610311ecd535d/Chapter02/2-2-basics/static/js.png -------------------------------------------------------------------------------- /Chapter02/2-3-configuration/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter02/2-3-configuration/next.config.js: -------------------------------------------------------------------------------- 1 | const withSass = require('@zeit/next-sass'); 2 | const withTypescript = require('@zeit/next-typescript'); 3 | 4 | const {PHASE_DEVELOPMENT_SERVER} = require('next/constants'); 5 | 6 | const addPlugins = (config) => withTypescript(withSass(config)); 7 | 8 | const additionalConfig = { 9 | serverRuntimeConfig: { 10 | serverOnly: 'secret' 11 | }, 12 | publicRuntimeConfig: { 13 | serverAndClient: 'public' 14 | }, 15 | // Uncomment if you don't use withTypescript plugin, but this is a bad choice :) 16 | // pageExtensions: ['jsx', 'js', 'tsx', 'ts'], 17 | // webpack(config, {dir, defaultLoaders}) { 18 | // config.resolve.extensions.push('.ts', '.tsx'); 19 | // config.module.rules.push({ 20 | // test: /\.+(ts|tsx)$/, 21 | // include: [dir], 22 | // exclude: /node_modules/, 23 | // use: [ 24 | // defaultLoaders.babel, 25 | // { 26 | // loader: 'ts-loader', 27 | // options: { 28 | // transpileOnly: true 29 | // } 30 | // } 31 | // ] 32 | // }); 33 | // return config; 34 | // } 35 | }; 36 | 37 | module.exports = (phase, {defaultConfig}) => { 38 | 39 | if (phase === PHASE_DEVELOPMENT_SERVER) { 40 | return addPlugins(Object.assign({}, defaultConfig, additionalConfig)); 41 | } 42 | 43 | return addPlugins(Object.assign({}, defaultConfig, additionalConfig, { 44 | distDir: 'build-custom' 45 | })); 46 | 47 | }; -------------------------------------------------------------------------------- /Chapter02/2-3-configuration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "2-2-basics", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "next", 8 | "build": "next build", 9 | "server": "next start" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@types/next": "2.4.8", 15 | "@types/react": "16.0.40", 16 | "@zeit/next-sass": "0.1.1", 17 | "@zeit/next-typescript": "0.0.10", 18 | "next": "5.0.1-canary.11", 19 | "node-sass": "4.7.2", 20 | "ts-loader": "3.5.0", 21 | "typescript": "2.7.2" 22 | }, 23 | "dependencies": { 24 | "react": "^16.2.0", 25 | "react-dom": "^16.2.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Chapter02/2-3-configuration/pages/_document.js: -------------------------------------------------------------------------------- 1 | // ./pages/_document.js 2 | import Document, {Head, Main, NextScript} from 'next/document'; 3 | 4 | export default class MyDocument extends Document { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | NextJS Condensed 11 | 12 | 13 |
14 | 15 | 16 | 17 | ) 18 | } 19 | } -------------------------------------------------------------------------------- /Chapter02/2-3-configuration/pages/index.sass: -------------------------------------------------------------------------------- 1 | body 2 | font-family: Arial, sans-serif 3 | font-size: 12px -------------------------------------------------------------------------------- /Chapter02/2-3-configuration/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import getConfig from 'next/config' 3 | import "./index.sass"; 4 | 5 | const {serverRuntimeConfig, publicRuntimeConfig} = getConfig(); 6 | 7 | console.log({serverRuntimeConfig, publicRuntimeConfig}); 8 | 9 | export default () => ( 10 |
11 | Styled text 12 |
{JSON.stringify(serverRuntimeConfig, null, 2)}
13 |
{JSON.stringify(publicRuntimeConfig, null, 2)}
14 |
15 | ); -------------------------------------------------------------------------------- /Chapter02/2-3-configuration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "jsx": "preserve", 7 | "allowJs": true, 8 | "moduleResolution": "node", 9 | "allowSyntheticDefaultImports": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "removeComments": false, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "skipLibCheck": true, 16 | "baseUrl": ".", 17 | "typeRoots": [ 18 | "./node_modules/@types" 19 | ], 20 | "lib": [ 21 | "dom", 22 | "es2015", 23 | "es2016" 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /Chapter04/4-4-graphql/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "passPerPreset": true, 3 | "presets": [ 4 | "next/babel" 5 | ], 6 | "plugins": [ 7 | "relay" 8 | ] 9 | } -------------------------------------------------------------------------------- /Chapter04/4-4-graphql/.gitignore: -------------------------------------------------------------------------------- 1 | data/schema.graphql 2 | __generated__ -------------------------------------------------------------------------------- /Chapter04/4-4-graphql/.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "schemaPath": "data/schema.graphql", 3 | "extensions": { 4 | "endpoints": { 5 | "dev": "https://swapi.graph.cool" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Chapter04/4-4-graphql/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter04/4-4-graphql/components/environment.js: -------------------------------------------------------------------------------- 1 | import 'isomorphic-fetch'; 2 | import {Environment, Network, RecordSource, Store} from 'relay-runtime'; 3 | import config from 'json-loader!../.graphqlconfig'; 4 | 5 | const fetchQuery = async (operation, variables) => { 6 | 7 | const res = await fetch(config.extensions.endpoints.dev, { 8 | method: 'POST', 9 | headers: { 10 | 'Content-Type': 'application/json' 11 | }, 12 | body: JSON.stringify({ 13 | query: operation.text, 14 | variables, 15 | }), 16 | }); 17 | 18 | return await res.json(); 19 | 20 | }; 21 | 22 | let environment; 23 | 24 | export const getEnviroment = (records) => { 25 | if (!environment || !process.browser) { 26 | environment = new Environment({ 27 | network: Network.create(fetchQuery), 28 | store: new Store(new RecordSource(records)), 29 | }); 30 | } 31 | return environment; 32 | }; 33 | 34 | export default getEnviroment; -------------------------------------------------------------------------------- /Chapter04/4-4-graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3-4-graphql", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start:next": "next", 8 | "start:replay": "npm run relay -- --watch", 9 | "start": "npm run schema && npm-run-all -p start:*", 10 | "build": "npm run schema && npm run relay && next build", 11 | "server": "next start", 12 | "schema": "graphql get-schema", 13 | "relay": "relay-compiler --src ./pages/ --schema data/schema.graphql" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "babel-plugin-relay": "1.5.0", 19 | "graphql-cli": "2.15.8", 20 | "next": "5.0.1-canary.11", 21 | "npm-run-all": "4.1.2", 22 | "relay-compiler": "1.5.0" 23 | }, 24 | "dependencies": { 25 | "isomorphic-fetch": "2.2.1", 26 | "react": "16.2.0", 27 | "react-dom": "16.2.0", 28 | "react-relay": "1.5.0", 29 | "relay-runtime": "1.5.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Chapter04/4-4-graphql/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {fetchQuery, graphql, QueryRenderer} from 'react-relay'; 3 | import getEnvironment from "../components/environment"; 4 | 5 | const query = graphql` 6 | query pagesFilmsQuery { 7 | allFilms { 8 | id, 9 | director, 10 | title, 11 | characters { 12 | name 13 | } 14 | } 15 | } 16 | `; 17 | 18 | const Films = ({error, allFilms = null}) => { 19 | 20 | if (error) return ( 21 |
Error! {error.message}
22 | ); 23 | 24 | if (!allFilms) return ( 25 |
Loading...
26 | ); 27 | 28 | return ( 29 |
30 | {allFilms.map(film => ( 31 |
32 |

{film.title}

33 |

Director: {film.director}

34 |

Characters: {film.characters.map(c => c.name).join(', ')}

35 |
36 | ))} 37 |
38 | ); 39 | 40 | }; 41 | 42 | class Index extends React.Component { 43 | 44 | constructor(props, context){ 45 | super(props, context); 46 | this.environment = getEnvironment(props.records); 47 | } 48 | 49 | render() { 50 | const {props, records} = this.props; 51 | return ( 52 |
53 |

On Server

54 | 55 |
{JSON.stringify(records, null, 2)}
56 |
57 |

On Client

58 | ( 63 | 64 | )} 65 | /> 66 |
67 | ); 68 | } 69 | 70 | } 71 | 72 | Index.getInitialProps = async () => { 73 | const environment = getEnvironment(); 74 | const props = await fetchQuery(environment, query, {}); 75 | const records = environment.getStore().getSource().toJSON(); // we use this to pre-populate the store on client 76 | return { 77 | props, records 78 | }; 79 | }; 80 | 81 | export default Index; -------------------------------------------------------------------------------- /Chapter04/4-5-apollo/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter04/4-5-apollo/components/withData.js: -------------------------------------------------------------------------------- 1 | import {withData} from 'next-apollo' 2 | import {HttpLink} from 'apollo-link-http' 3 | 4 | const config = { 5 | link: new HttpLink({ 6 | uri: 'https://swapi.graph.cool', 7 | opts: { 8 | credentials: 'same-origin' 9 | } 10 | }) 11 | }; 12 | 13 | export default withData(config); -------------------------------------------------------------------------------- /Chapter04/4-5-apollo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3-5-apollo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "next" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "next": "5.0.1-canary.11" 13 | }, 14 | "dependencies": { 15 | "apollo-link-http": "1.5.3", 16 | "graphql-tag": "2.8.0", 17 | "next-apollo": "1.0.13", 18 | "react": "16.2.0", 19 | "react-apollo": "2.0.4", 20 | "react-dom": "16.2.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Chapter04/4-5-apollo/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {graphql} from 'react-apollo'; 3 | import gql from 'graphql-tag'; 4 | import withData from "../components/withData"; 5 | 6 | const query = gql` 7 | query { 8 | allFilms { 9 | id, 10 | director, 11 | title, 12 | characters { 13 | name 14 | } 15 | } 16 | } 17 | `; 18 | 19 | let Index = ({data: {loading, allFilms, error}}) => { 20 | 21 | if (error) return ( 22 |
Error! {error.message}
23 | ); 24 | 25 | if (loading) return ( 26 |
Loading...
27 | ); 28 | 29 | return ( 30 |
31 | {allFilms.map(film => ( 32 |
33 |

{film.title}

34 |

Director: {film.director}

35 |

Characters: {film.characters.map(c => c.name).join(', ')}

36 |
37 | ))} 38 |
39 | ); 40 | 41 | }; 42 | 43 | Index = graphql(query)(Index); 44 | Index = withData(Index); 45 | 46 | export default Index; -------------------------------------------------------------------------------- /Chapter05/5-1-auth/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter05/5-1-auth/lib/redux.js: -------------------------------------------------------------------------------- 1 | import {applyMiddleware, createStore} from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import logger from 'redux-logger'; 4 | 5 | const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; 6 | const LOGIN_ERROR = 'LOGIN_ERROR'; 7 | const LOGOUT = 'LOGOUT'; 8 | const DEFAULT_STATE = { 9 | user: null, 10 | token: null, 11 | error: null 12 | }; 13 | 14 | export const reducer = (state = DEFAULT_STATE, {type, payload}) => { 15 | switch (type) { 16 | case LOGIN_SUCCESS: 17 | return { 18 | ...state, 19 | ...DEFAULT_STATE, 20 | user: payload.user, 21 | token: payload.token 22 | }; 23 | case LOGOUT: 24 | return { 25 | ...state, 26 | ...DEFAULT_STATE 27 | }; 28 | case LOGIN_ERROR: 29 | return { 30 | ...state, 31 | ...DEFAULT_STATE, 32 | error: payload 33 | }; 34 | default: 35 | return state 36 | } 37 | }; 38 | 39 | const SERVER = 'http://localhost:3000'; 40 | 41 | const apiRequest = ({url, body = undefined, method = 'GET'}) => fetch(SERVER + url, { 42 | method, 43 | body: typeof body === 'undefined' ? body : JSON.stringify(body), 44 | headers: {'content-type': 'application/json'}, 45 | credentials: 'include' 46 | }); 47 | 48 | export const login = (username, password) => async (dispatch) => { 49 | try { 50 | const res = await apiRequest({ 51 | url: '/api/login', 52 | body: {username, password}, 53 | method: 'POST' 54 | }); 55 | const json = await res.json(); 56 | if (!res.ok) { 57 | dispatch({type: LOGIN_ERROR, payload: json.message}); 58 | return; 59 | } 60 | dispatch({type: LOGIN_SUCCESS, payload: json}); 61 | } catch (e) { 62 | dispatch({type: LOGIN_ERROR, payload: e.message}); 63 | } 64 | }; 65 | 66 | export const logout = () => async (dispatch) => { 67 | try { 68 | await apiRequest({ 69 | url: '/api/logout', 70 | method: 'POST' 71 | }); 72 | } catch (e) {} 73 | dispatch({type: LOGOUT}); 74 | }; 75 | 76 | export const me = () => async (dispatch) => { 77 | // We don't dispatch anything for demo purposes only 78 | try { 79 | const res = await (await apiRequest({url: '/api/me'})).json(); 80 | alert(JSON.stringify(res)); 81 | } catch (e) { 82 | console.error(e.stack); 83 | } 84 | }; 85 | 86 | export const makeStore = (initialState, {isServer, req, debug, storeKey}) => { 87 | if (isServer) { 88 | // only do it now bc on client it should be undefined by default 89 | initialState = initialState || {}; 90 | // server will put things in req 91 | initialState.user = req.user; 92 | initialState.token = req.token; 93 | initialState.error = null; 94 | } 95 | return createStore(reducer, initialState, applyMiddleware(thunk, logger)); 96 | }; -------------------------------------------------------------------------------- /Chapter05/5-1-auth/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack: (config, defaultConfig) => { 3 | config.resolve.symlinks = false; 4 | return config; 5 | } 6 | }; -------------------------------------------------------------------------------- /Chapter05/5-1-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "2-2-basics", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "body-parser": "1.18.2", 13 | "cookie-parser": "1.4.3", 14 | "express": "4.16.2", 15 | "lodash": "4.17.5", 16 | "next": "5.0.0", 17 | "uuid": "3.2.1" 18 | }, 19 | "dependencies": { 20 | "next-redux-wrapper": "1.3.5", 21 | "react": "16.2.0", 22 | "react-dom": "16.2.0", 23 | "react-redux": "5.0.7", 24 | "redux": "3.7.2", 25 | "redux-logger": "3.0.6", 26 | "redux-thunk": "2.2.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Chapter05/5-1-auth/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {login, logout, me, makeStore} from "../lib/redux"; 3 | import withRedux from "next-redux-wrapper"; 4 | 5 | const Index = ({login, logout, me, user, error}) => ( 6 |
7 | {error && (
Login error: {error}
)} 8 | {(!!user ? ( 9 |
10 |

Logged in as {user.username}

11 | 12 | 13 |
14 | ) : ( 15 |
16 |

Not logged in

17 | 18 |
19 | ))} 20 |
21 | ); 22 | 23 | export default withRedux( 24 | makeStore, 25 | (state) => ({user: state.user, error: state.error}), 26 | {login, logout, me} 27 | )(Index); -------------------------------------------------------------------------------- /Chapter05/5-1-auth/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const next = require('next'); 3 | const bodyParser = require('body-parser'); 4 | const cookieParser = require('cookie-parser'); 5 | const users = require('./users'); 6 | const conf = require('./next.config'); 7 | 8 | const port = 3000; 9 | const cookieName = 'token'; 10 | const dev = process.env.NODE_ENV !== 'production'; // use default NodeJS environment variable to figure out dev mode 11 | const app = next({dev, conf}); 12 | const handle = app.getRequestHandler(); 13 | const server = express(); 14 | 15 | const cleanupUser = (user) => { 16 | const newUser = Object.assign({}, user); 17 | delete newUser.password; 18 | return newUser; 19 | }; 20 | 21 | server.use(cookieParser()); 22 | 23 | server.use(bodyParser.json()); 24 | 25 | server.post('/api/login', (req, res) => { 26 | 27 | try { 28 | 29 | console.log('Attempting to login', req.body); 30 | 31 | const authInfo = users.login(req.body.username, req.body.password); 32 | authInfo.user = cleanupUser(authInfo.user); 33 | 34 | res.cookie(cookieName, authInfo.token, { 35 | expires: new Date(Date.now() + 1000 * 60 * 60 * 24), 36 | httpOnly: true 37 | }); 38 | res.send(authInfo); 39 | 40 | } catch (e) { 41 | 42 | console.log('Login error', e.stack); 43 | 44 | res.status(400).send({message: 'Wrong username and/or password'}); 45 | 46 | } 47 | }); 48 | 49 | const authMiddleware = (dieOnError) => (req, res, next) => { 50 | req.user = null; 51 | req.token = null; 52 | try { 53 | req.token = req.cookies[cookieName]; 54 | req.user = cleanupUser(users.findUserByToken(req.token)); 55 | next(); 56 | } catch (e) { 57 | if (dieOnError) { 58 | res.status(401).send({message: 'Not Authorized'}); 59 | } else { 60 | next(); 61 | } 62 | } 63 | }; 64 | 65 | server.post('/api/logout', authMiddleware(false), (req, res) => { 66 | 67 | try { 68 | if (req.token) users.logout(req.token); 69 | } catch (e) {} // ignore errors 70 | 71 | res.clearCookie('token'); 72 | res.send({}); 73 | 74 | }); 75 | 76 | server.get('/api/me', authMiddleware(true), (req, res) => { 77 | res.send(req.user); 78 | }); 79 | 80 | server.get('*', authMiddleware(false), (req, res) => { 81 | // pass through everything to NextJS 82 | return handle(req, res); 83 | }); 84 | 85 | app.prepare().then(() => { 86 | 87 | server.listen(port, (err) => { 88 | if (err) throw err; 89 | console.log('NextJS is ready on http://localhost:' + port); 90 | }); 91 | 92 | }).catch(e => { 93 | 94 | console.error(e.stack); 95 | process.exit(1); 96 | 97 | }); 98 | 99 | exports.server = server; 100 | exports.authMiddleware = authMiddleware; -------------------------------------------------------------------------------- /Chapter05/5-1-auth/users.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid/v4'); 2 | const find = require('lodash/find'); 3 | 4 | const users = [ 5 | {username: 'admin', password: 'foo', group: 'admin'}, 6 | {username: 'user', password: 'foo', group: 'user'}, 7 | ]; 8 | 9 | const tokens = {}; 10 | 11 | const findUserByUsername = (username) => find(users, {username}); 12 | 13 | const findUserByToken = (token) => { 14 | if (!(token in tokens)) throw new Error('Token does not exist'); 15 | return users[tokens[token]]; 16 | }; 17 | 18 | const login = (username, password) => { 19 | 20 | const user = findUserByUsername(username); 21 | 22 | if (!user) throw new Error('Cannot find user'); 23 | 24 | if (user.password !== password) throw new Error('Wrong password'); 25 | 26 | const token = uuid(); 27 | 28 | tokens[token] = users.indexOf(user); 29 | 30 | return { 31 | token, 32 | user 33 | }; 34 | 35 | }; 36 | 37 | const logout = (token) => { 38 | delete tokens[token]; 39 | }; 40 | 41 | exports.findUserByUsername = findUserByUsername; 42 | exports.findUserByToken = findUserByToken; 43 | exports.login = login; 44 | exports.logout = logout; -------------------------------------------------------------------------------- /Chapter05/5-2-rbac/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter05/5-2-rbac/lib/pages.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | user: 'admin', 4 | title: 'Page by Admin' 5 | }, 6 | { 7 | user: 'user', 8 | title: 'Page by User' 9 | } 10 | ]; -------------------------------------------------------------------------------- /Chapter05/5-2-rbac/lib/rbac.js: -------------------------------------------------------------------------------- 1 | const accesscontrol = require('accesscontrol'); 2 | 3 | const ac = new accesscontrol.AccessControl({ 4 | admin: { 5 | page: { 6 | 'create:any': ['*'], 7 | 'read:any': ['*'], 8 | 'update:any': ['*'], 9 | 'delete:any': ['*'] 10 | } 11 | }, 12 | user: { 13 | page: { 14 | 'create:own': ['*'], 15 | 'read:any': ['*'], 16 | 'update:own': ['*'], 17 | 'delete:own': ['*'] 18 | } 19 | } 20 | }); 21 | 22 | const checkGrant = (user, action, resource) => user ? ac.can(user.group)[action](resource).granted : false; 23 | 24 | exports.checkGrant = checkGrant; -------------------------------------------------------------------------------- /Chapter05/5-2-rbac/lib/redux.js: -------------------------------------------------------------------------------- 1 | ../../5-1-auth/lib/redux.js -------------------------------------------------------------------------------- /Chapter05/5-2-rbac/lib/withRbac.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "react-redux"; 3 | 4 | export default (checkCb) => (WrappedComponent) => { 5 | 6 | class WithRbac extends React.Component { 7 | 8 | static async getInitialProps(args) { 9 | 10 | const {user} = args.store.getState(); 11 | 12 | // First time check 13 | const granted = checkCb(user); 14 | 15 | if (!granted && args.res) { 16 | args.res.statusCode = 401; 17 | } 18 | 19 | const additionalArgs = {...args, granted}; 20 | 21 | return WrappedComponent.getInitialProps 22 | ? await WrappedComponent.getInitialProps(additionalArgs) 23 | : {}; 24 | 25 | } 26 | 27 | render() { 28 | 29 | const granted = checkCb(this.props.user); 30 | 31 | // Runtime checks 32 | return ( 33 | 34 | ); 35 | 36 | } 37 | 38 | } 39 | 40 | WithRbac.displayName = `withRbac(${WrappedComponent.displayName 41 | || WrappedComponent.name 42 | || 'Component'})`; 43 | 44 | return connect(state => ({ 45 | user: state.user 46 | }))(WithRbac); 47 | 48 | } -------------------------------------------------------------------------------- /Chapter05/5-2-rbac/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "2-2-basics", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "body-parser": "1.18.2", 13 | "cookie-parser": "1.4.3", 14 | "express": "4.16.2", 15 | "lodash": "4.17.5", 16 | "next": "5.0.0", 17 | "uuid": "3.2.1" 18 | }, 19 | "dependencies": { 20 | "accesscontrol": "2.2.1", 21 | "next-redux-wrapper": "1.3.5", 22 | "react": "16.2.0", 23 | "react-dom": "16.2.0", 24 | "react-redux": "5.0.7", 25 | "recompose": "0.26.0", 26 | "redux": "3.7.2", 27 | "redux-logger": "3.0.6", 28 | "redux-thunk": "2.2.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Chapter05/5-2-rbac/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {login, logout, makeStore} from "../lib/redux.js"; 3 | import withRedux from "next-redux-wrapper"; 4 | import withRbac from "../lib/withRbac"; 5 | import {checkGrant} from "../lib/rbac"; 6 | import pages from "../lib/pages"; 7 | 8 | let Index = ({login, logout, user, error, granted}) => ( 9 |
10 | {error && (
Login error: {error}
)} 11 | {(granted ? ( 12 |
13 |

Logged in as {user.username}

14 | {pages.map((page, index) => ( 15 |
16 | {page.title} 17 | { 18 | checkGrant( 19 | user, 20 | (user.username === page.user ? 'updateOwn' : 'updateAny'), 'page' 21 | ) 22 | ? 'Can Write' 23 | : 'Can Read' 24 | } 25 |
26 | ))} 27 | 28 |
29 | ) : ( 30 |
31 |

Not logged in

32 | 33 | 34 |
35 | ))} 36 |
37 | ); 38 | 39 | Index = withRbac(user => checkGrant(user, 'readAny', 'page'))(Index); 40 | Index = withRedux( 41 | makeStore, 42 | (state) => ({ 43 | user: state.user, 44 | error: state.error 45 | }), 46 | {login, logout} 47 | )(Index); 48 | 49 | export default Index; -------------------------------------------------------------------------------- /Chapter05/5-2-rbac/server.js: -------------------------------------------------------------------------------- 1 | const authServer = require('../5-1-auth/server'); 2 | const rbac = require('./lib/rbac'); 3 | 4 | const rbacMiddleware = (action, resource) => (req, res, next) => { 5 | if (rbac.checkGrant(req.user, action, resource)) { 6 | next(); 7 | } else { 8 | res.status(403).send({message: 'Not enough permissions'}); 9 | } 10 | }; 11 | 12 | authServer.server.post('/api/rbac', authServer.authMiddleware(true), rbacMiddleware('readAny', 'page'), (req, res) => { 13 | res.send({foo: 'bar'}); 14 | }); 15 | 16 | -------------------------------------------------------------------------------- /Chapter05/5-3-rnp/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter05/5-3-rnp/lib/bl.js: -------------------------------------------------------------------------------- 1 | import {checkGrant} from "./rbac"; 2 | 3 | export const canWritePost = (user, post) => 4 | checkGrant(user, (user.username == post.user ? 'updateOwn' : 'updateAny'), 'page'); 5 | 6 | export const canReadPages = (user) => 7 | checkGrant(user, 'readAny', 'page'); -------------------------------------------------------------------------------- /Chapter05/5-3-rnp/lib/pages.js: -------------------------------------------------------------------------------- 1 | ../../5-2-rbac/lib/pages.js -------------------------------------------------------------------------------- /Chapter05/5-3-rnp/lib/rbac.js: -------------------------------------------------------------------------------- 1 | ../../5-2-rbac/lib/rbac.js -------------------------------------------------------------------------------- /Chapter05/5-3-rnp/lib/redux.js: -------------------------------------------------------------------------------- 1 | ../../5-1-auth/lib/redux.js -------------------------------------------------------------------------------- /Chapter05/5-3-rnp/lib/withRbac.js: -------------------------------------------------------------------------------- 1 | ../../5-2-rbac/lib/withRbac.js -------------------------------------------------------------------------------- /Chapter05/5-3-rnp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "2-2-basics", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "body-parser": "1.18.2", 13 | "cookie-parser": "1.4.3", 14 | "express": "4.16.2", 15 | "lodash": "4.17.5", 16 | "next": "5.0.0", 17 | "uuid": "3.2.1" 18 | }, 19 | "dependencies": { 20 | "accesscontrol": "2.2.1", 21 | "next-redux-wrapper": "1.3.5", 22 | "react": "16.2.0", 23 | "react-dom": "16.2.0", 24 | "react-redux": "5.0.7", 25 | "recompose": "0.26.0", 26 | "redux": "3.7.2", 27 | "redux-logger": "3.0.6", 28 | "redux-thunk": "2.2.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Chapter05/5-3-rnp/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {login, logout, makeStore} from "../lib/redux"; 3 | import withRedux from "next-redux-wrapper"; 4 | import withRbac from "../lib/withRbac"; 5 | import pages from "../lib/pages"; 6 | import {canReadPages, canWritePost} from "../lib/bl"; 7 | 8 | let Index = ({login, logout, user, error, granted}) => ( 9 |
10 | {error && (
Login error: {error}
)} 11 | {(granted ? ( 12 |
13 |

Logged in as {user.username}

14 | {pages.map((page, index) => ( 15 |
16 | {page.title} - {canWritePost(user, page) ? 'Can Write' : 'Can Read'} 17 |
18 | ))} 19 | 20 |
21 | ) : ( 22 |
23 |

Not logged in

24 | 25 | 26 |
27 | ))} 28 |
29 | ); 30 | 31 | Index = withRbac(user => canReadPages(user))(Index); 32 | Index = withRedux( 33 | makeStore, 34 | (state) => ({ 35 | user: state.user, 36 | error: state.error 37 | }), 38 | {login, logout} 39 | )(Index); 40 | 41 | export default Index; -------------------------------------------------------------------------------- /Chapter05/5-3-rnp/server.js: -------------------------------------------------------------------------------- 1 | require('../5-2-rbac/server'); -------------------------------------------------------------------------------- /Chapter05/5-4-i18n/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter05/5-4-i18n/components/Common.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {translate} from 'react-i18next' 3 | 4 | const Common = ({t}) => ( 5 |
Component common: {t('HELLO')}}
6 | ); 7 | 8 | export default translate()(Common); -------------------------------------------------------------------------------- /Chapter05/5-4-i18n/components/Other.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {translate} from 'react-i18next'; 3 | 4 | const Other = ({t}) => ( 5 |
6 | Component other: {t('other:GOOD_MORNING')} 7 | {(t('other:GOOD_MORNING') === 'GOOD_MORNING') ? ( 8 |  it's not translated because "Index" page was opened directly and it does not have "other" namespace explicitly requested 9 | ) : ( 10 |  it is translated because "Other" page has requested "other" namespace and now it is cached (even on Index page) 11 | )} 12 |
13 | ); 14 | 15 | export default translate(['other'])(Other); -------------------------------------------------------------------------------- /Chapter05/5-4-i18n/lib/withI18n.js: -------------------------------------------------------------------------------- 1 | import 'isomorphic-unfetch'; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | import i18n from 'i18next'; 5 | import {I18nextProvider, translate} from 'react-i18next' 6 | import moment from 'moment'; 7 | import formatMessage from 'format-message'; 8 | 9 | const fallbackLng = 'en'; 10 | const defaultNS = 'common'; 11 | 12 | const baseUrl = 'http://localhost:3000/static/locales'; 13 | const getLangUrl = (lang, ns) => `${baseUrl}/${lang}/${ns}.json`; 14 | let translation = null; 15 | 16 | export const getTranslation = async (lang, namespaces) => { 17 | 18 | translation = translation || {}; //TODO Invalidate in dev mode 19 | 20 | for (let ns of namespaces) { 21 | 22 | if (!translation[lang] || !translation[lang][ns]) { 23 | 24 | let response = await fetch(getLangUrl(lang, ns)); 25 | 26 | if (!response.ok) { 27 | response = await fetch(getLangUrl(fallbackLng, ns)); 28 | } 29 | 30 | translation[lang] = translation[lang] || {}; 31 | translation[lang][ns] = await response.json(); 32 | } 33 | 34 | } 35 | 36 | return translation; 37 | 38 | }; 39 | 40 | const getLang = (cookie) => cookie.match(/lang=([a-z]+)/)[1]; 41 | 42 | export default (namespaces = []) => (WrappedComponent) => ( 43 | 44 | class WithI18n extends React.Component { 45 | 46 | static displayName = `withI18n(${WrappedComponent.displayName 47 | || WrappedComponent.name 48 | || 'Component'})`; 49 | 50 | static async getInitialProps(args) { 51 | 52 | const req = args.req; 53 | 54 | const lng = (req && req.headers.cookie && getLang(req.headers.cookie)) 55 | || (document && getLang(document.cookie)) 56 | || fallbackLng; 57 | 58 | const resources = await getTranslation( 59 | lng, 60 | [defaultNS, ...namespaces] // list other namespaces here when needed 61 | ); 62 | 63 | const props = WrappedComponent.getInitialProps 64 | ? await WrappedComponent.getInitialProps(args) 65 | : {}; 66 | 67 | return { 68 | ...props, 69 | resources, 70 | lng 71 | }; 72 | 73 | } 74 | 75 | constructor(props, context) { 76 | 77 | super(props, context); 78 | 79 | const {lng, resources} = props; 80 | 81 | translation = translation || resources; // recover client side cache of translations 82 | 83 | this.i18n = i18n.init({ 84 | fallbackLng, 85 | lng, 86 | resources, 87 | defaultNS, 88 | ns: [defaultNS], 89 | debug: false 90 | }); 91 | 92 | // this allows to use translation in pages 93 | this.Wrapper = translate(namespaces)(WrappedComponent); 94 | 95 | this.moment = moment().locale(lng); 96 | 97 | } 98 | 99 | render() { 100 | 101 | const {Wrapper, moment, props: {resources, ...props}} = this; 102 | 103 | return ( 104 | 105 | 106 | 107 | ); 108 | 109 | } 110 | 111 | } 112 | 113 | ); -------------------------------------------------------------------------------- /Chapter05/5-4-i18n/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5-8-i18n", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "next" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "cookie-parser": "1.4.3", 13 | "express": "4.16.3", 14 | "isomorphic-unfetch": "2.0.0", 15 | "next": "5.0.0" 16 | }, 17 | "dependencies": { 18 | "format-message": "5.2.6", 19 | "i18next": "11.1.1", 20 | "moment": "2.22.0", 21 | "prop-types": "15.6.1", 22 | "react": "16.2.0", 23 | "react-dom": "16.2.0", 24 | "react-i18next": "7.5.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Chapter05/5-4-i18n/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import Common from '../components/Common'; 4 | import Other from '../components/Other'; 5 | import withI18n from '../lib/withI18n'; 6 | 7 | const setLocale = (lang) => { 8 | document.cookie = 'lang=' + lang + '; path=/'; 9 | window.location.reload(); 10 | }; 11 | 12 | const getStyle = (current, lang) => ({fontWeight: current === lang ? 'bold' : 'normal'}); 13 | 14 | const Index = ({t, lng, moment, msg}) => ( 15 |
16 |
Page-level common: {t('common:HELLO')}
17 | 18 | 19 |
{moment.format('LLLL')}
20 |
{msg(t('common:MESSAGES'), {count: 0})}
21 |
{msg(t('common:MESSAGES'), {count: 1})}
22 |
{msg(t('common:MESSAGES'), {count: 2})}
23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | ); 32 | 33 | export default withI18n()(Index); -------------------------------------------------------------------------------- /Chapter05/5-4-i18n/pages/other.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import Common from '../components/Common'; 4 | import Other from '../components/Other'; 5 | import withI18n from '../lib/withI18n'; 6 | 7 | const OtherPage = ({t}) => ( 8 |
9 |
Page-level other: {t('other:GOOD_MORNING')}
10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | ); 18 | 19 | export default withI18n(['other'])(OtherPage); -------------------------------------------------------------------------------- /Chapter05/5-4-i18n/static/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "HELLO": "Hello!", 3 | "BACK": "Back", 4 | "OTHER_PAGE": "Other page", 5 | "MESSAGES": "{count, plural, =0 {No unread messages} one {# unread message} other {# unread messages}}" 6 | } -------------------------------------------------------------------------------- /Chapter05/5-4-i18n/static/locales/en/other.json: -------------------------------------------------------------------------------- 1 | { 2 | "GOOD_MORNING": "Good Morning!" 3 | } -------------------------------------------------------------------------------- /Chapter05/5-4-i18n/static/locales/es/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "HELLO": "Hola!", 3 | "BACK": "Atrás", 4 | "OTHER_PAGE": "Otra página", 5 | "MESSAGES": "{count, plural, =0 {Sin mensajes no leídos} one {# mensaje no leído} other {# mensajes no leídos}}" 6 | } -------------------------------------------------------------------------------- /Chapter05/5-4-i18n/static/locales/es/other.json: -------------------------------------------------------------------------------- 1 | { 2 | "GOOD_MORNING": "Buenos días!" 3 | } -------------------------------------------------------------------------------- /Chapter05/5-5-error/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter05/5-5-error/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5-2-error", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "next" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "next": "5.1.0" 13 | }, 14 | "dependencies": { 15 | "react": "16.2.0", 16 | "react-dom": "16.2.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Chapter05/5-5-error/pages/_error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Error from 'next/error'; 3 | 4 | export default class ErrorPage extends React.Component { 5 | 6 | componentWillMount() { 7 | // here we can log an error for further analysis 8 | console.log('Unhandled error', this.props.url.pathname); 9 | } 10 | 11 | render() { 12 | return ( 13 | 14 | ); 15 | } 16 | } -------------------------------------------------------------------------------- /Chapter05/5-5-error/pages/boundary.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class FaultyComponent extends React.Component { 4 | componentWillMount() { 5 | // only synchronous errors will be captured 6 | throw new Error('FaultyComponent threw an error'); 7 | } 8 | 9 | render() { 10 | return null; 11 | } 12 | } 13 | 14 | export default class Page extends React.Component { 15 | 16 | state = {error: null, mount: false}; 17 | 18 | componentDidCatch(e, info) { 19 | console.error(e, info); 20 | this.setState({error: e.message}); 21 | } 22 | 23 | mount() { 24 | this.setState({mount: true}); 25 | } 26 | 27 | render() { 28 | 29 | const {error, mount} = this.state; 30 | 31 | if (error) return ( 32 |
Boundary captured an error: {error}
33 | ); 34 | 35 | return ( 36 |
37 | 38 | {mount ? : null} 39 |
40 | ); 41 | } 42 | } -------------------------------------------------------------------------------- /Chapter05/5-5-error/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const faultyPromise = (client) => new Promise((resolve, reject) => { 4 | setTimeout(() => { 5 | reject(new Error('Faulty ' + (client ? 'client' : 'server'))); 6 | }, 500); 7 | }); 8 | 9 | export default class Page extends React.Component { 10 | 11 | state = { 12 | loading: false, 13 | error: null, 14 | result: null 15 | }; 16 | 17 | static async loadPosts(client) { 18 | return await faultyPromise(client); 19 | } 20 | 21 | static async getInitialProps() { 22 | try { 23 | return {result: await Page.loadPosts(false)}; 24 | } catch (e) { 25 | return {error: e.message}; 26 | } 27 | } 28 | 29 | retry = async () => { 30 | this.setState({result: null, error: null, loading: true}); 31 | try { 32 | this.setState({loading: false, result: await Page.loadPosts(true)}); 33 | } catch (e) { 34 | this.setState({loading: false, error: e.message}); 35 | } 36 | }; 37 | 38 | getError() { 39 | return (this.state.error || this.props.error); 40 | } 41 | 42 | getResult() { 43 | return (this.state.result || this.props.result); 44 | } 45 | 46 | isLoading() { 47 | return this.state.loading; 48 | } 49 | 50 | render() { 51 | if (this.state.loading) return (
Loading...
); 52 | 53 | const error = this.getError(); 54 | if (error) return ( 55 |
56 | Cannot load posts: "{error}" 57 |
58 | 59 |
60 | ); 61 | 62 | return ( 63 |
{JSON.stringify(this.getResult())}
64 | ); 65 | } 66 | } -------------------------------------------------------------------------------- /Chapter05/5-5-error/pages/unhandledError.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class UnhandledErrorPage extends React.Component { 4 | 5 | static async getInitialProps() { 6 | throw new Error('Unhandled error from getInitialProps'); 7 | } 8 | 9 | render() { 10 | return ( 11 |
Whatever
12 | ); 13 | } 14 | } -------------------------------------------------------------------------------- /Chapter05/5-6-caching/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter05/5-6-caching/lib/redux.js: -------------------------------------------------------------------------------- 1 | import logger from 'redux-logger'; 2 | import {applyMiddleware, createStore} from 'redux'; 3 | 4 | const SET_CLIENT_STATE = 'SET_CLIENT_STATE'; 5 | 6 | export const reducer = (state, {type, payload}) => { 7 | if (type == SET_CLIENT_STATE) { 8 | return { 9 | ...state, 10 | fromClient: payload 11 | }; 12 | } 13 | return state; 14 | }; 15 | 16 | const makeConfiguredStore = (reducer, initialState) => 17 | createStore(reducer, initialState, applyMiddleware(logger)); 18 | 19 | export const makeStore = (initialState, {isServer, req, debug, storeKey}) => { 20 | 21 | if (isServer) { 22 | 23 | // only do it now bc on client it should be undefined by default 24 | // server will put things in req 25 | initialState = initialState || {fromServer: 'foo'}; //req.initialState; 26 | 27 | return makeConfiguredStore(reducer, initialState); 28 | 29 | } else { 30 | 31 | const {persistStore, persistReducer} = require('redux-persist'); 32 | const storage = require('redux-persist/lib/storage').default; 33 | 34 | const persistConfig = { 35 | key: 'nextjs', 36 | whitelist: ['fromClient'], // make sure it does not clash with server keys 37 | storage 38 | }; 39 | 40 | const persistedReducer = persistReducer(persistConfig, reducer); 41 | const store = makeConfiguredStore(persistedReducer, initialState); 42 | 43 | store.__persistor = persistStore(store); // Nasty hack 44 | 45 | return store; 46 | } 47 | }; 48 | 49 | export const setClientState = (clientState) => ({ 50 | type: SET_CLIENT_STATE, 51 | payload: clientState 52 | }); -------------------------------------------------------------------------------- /Chapter05/5-6-caching/lib/withPersistGate.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {PersistGate} from 'redux-persist/integration/react'; 4 | 5 | export default (gateProps = {}) => (WrappedComponent) => ( 6 | 7 | class WithPersistGate extends React.Component { 8 | 9 | static displayName = `withPersistGate(${WrappedComponent.displayName 10 | || WrappedComponent.name 11 | || 'Component'})`; 12 | static contextTypes = { 13 | store: PropTypes.object.isRequired 14 | }; 15 | 16 | constructor(props, context) { 17 | super(props, context); 18 | this.store = context.store; 19 | } 20 | 21 | render() { 22 | return ( 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | } 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /Chapter05/5-6-caching/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5-6-caching", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "next" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "next": "5.0.0" 13 | }, 14 | "dependencies": { 15 | "next-redux-wrapper": "1.3.5", 16 | "prop-types": "15.6.1", 17 | "react": "16.2.0", 18 | "react-dom": "16.2.0", 19 | "react-redux": "5.0.7", 20 | "redux": "3.7.2", 21 | "redux-logger": "3.0.6", 22 | "redux-persist": "5.9.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Chapter05/5-6-caching/pages/gated.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import withRedux from "next-redux-wrapper"; 4 | import {makeStore, setClientState} from "../lib/redux"; 5 | import withPersistGate from "../lib/withPersistGate"; 6 | 7 | const Index = ({fromServer, fromClient, setClientState}) => ( 8 |
9 |
10 | Not gated | Gated 11 |
12 |
fromServer: {fromServer}
13 |
fromClient: {fromClient}
14 |
15 | 16 |
17 |
18 | ); 19 | 20 | export default withRedux( 21 | makeStore, 22 | (state) => state, 23 | {setClientState} 24 | )(withPersistGate({ 25 | loading: (
Loading
) 26 | })(Index)); -------------------------------------------------------------------------------- /Chapter05/5-6-caching/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import withRedux from "next-redux-wrapper"; 4 | import {makeStore, setClientState} from "../lib/redux"; 5 | 6 | const Index = ({fromServer, fromClient, setClientState}) => ( 7 |
8 |
9 | Not gated | Gated 10 |
11 |
fromServer: {fromServer}
12 |
fromClient: {fromClient}
13 |
14 | 15 |
16 |
17 | ); 18 | 19 | export default withRedux( 20 | makeStore, 21 | (state) => state, 22 | {setClientState} 23 | )(Index); -------------------------------------------------------------------------------- /Chapter05/5-7-analytics/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter05/5-7-analytics/lib/withGA.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Router from 'next/router'; 3 | import ReactGA from 'react-ga'; 4 | 5 | const GA_TRACKING_ID = '5594409'; 6 | const WINDOWPROP = '__NEXT_GA_INITIALIZED__'; 7 | const debug = process.env.NODE_ENV !== 'production'; 8 | 9 | export default (WrappedComponent) => (class WithGA extends React.Component { 10 | 11 | lastPath = null; 12 | 13 | componentDidMount() { 14 | this.initGa(); 15 | this.trackPageview(); 16 | Router.router.events.on('routeChangeComplete', this.trackPageview); 17 | } 18 | 19 | componentWillUnmount() { 20 | Router.router.events.off('routeChangeComplete', this.trackPageview); 21 | } 22 | 23 | trackPageview = (path = document.location.pathname) => { 24 | if (path === this.lastPath) return; 25 | ReactGA.pageview(path); 26 | this.lastPath = path; 27 | }; 28 | 29 | initGa = () => { 30 | if (WINDOWPROP in window) return; 31 | ReactGA.initialize(GA_TRACKING_ID, {debug}); 32 | window[WINDOWPROP] = true; 33 | }; 34 | 35 | render() { 36 | return ( 37 | 38 | ); 39 | } 40 | 41 | }); -------------------------------------------------------------------------------- /Chapter05/5-7-analytics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5-7-analytics", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "next" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "next": "5.1.0" 13 | }, 14 | "dependencies": { 15 | "react": "16.2.0", 16 | "react-dom": "16.2.0", 17 | "react-ga": "2.4.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Chapter05/5-7-analytics/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import withGA from "../lib/withGA"; 4 | 5 | const Index = () => ( 6 |
7 | Analyze this! 8 |
9 | Analyze this! | Analyze that! 10 |
11 |
12 | ); 13 | 14 | export default withGA(Index); -------------------------------------------------------------------------------- /Chapter05/5-7-analytics/pages/that.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import withGA from "../lib/withGA"; 4 | 5 | const Index = () => ( 6 |
7 | Analyze this! 8 |
9 | Analyze this! | Analyze that! 10 |
11 |
12 | ); 13 | 14 | export default withGA(Index); -------------------------------------------------------------------------------- /Chapter06/6-2-unit-tests/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "env", 7 | { 8 | "modules": "commonjs" 9 | } 10 | ], 11 | "next/babel" 12 | ] 13 | }, 14 | "development": { 15 | "presets": [ 16 | "next/babel" 17 | ] 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter06/6-2-unit-tests/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter06/6-2-unit-tests/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: [ 3 | './jest.setup.js' 4 | ], 5 | testPathIgnorePatterns: [ 6 | './.idea', 7 | './.next', 8 | './node_modules' 9 | ] 10 | }; -------------------------------------------------------------------------------- /Chapter06/6-2-unit-tests/jest.setup.js: -------------------------------------------------------------------------------- 1 | import {configure} from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({adapter: new Adapter()}); -------------------------------------------------------------------------------- /Chapter06/6-2-unit-tests/lib/index.js: -------------------------------------------------------------------------------- 1 | import 'isomorphic-unfetch'; 2 | 3 | export const sum = (a, b) => (a + b); 4 | 5 | export const getOctocat = async () => 6 | (await fetch('https://api.github.com/users/octocat')).json(); -------------------------------------------------------------------------------- /Chapter06/6-2-unit-tests/lib/index.test.js: -------------------------------------------------------------------------------- 1 | import {getOctocat, sum} from "./index"; 2 | 3 | describe('sum', () => { 4 | 5 | it('sums two values', () => { 6 | expect(sum(2, 2)).toEqual(4); 7 | }); 8 | 9 | }); 10 | 11 | describe('getOctocat', () => { 12 | 13 | it('fetches octocat userinfo from GitHub', async () => { 14 | const userinfo = await getOctocat(); 15 | expect(userinfo.login).toEqual('octocat'); 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /Chapter06/6-2-unit-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "6-2-unit-tests", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "next", 8 | "test": "NODE_ENV=test jest", 9 | "coveralls": "cat ./coverage/lcov.info | coveralls" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "enzyme": "3.3.0", 15 | "enzyme-adapter-react-16": "1.1.1", 16 | "jest": "22.4.3", 17 | "next": "5.1.0", 18 | "react-test-renderer": "16.3.1" 19 | }, 20 | "dependencies": { 21 | "coveralls": "3.0.0", 22 | "isomorphic-unfetch": "2.0.0", 23 | "react": "16.2.0", 24 | "react-dom": "16.2.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Chapter06/6-2-unit-tests/pages/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot Testing Renders "Hello octocat!" for emulated NextJS lifecycle 1`] = ` 4 |
5 | Hello, 6 | ! 7 |
8 | `; 9 | 10 | exports[`Snapshot Testing Renders "Hello octocat!" for given props 1`] = ` 11 |
12 | Hello, 13 | octocat 14 | ! 15 |
16 | `; 17 | -------------------------------------------------------------------------------- /Chapter06/6-2-unit-tests/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {getOctocat} from "../lib"; 3 | 4 | export default class Index extends React.Component { 5 | 6 | static async getInitialProps({err, req, res, pathname, query, asPath}) { 7 | const userinfo = await getOctocat(); 8 | return { 9 | userinfo: userinfo 10 | }; 11 | } 12 | 13 | render() { 14 | return ( 15 |
Hello, {this.props.userinfo.login}!
16 | ); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /Chapter06/6-2-unit-tests/pages/index.test.js: -------------------------------------------------------------------------------- 1 | import {shallow} from 'enzyme'; 2 | import React from 'react'; 3 | import renderer from 'react-test-renderer'; 4 | import Index from './index.js' 5 | 6 | describe('Enzyme', () => { 7 | 8 | it('Renders "Hello octocat!" for given props', () => { 9 | const app = shallow(); 10 | expect(app.find('div').text()).toEqual('Hello, octocat!'); 11 | }); 12 | 13 | }); 14 | 15 | describe('Snapshot Testing', () => { 16 | 17 | it('Renders "Hello octocat!" for given props', () => { 18 | const component = renderer.create(); 19 | const tree = component.toJSON(); 20 | expect(tree).toMatchSnapshot(); 21 | }); 22 | 23 | it('Renders "Hello octocat!" for emulated NextJS lifecycle', async () => { 24 | const userinfo = Index.getInitialProps({}); 25 | const component = renderer.create(); 26 | const tree = component.toJSON(); 27 | expect(tree).toMatchSnapshot(); 28 | }); 29 | 30 | }); -------------------------------------------------------------------------------- /Chapter06/6-3-e2e-tests/.gitignore: -------------------------------------------------------------------------------- 1 | coverage -------------------------------------------------------------------------------- /Chapter06/6-3-e2e-tests/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter06/6-3-e2e-tests/jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | server: { 3 | command: 'npm start', 4 | port: 3000 5 | } 6 | }; -------------------------------------------------------------------------------- /Chapter06/6-3-e2e-tests/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'jest-puppeteer', 3 | testPathIgnorePatterns: [ 4 | './.idea', 5 | './.next', 6 | './node_modules' 7 | ] 8 | }; -------------------------------------------------------------------------------- /Chapter06/6-3-e2e-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "6-3-e2e-tests", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "next", 8 | "test": "NODE_ENV=test jest" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "jest": "22.4.3", 14 | "jest-puppeteer": "2.3.0", 15 | "next": "5.1.0", 16 | "puppeteer": "1.3.0" 17 | }, 18 | "dependencies": { 19 | "isomorphic-unfetch": "2.0.0", 20 | "react": "16.2.0", 21 | "react-dom": "16.2.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Chapter06/6-3-e2e-tests/pages/index.js: -------------------------------------------------------------------------------- 1 | import 'isomorphic-unfetch'; 2 | import React from "react"; 3 | 4 | export default class Index extends React.Component { 5 | 6 | static async getInitialProps({err, req, res, pathname, query, asPath}) { 7 | const userinfo = await (await fetch('https://api.github.com/users/octocat')).json(); 8 | return { 9 | userinfo: userinfo 10 | }; 11 | } 12 | 13 | state = { 14 | clicked: false 15 | }; 16 | 17 | handleClick = (e) => { 18 | this.setState({clicked: true}); 19 | }; 20 | 21 | render() { 22 | return ( 23 |
24 |
Hello, {this.props.userinfo.login}!
25 |
26 | 27 |
28 | {this.state.clicked && (
Clicked
)} 29 |
30 | ); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /Chapter06/6-3-e2e-tests/pages/index.test.js: -------------------------------------------------------------------------------- 1 | const config = require('../jest-puppeteer.config'); 2 | 3 | const openPage = (url = '/') => page.goto(`http://localhost:${config.server.port}${url}`); 4 | 5 | describe('Basic integration', () => { 6 | 7 | it('shows the page', async () => { 8 | await openPage(); 9 | await page.content(); 10 | await expect(page).toMatch('Hello, octocat!Click'); 11 | }); 12 | 13 | it('clicks the button', async () => { 14 | await openPage(); 15 | await page.content(); 16 | await expect(page).toClick('button', {text: 'Click'}); 17 | await expect(page).toMatch('Hello, octocat!ClickClicked'); 18 | }); 19 | 20 | }); -------------------------------------------------------------------------------- /Chapter06/6-4-ci/.gitignore: -------------------------------------------------------------------------------- 1 | coverage -------------------------------------------------------------------------------- /Chapter06/6-4-ci/.gitlab-ci.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Next.js-Quick-Start-Guide/fb5a0549e8b39752f89db19600b610311ecd535d/Chapter06/6-4-ci/.gitlab-ci.yml -------------------------------------------------------------------------------- /Chapter06/6-4-ci/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter06/6-4-ci/.travis.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Next.js-Quick-Start-Guide/fb5a0549e8b39752f89db19600b610311ecd535d/Chapter06/6-4-ci/.travis.yml -------------------------------------------------------------------------------- /Chapter06/6-4-ci/jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | server: { 3 | command: 'npm start', 4 | port: 3000 5 | } 6 | }; -------------------------------------------------------------------------------- /Chapter06/6-4-ci/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'jest-puppeteer', 3 | testPathIgnorePatterns: [ 4 | './.idea', 5 | './.next', 6 | './node_modules' 7 | ] 8 | }; -------------------------------------------------------------------------------- /Chapter06/6-4-ci/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "6-3-e2e-tests", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "next", 8 | "test": "NODE_ENV=test jest" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "jest": "22.4.3", 14 | "jest-puppeteer": "2.3.0", 15 | "next": "5.1.0", 16 | "puppeteer": "1.3.0" 17 | }, 18 | "dependencies": { 19 | "isomorphic-unfetch": "2.0.0", 20 | "react": "16.2.0", 21 | "react-dom": "16.2.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Chapter06/6-4-ci/pages/index.js: -------------------------------------------------------------------------------- 1 | import 'isomorphic-unfetch'; 2 | import React from "react"; 3 | 4 | export default class Index extends React.Component { 5 | 6 | static async getInitialProps({err, req, res, pathname, query, asPath}) { 7 | const userinfo = await (await fetch('https://api.github.com/users/octocat')).json(); 8 | return { 9 | userinfo: userinfo 10 | }; 11 | } 12 | 13 | state = { 14 | clicked: false 15 | }; 16 | 17 | handleClick = (e) => { 18 | this.setState({clicked: true}); 19 | }; 20 | 21 | render() { 22 | return ( 23 |
24 |
Hello, {this.props.userinfo.login}!
25 |
26 | 27 |
28 | {this.state.clicked && (
Clicked
)} 29 |
30 | ); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /Chapter06/6-4-ci/pages/index.test.js: -------------------------------------------------------------------------------- 1 | const config = require('../jest-puppeteer.config'); 2 | 3 | const openPage = (url = '/') => page.goto(`http://localhost:${config.server.port}${url}`); 4 | 5 | describe('Basic integration', () => { 6 | 7 | it('shows the page', async () => { 8 | await openPage(); 9 | await page.content(); 10 | await expect(page).toMatch('Hello, octocat!Click'); 11 | }); 12 | 13 | it('clicks the button', async () => { 14 | await openPage(); 15 | await page.content(); 16 | await expect(page).toClick('button', {text: 'Click'}); 17 | await expect(page).toMatch('Hello, octocat!ClickClicked'); 18 | }); 19 | 20 | }); -------------------------------------------------------------------------------- /Chapter06/6-5-coveralls/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "env", 7 | { 8 | "modules": "commonjs" 9 | } 10 | ], 11 | "next/babel" 12 | ] 13 | }, 14 | "development": { 15 | "presets": [ 16 | "next/babel" 17 | ] 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter06/6-5-coveralls/.gitignore: -------------------------------------------------------------------------------- 1 | coverage -------------------------------------------------------------------------------- /Chapter06/6-5-coveralls/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter06/6-5-coveralls/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testPathIgnorePatterns: [ 3 | './.idea', 4 | './.next', 5 | './node_modules' 6 | ], 7 | collectCoverage: true, 8 | coverageDirectory: './coverage', 9 | coveragePathIgnorePatterns: [ 10 | "./node_modules" 11 | // also exclude your setup files here if you have any 12 | ] 13 | }; -------------------------------------------------------------------------------- /Chapter06/6-5-coveralls/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "6-5-hooks", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "next", 8 | "test": "NODE_ENV=test jest", 9 | "precommit": "npm test" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "husky": "0.14.3", 15 | "jest": "22.4.3", 16 | "next": "5.1.0", 17 | "react-test-renderer": "16.3.1" 18 | }, 19 | "dependencies": { 20 | "react": "16.2.0", 21 | "react-dom": "16.2.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Chapter06/6-5-coveralls/pages/index.js: -------------------------------------------------------------------------------- 1 | export default () => ( 2 |
Hello, World!
3 | ); -------------------------------------------------------------------------------- /Chapter06/6-5-coveralls/pages/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Index from './index.js' 4 | 5 | describe('Snapshot Testing', () => { 6 | 7 | it('Renders "Hello, World!"', () => { 8 | expect(renderer.create().toJSON()).toMatchSnapshot(); 9 | }); 10 | 11 | }); -------------------------------------------------------------------------------- /Chapter06/6-6-hooks/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "env", 7 | { 8 | "modules": "commonjs" 9 | } 10 | ], 11 | "next/babel" 12 | ] 13 | }, 14 | "development": { 15 | "presets": [ 16 | "next/babel" 17 | ] 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Chapter06/6-6-hooks/.gitignore: -------------------------------------------------------------------------------- 1 | coverage -------------------------------------------------------------------------------- /Chapter06/6-6-hooks/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter06/6-6-hooks/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testPathIgnorePatterns: [ 3 | './.idea', 4 | './.next', 5 | './node_modules' 6 | ], 7 | collectCoverage: true, 8 | coverageDirectory: './coverage', 9 | coveragePathIgnorePatterns: [ 10 | "./node_modules" 11 | // also exclude your setup files here if you have any 12 | ] 13 | }; -------------------------------------------------------------------------------- /Chapter06/6-6-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "6-2-unit-tests", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "next", 8 | "test": "NODE_ENV=test jest", 9 | "coveralls": "cat ./coverage/lcov.info | coveralls" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "coveralls": "3.0.0", 15 | "jest": "22.4.3", 16 | "next": "5.1.0", 17 | "react-test-renderer": "16.3.1" 18 | }, 19 | "dependencies": { 20 | "react": "16.2.0", 21 | "react-dom": "16.2.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Chapter06/6-6-hooks/pages/index.js: -------------------------------------------------------------------------------- 1 | export default () => (
Hello, World!
); -------------------------------------------------------------------------------- /Chapter06/6-6-hooks/pages/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Index from './index.js' 4 | 5 | describe('Snapshot Testing', () => { 6 | 7 | it('Renders "Hello, World!"', () => { 8 | const component = renderer.create(); 9 | const tree = component.toJSON(); 10 | expect(tree).toMatchSnapshot(); 11 | }); 12 | 13 | }); -------------------------------------------------------------------------------- /Chapter07/7-2-docker/ssr/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter07/7-2-docker/ssr/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | WORKDIR /app 3 | ADD package.json .npmrc ./ 4 | ENV NPM_CONFIG_LOGLEVEL warn 5 | RUN npm install 6 | ADD pages ./pages 7 | RUN NODE_ENV=production npm run build 8 | CMD NODE_ENV=production npm run server -------------------------------------------------------------------------------- /Chapter07/7-2-docker/ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "7-2-docker-ssr", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "next", 8 | "build": "next build", 9 | "server": "next start" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "next": "5.1.0" 15 | }, 16 | "dependencies": { 17 | "isomorphic-unfetch": "2.0.0", 18 | "react": "16.2.0", 19 | "react-dom": "16.2.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Chapter07/7-2-docker/ssr/pages/index.js: -------------------------------------------------------------------------------- 1 | import 'isomorphic-unfetch'; 2 | import React from "react"; 3 | 4 | export default class Index extends React.Component { 5 | 6 | static async getInitialProps({err, req, res, pathname, query, asPath}) { 7 | const userinfo = await (await fetch('https://api.github.com/users/octocat')).json(); 8 | return { 9 | userinfo: userinfo 10 | }; 11 | } 12 | 13 | render() { 14 | return ( 15 |
Hello, {this.props.userinfo.login}!
16 | ); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /Chapter07/7-2-docker/static/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter07/7-2-docker/static/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest as build 2 | WORKDIR /app 3 | ADD package.json .npmrc ./ 4 | ENV NPM_CONFIG_LOGLEVEL warn 5 | RUN npm install 6 | ADD pages ./pages 7 | ADD next.config.js ./ 8 | RUN NODE_ENV=production npm run build 9 | RUN NODE_ENV=production npm run static 10 | 11 | FROM nginx:latest AS production 12 | RUN mkdir -p /usr/share/nginx/html 13 | WORKDIR /usr/share/nginx/html 14 | COPY --from=build /app/out . 15 | -------------------------------------------------------------------------------- /Chapter07/7-2-docker/static/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | exportPathMap: () => ({ 3 | '/': {page: '/'} 4 | }) 5 | }; -------------------------------------------------------------------------------- /Chapter07/7-2-docker/static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "7-2-docker-static", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "next", 8 | "build": "next build", 9 | "static": "next export" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "next": "5.1.0" 15 | }, 16 | "dependencies": { 17 | "isomorphic-unfetch": "2.0.0", 18 | "react": "16.2.0", 19 | "react-dom": "16.2.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Chapter07/7-2-docker/static/pages/index.js: -------------------------------------------------------------------------------- 1 | import 'isomorphic-unfetch'; 2 | import React from "react"; 3 | 4 | export default class Index extends React.Component { 5 | 6 | static async getInitialProps({err, req, res, pathname, query, asPath}) { 7 | const userinfo = await (await fetch('https://api.github.com/users/octocat')).json(); 8 | return { 9 | userinfo: userinfo 10 | }; 11 | } 12 | 13 | render() { 14 | return ( 15 |
Hello, {this.props.userinfo.login}!
16 | ); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /Chapter07/7-3-heroku/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter07/7-3-heroku/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable -------------------------------------------------------------------------------- /Chapter07/7-3-heroku/Procfile: -------------------------------------------------------------------------------- 1 | web: npm run server -- --port $PORT -------------------------------------------------------------------------------- /Chapter07/7-3-heroku/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testPathIgnorePatterns: [ 3 | './.idea', 4 | './.next', 5 | './node_modules' 6 | ] 7 | }; -------------------------------------------------------------------------------- /Chapter07/7-3-heroku/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "7-3-heroku", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "heroku-postbuild": "next build", 8 | "server": "next start", 9 | "start": "next", 10 | "test": "NODE_ENV=test jest" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "jest": "22.4.3", 16 | "next": "5.1.0" 17 | }, 18 | "dependencies": { 19 | "react": "16.2.0", 20 | "react-dom": "16.2.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Chapter07/7-3-heroku/pages/index.js: -------------------------------------------------------------------------------- 1 | import 'isomorphic-unfetch'; 2 | import React from "react"; 3 | 4 | export default class Index extends React.Component { 5 | 6 | static async getInitialProps({err, req, res, pathname, query, asPath}) { 7 | const userinfo = await (await fetch('https://api.github.com/users/octocat')).json(); 8 | return { 9 | userinfo: userinfo 10 | }; 11 | } 12 | 13 | state = { 14 | clicked: false 15 | }; 16 | 17 | handleClick = (e) => { 18 | this.setState({clicked: true}); 19 | }; 20 | 21 | render() { 22 | return ( 23 |
24 |
Hello, {this.props.userinfo.login}!
25 |
26 | 27 |
28 | {this.state.clicked && (
Clicked
)} 29 |
30 | ); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /Chapter07/7-3-heroku/pages/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Index from './index.js' 4 | 5 | describe('Snapshot Testing', () => { 6 | 7 | it('Renders "Hello, World!"', () => { 8 | expect(renderer.create().toJSON()).toMatchSnapshot(); 9 | }); 10 | 11 | }); -------------------------------------------------------------------------------- /Chapter07/7-4-now/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=true -------------------------------------------------------------------------------- /Chapter07/7-4-now/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | after_success: 5 | - npm run now-deploy -------------------------------------------------------------------------------- /Chapter07/7-4-now/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testPathIgnorePatterns: [ 3 | './.idea', 4 | './.next', 5 | './node_modules' 6 | ] 7 | }; -------------------------------------------------------------------------------- /Chapter07/7-4-now/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "7-4-now", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "next build", 8 | "now-deploy": "now -e NODE_ENV=production --token $NOW_TOKEN --npm --public", 9 | "now-start": "next start", 10 | "start": "next", 11 | "test": "NODE_ENV=test jest" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "jest": "22.4.3", 17 | "next": "5.1.0", 18 | "now": "^11.1.5" 19 | }, 20 | "dependencies": { 21 | "react": "16.2.0", 22 | "react-dom": "16.2.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Chapter07/7-4-now/pages/index.js: -------------------------------------------------------------------------------- 1 | export default () => ( 2 |
Hello, World!
3 | ); -------------------------------------------------------------------------------- /Chapter07/7-4-now/pages/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Index from './index.js' 4 | 5 | describe('Snapshot Testing', () => { 6 | 7 | it('Renders "Hello, World!"', () => { 8 | expect(renderer.create().toJSON()).toMatchSnapshot(); 9 | }); 10 | 11 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Next.js Quick Start Guide 5 | 6 | Next.js Quick Start Guide 7 | 8 | This is the code repository for [Next.js Quick Start Guide](https://www.packtpub.com/web-development/nextjs-quick-start-guide?utm_source=github&utm_medium=repository&utm_campaign=9781788993661), published by Packt. 9 | 10 | **Server-side rendering done right** 11 | 12 | ## What is this book about? 13 | Next.js is a powerful addition to the ever-growing and dynamic JavaScript world. Built on top of React, Webpack, and Babel, it is a minimalistic framework for server-rendered universal JavaScript applications. This book will show you the best practices for building sites using Next. js, enabling you to build SEO-friendly and superfast websites. 14 | 15 | This book covers the following exciting features: 16 | * Explore the benefts of server-side rendering with Next.js 17 | * Create and link JavaScript modules together by understanding code splitting and bundling 18 | * Create website pages and wire them together through website navigation 19 | * Extend your application with additional Webpack loaders and features, as well as custom Babel plugins and presets 20 | * Use GraphQL and Apollo frameworks with Next.js to fetch data and receive push notifcations 21 | 22 | If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1788993667) today! 23 | 24 | https://www.packtpub.com/ 26 | 27 | 28 | ## Instructions and Navigations 29 | All of the code is organized into folders. For example, Chapter02. 30 | 31 | The code will look like the following: 32 | ``` 33 | import 'isomorphic-fetch'; 34 | 35 | (async () => { 36 | const res = await fetch(...); // already polyfilled 37 | })(); 38 | ``` 39 | 40 | **Following is what you need for this book:** 41 | This book is for JavaScript developers who want to learn how to generate server-rendered applications. 42 | 43 | With the following software and hardware list you can run all code files present in the book (Chapter 1-7). 44 | 45 | ### Software and Hardware List 46 | 47 | | Chapter | Software required | OS required | 48 | | -------- | ------------------------------------| -----------------------------------| 49 | | 1 | Node JS | Windows, Mac OS X, and Linux (Any) | 50 | | 2 | Node JS | Windows, Mac OS X, and Linux (Any) | 51 | | 4 | Node JS | Windows, Mac OS X, and Linux (Any) | 52 | | 5 | Node JS | Windows, Mac OS X, and Linux (Any) | 53 | | 6 | Node JS | Windows, Mac OS X, and Linux (Any) | 54 | | 7 | Node JS | Windows, Mac OS X, and Linux (Any) | 55 | 56 | 57 | We also provide a PDF file that has color images of the screenshots/diagrams used in this book. [Click here to download it](http://www.packtpub.com/sites/default/files/downloads/NextDotjsQuickStartGuide_ColorImages.pdf). 58 | 59 | 60 | ### Related products 61 | * Full-Stack React Projects [[Packt]](https://www.packtpub.com/web-development/full-stack-react-projects?utm_source=github&utm_medium=repository&utm_campaign=9781788835534) [[Amazon]](https://www.amazon.com/dp/1788835530) 62 | 63 | * React: Cross-Platform Application Development with React Native [[Packt]](https://www.packtpub.com/web-development/react-cross-platform-application-development-react-native?utm_source=github&utm_medium=repository&utm_campaign=9781789136081) [[Amazon]](https://www.amazon.com/dp/1789136083) 64 | 65 | ## Get to Know the Author 66 | **Kirill Konshi** 67 | Kirill Konshin is the principal software developer at RingCentral, the world's leading Cloud communications provider. He is a highly experienced professional in full-stack web engineering with more than 10 years of experience, proficient in all the most recent web technologies. He is also an active open source contributor to React-related projects. You can follow him on Medium. 68 | 69 | 70 | ### Suggestions and Feedback 71 | [Click here](https://docs.google.com/forms/d/e/1FAIpQLSdy7dATC6QmEL81FIUuymZ0Wy9vH1jHkvpY57OiMeKGqib_Ow/viewform) if you have any feedback or suggestions. 72 | ### Download a free PDF 73 | 74 | If you have already purchased a print or Kindle version of this book, you can get a DRM-free PDF version at no cost.
Simply click on the link to claim your free PDF.
75 |

https://packt.link/free-ebook/9781788993661

-------------------------------------------------------------------------------- /book/1-introduction.md: -------------------------------------------------------------------------------- 1 | For quite some time the client-server architecture was one of the most wide spread patterns in large scale software development. Even systems that run purely on one computer often are designed this way. This allows to clearly separate concerns: server takes care of heavy business logic, persistent storage, accessing data from third party services and so on and client is responsible solely for presentation to end users. 2 | 3 | This architecture also allows to have multiple clients connected to one backend: mobile apps, IoT devices, third party REST API consumers (e.g. external developers) and web, for example. 4 | 5 | In early days of web development it was not that way though. Server was responsible for everything. Usually it was a combination of DB, app itself, template engine, bunch of static assets (images, CSS and so on) all baked together into a monolithic app. Later on it became obvious that this kind of architecture does not scale well. 6 | 7 | Nowadays modern web is moving back to client-server architecture with clean separation of concerns and concrete responsibilities of each component. Server side apps deal with data and client side apps deal with presentation of that data. 8 | 9 | We will cover following topics in this chapter: 10 | 11 | What is a single page app 12 | Introduction to React 13 | Single page apps performance issues 14 | Server side rendering with React 15 | What is a single page app 16 | Single page app implements such architecture for the web clients: the JavaScript app launches from a web page and then runs entirely on a client, all visual changes on website happen as a reaction on user actions and the data, that is fetched from the remote API server. 17 | 18 | It is called single page because server does not render pages for client, it always delivers same minimalistic markup required to bootstrap the JS app. All page rendering and navigation is happening purely on client using JavaScript which utilizes History APIs to dynamically swap page contents and URL in location bar. 19 | 20 | The advantages that this approach gives are: client can run something in background between page transitions, client does not have to re-download and re-draw the entire page in order to swap only the main content. Unfortunately, it also brings drawbacks, because now client is responsible for all state changes, synchronization of such changes across the entire interface, it must know when to load the data and what particular data. In other words, server generated app conceptually is a way simpler thing thanks to REST service + JS client. 21 | 22 | Creating JS Modules, code sharing, code splitting, bundling 23 | Separation of concerns is one of the key principles in software design, and since each entity in the code has to be isolated from others it makes sense to put them into separate files to simplify the navigation and ensure isolation. 24 | Modern JS applications consist of modules which can have exports and imports. JS modules export some entities and may consume exported entities from other modules. 25 | 26 | In this book we will use latest JS syntax with classes, arrow functions, spread operators and so on. If you are not familiar with this syntax you can always refer to it here: http://exploringjs.com. 27 | 28 | The simplest JS module looks like this: 29 | 30 | // A.js: 31 | export const noop = () => {}; 32 | This file now has a named export noop which is a an arrow function that does nothing. 33 | 34 | Now in B.js we can import a function from file A.js: 35 | 36 | //B.js: 37 | import {noop} from "./A.js"; 38 | noop(); 39 | In real world, dependencies are much more complex and modules can export dozens of entities and import dozens of other modules, including those from NPM. Module system in JS allows to statically trace all dependencies and figure out ways to optimize them. 40 | 41 | If the client will download all JS in a straightforward way (download initial one JS file, parse it’s dependencies, and recursively download them and their deps) then load time will be dramatic. First of all because network interaction takes time, second, because parsing also takes time. Simultaneous connections are often limited by browser and HTTP 2.0 42 | (which allows to transfer many files through one connection) is not yet available everywhere, so it makes sense to bundle all assets into one big bundle and deliver all at once. 43 | 44 | In order to do this, we can use a bundler like Weback or Rollup. These bundlers are capable of tracing all dependencies starting from initial module and up to leaf ones and packing those modules together in a single bundle. Also if, configured they allow to minify the bundle using UglifyJS or any other compressor. 45 | 46 | Bud bundle approach also have caveats. Bundle may contain things that are not required to render particular requested page. Basically, client can download a huge initial bundle but in fact need only 30-40% of it. 47 | 48 | Modern bundlers allow to split the app into smaller chunks and progressively load them on demand when needed. In order to create a code split point we can use the dynamic import syntax: 49 | 50 | //B.js: 51 | import('./A.js').then(({noop}) => { 52 | noop(); 53 | }); 54 | Now the build tool can see that certain modules should not be included in initial chunk and can be loaded on demand. But on the other hand, if those chunks will be too granular we will return to the starting point with tons of small files. 55 | 56 | Unfortunately, if chunks will be less granular then, most likely they will have some modules included in more than one chunk. Those common modules (primarily the ones installed from NPM) could be moved to so-called common chunk. The goal is to find optimal proportion between initial bundle size, commons chunk size and size of code-splitted chunks. 57 | 58 | Introduction to React 59 | For this section we will create a simple React based project and learn how this library works and what are the core concepts. 60 | 61 | Let’s create an empty project folder and initialize NPM: 62 | 63 | $ mkdir learn-react 64 | $ cd learn-react 65 | $ npm init 66 | $ npm install react react-dom --save 67 | The quickest way to get started with React is to use package react-scripts. 68 | 69 | $ npm install react-scripts --save-dev 70 | Now let’s add a start script to package.json: 71 | 72 | { 73 | "scripts": { 74 | "start": "react-scripts start" 75 | } 76 | } 77 | NPM auto-binds CLI scripts installed to node_modules/.bin directory along with the packages, so we can use scripts directly. 78 | 79 | The smallest possible setup for React app is the following, we need a landing HTML page and one script with the app. 80 | 81 | Let’s start with bedrock HTML: 82 | 83 | 84 | 85 | 86 | 87 | 88 | Learn React 89 | 90 | 91 |
92 | 93 | 94 | And the main JS file: 95 | 96 | //src/index.js: 97 | import React from "react"; 98 | import {render} from "react-dom"; 99 | render( 100 |

It works!

, 101 | document.getElementById('app') 102 | ); 103 | This is it. Now we can run the command to start development server: 104 | 105 | $ npm start 106 | It will open http://localhost:3000 and display It works! text. 107 | 108 | To learn more about React JSX, I encourage you to take a look at official documentation: 109 | https://reactjs.org/docs/introducing-jsx.html. This chapter will only briefly cover the main aspects that are essential for NextJS apps. 110 | The simplest React component is just a function that takes props as argument and returns JSX: 111 | 112 | const Cmp = (props) => (
{props.children}
); 113 | A more complicated components may have state: 114 | 115 | class Cmp extends React.Component { 116 | state = {value: 'init'}; 117 | onClick = (event) => { this.setState({value: 'clicked'}); }; 118 | render() { 119 | return ( 120 | 121 | ); 122 | } 123 | } 124 | Components may have static properties: 125 | 126 | class Cmp extends React.Component { 127 | static foo = 'foo'; 128 | } 129 | or 130 | 131 | Cmp.foo = 'foo'; 132 | These static properties are often used to describe some meta information about the components: 133 | 134 | import PropTypes from "prop-types"; 135 | Cmp.propTypes = { 136 | propName: PropTypes.string 137 | }; 138 | Next JS is actively using them and we will later show you how. 139 | 140 | The simplest way to achieve code splitting in React application is to store the entire progressively loaded component in state: 141 | 142 | class Cmp extends React.Component { 143 | state = {Sub: null}; 144 | onClick = async (event) => { 145 | const Sub = (await import('./path/to/Sub.js')).default; 146 | this.setState({Sub}); 147 | }; 148 | render() { 149 | const {Sub} = this.state; 150 | return ( 151 |
152 | 153 | 154 |
155 | ); 156 | } 157 | } 158 | Another way to achieve the code splitting is to use the React Router. 159 | 160 | All React components have lifecycle hooks that can be utilized, for example, to load the data from remote server: 161 | 162 | class Cmp extends React.Component { 163 | state = {data: null}; 164 | async componentWillMount() { 165 | const data = await (await fetch('https://example.com')).json(); 166 | this.setState({data}); 167 | } 168 | render() { 169 | const {data} = this.state; 170 | return ( 171 |
172 |         {JSON.stringify(data)}
173 |       
174 | ); 175 | } 176 | } 177 | React API has way more stuff, but these things are absolutely essential for Next JS. 178 | 179 | Why single page apps suffer performance issues? 180 | In order to start SPA has to download lots of assets to the client: JS files with the app itself, CSS files with styles, images, media and so on. It is impossible to develop a large scale JS app without any kind of modularization, so most JS apps consist of numerous small JS files (the above mentioned modules). CSS files also usually are separated by some criteria: per component, per page, etc. 181 | 182 | The nature of SPAs forces them to have heavy API traffic, basically, any user action that has to be persisted requires an API call. Pulling data from persistent storage also requires API calls. 183 | 184 | Both of these two aspects bring us to the most terrible SPA performance issue: the initial load time could be quite long. There were studies that clearly shown the correlation between the load time and page views, conversion and other vital metrics. On average customers leave the page if it fails to load within 2-3 seconds. 185 | 186 | Another big issue is SEO. Search engines tend to give higher rank to pages that load quicker. Plus, only recently crawlers learned how to parse and crawl SPAs properly. 187 | 188 | How to deal with it? 189 | 190 | Assume we have found a good balance between initial chunk and on demand chunks. We have applied compression and good cache strategies, but still, there is API layer that also has to be optimized for initial load. 191 | 192 | Potentially, we can combine all API requests in one huge request and load it. But different pages need different data, so we can’t create a request that will fit all, at least not within REST principles. Also, some of the data requires client side processing before we can make a subsequent request for more data. Modern API techniques like GraphQL allows 193 | to solve it more or less, and we will talk about it later in the book, but this still does not address the issue with not-so-smart search engine crawlers. 194 | 195 | Sad? Yes. But there is a solution for that. It is called server side rendering. 196 | 197 | Server side rendering 198 | Back in the days web servers used templates to deliver initial HTML to the client. Languages like Java, PHP, Python and Ruby were quite suitable for such kind of tasks. Those pages were called server-generated. Basically, all navigation and interaction was based on those dynamically generated pages. 199 | 200 | Those server generated pages were very simple in terms of user interaction, some hover effects and simple scripts. Some time after a more complicated scenarios were introduced and the bias moved towards client side. Servers began to generate not just full templates but also replaceable fragments to reflect more in-place changes. Later on because of the shift to REST APIs, a cleaner separation of concerns brought industry away from server-generated approaches to fully JS driven apps. 201 | 202 | But in order to more efficiently load the initial data for JS app, we can utilize this approach a little bit. We can render the initial markup on the server and then let JS app take over. The main assumption here is the fact that server side renderer is usually much closer to API server, ideally in same collocation, so it has much better connection and way more bandwidth than remote clients. It also can utilize all benefits of HTTP2 or any other protocols to maintain fast data exchange. The server side renderer is capable of doing all those chained requests much faster than clients, and all codebase can be pre-loaded and pre-parsed. Also it can use more aggressive data caching strategies because invalidation 203 | also could be centrally maintained. 204 | 205 | To decrease code duplication we would like to use same technology and same templates both on client and on the server. 206 | Such kind of apps is called universal or isomorphic. 207 | 208 | The general approach is as follows: we take NodeJS server, install a web framework and start listening to incoming requests. On every request that matches certain URL we take the client scripts and use them to bootstrap the initial state of the app for given page. Then we serialize the resulting HTML and data, bake it together and send to client. 209 | 210 | Client immediately shows the markup and then bootstraps the app on the client, applying initial data and state and hence taking control. 211 | Next page transition will happen purely on the client, it will load data from regular API endpoints just like before. One of the trickiest parts of this approach is to make sure that same page with same HTML will be rendered both on client and on the server, which means we need to make sure the client app will be bootstrapped in a certain state, that will result in same HTML. 212 | 213 | This brings us to framework choice. Not all client-side frameworks are capable of server-side rendering, for example it would be quite challenging to write a jQuery app that will pick up state and render itself correctly and on top of existing HTML. 214 | 215 | How to do server side rendering with React 216 | Luckily React is built with 2 main concepts in mind: it’s state driven and it is capable of rendering to plain HTML. React is often used with React Router, so let’s take this and explain how to render your React app on a server. 217 | 218 | React-based server side rendering frameworks, why Next JS 219 | Nowadays there are few competitors in React-based server side rendering market. We can divide them into the following categories: 220 | 221 | Drop in dynamic solution (NextJS, Electrode) 222 | Drop in static solution (Gatsby, React Static) 223 | Custom solutions 224 | The main difference between first two approaches is the way it builds and serves the app from server. 225 | 226 | Static solution makes a static HTML build (with all possible router pages) and then this build can be served by static server like Nginx, or any other. All HTML is pre-bakes, as well as initial state. This is very suitable for websites with incremental content updates that happen infrequently, for example, a blog. 227 | 228 | Dynamic solution generates HTML on the fly every time when client requests it. This means we can put any dynamic logic, any dynamic HTML blocks like per-request ads and so on. But the drawback is that it requires a long running server. 229 | This server has to be monitored and ideally should become a cluster of servers for redundancy to make sure of its’ high availability. 230 | 231 | We will put main focus of this book on dynamic solutions as they are more flexible and more complex and so require deeper understanding. 232 | 233 | For better understanding lets dive deeper in custom solution using only React and React Router. 234 | 235 | Let's install the router and special package to configure routes statically (it's impossible to generate purely dynamic routes on server): 236 | 237 | npm i --save react-router-dom react-router-config 238 | Now let's configure the routes: 239 | 240 | const routes = [ 241 | { 242 | path: '/', 243 | exact: true, 244 | component: Index 245 | }, 246 | { 247 | path: '/list', 248 | component: List 249 | } 250 | ]; 251 | export default routes; 252 | The main app entry point should then look like this: 253 | 254 | // index.js 255 | import React from 'react'; 256 | import {render} from 'react-dom'; 257 | import BrowserRouter from 'react-router-dom/BrowserRouter'; 258 | import { renderRoutes } from 'react-router-config'; 259 | import routes from './routes'; 260 | 261 | const Router = () => { 262 | return ( 263 | 264 | {renderRoutes(routes)} 265 | 266 | ) 267 | }; 268 | 269 | render(, document.getElementById('app')); 270 | On server it will be the following: 271 | 272 | import express from 'express'; 273 | import React from 'react'; 274 | import { renderToString } from 'react-dom/server'; 275 | import StaticRouter from 'react-router-dom/StaticRouter'; 276 | import { renderRoutes } from 'react-router-config'; 277 | import routes from './src/routes'; 278 | 279 | const app = express(); 280 | 281 | app.get('*', (req, res) => { 282 | let context = {}; // pre-fill somehow 283 | const content = renderToString( 284 | 285 | {renderRoutes(routes)} 286 | 287 | ); 288 | res.render('index', {title: 'SSR', content }); 289 | }); 290 | But this will simply render the page with no data. In order to pre-populate data into the page we need to do the following, both in component and in server: 291 | 292 | Each data-enabled component must expose a method that the server should call during route resolution 293 | Server iterates over all matched components and utilizes exposed methods 294 | Server collects the data and puts in some storage 295 | Server renders the HTML using routes and data from storage 296 | Server sends to the client the resulting HTML along with data 297 | Client initializes using HTML and pre-populates state using data 298 | We purposely won't show steps 3 and more because there is no generic way for pure React and React Router. For storage most solutions will use Redux and this is a whole another topic. So here we just show the basic principle. 299 | 300 | // list.js 301 | import React from "react"; 302 | 303 | const getText = async () => (await (await fetch('https://api.github.com/users/octocat')).text()); 304 | 305 | export default class List extends React.Component { 306 | 307 | state = {text: ''}; 308 | 309 | static async getInitialProps(context) { 310 | context.text = await getText(); 311 | } 312 | 313 | async componentWillMount() { 314 | const text = await getText(); 315 | this.setState({text}) 316 | } 317 | 318 | render() { 319 | const {staticContext} = this.props; 320 | let {text} = this.state; 321 | if (staticContext && !text) text = staticContext.text; 322 | return ( 323 |
Text: {text}
324 | ); 325 | } 326 | 327 | } 328 | // server.js 329 | // all from above 330 | app.get('*', (req, res) => { 331 | const {url} = req; 332 | const matches = matchRoutes(routes, url); 333 | const context = {}; 334 | const promises = matches.map(({route}) => { 335 | const getInitialProps = route.component.getInitialProps; 336 | return getInitialProps ? getInitialProps(context) : Promise.resolve(null) 337 | }); 338 | return Promise.all(promises).then(() => { 339 | console.log('Context', context); 340 | const content = renderToString( 341 | 342 | {renderRoutes(routes)} 343 | 344 | ); 345 | res.render('index', {title: 'SSR', content}); 346 | }); 347 | }); 348 | The reason why we are not covering those aspects is because even after tons of research it becomes obvious that custom solution will always have quirks and glitches primarily because React Router was not meant to be used on a server, so 349 | every custom solution is basically a bunch of hacks. It would be much better to take a stable solution which was built with Server Side Rendering in mind from day one. 350 | 351 | Among other competitors NextJS stands out as one of the pioneers of the approach, this framework is so far the most popular these days. Primarily because it offers a very convenient API, easy installation, zero config and huge community. Unlike Electrode which is extremely painful to configure. 352 | 353 | Full comparison is available in my article https://medium.com/disdj/solutions-for-react-app-development-f9fcaeba504. 354 | 355 | Summary 356 | In this chapter we have learned how web apps evolved over time from simple server generated pages to single page apps and then back to server generated pages with SPAs on top. We learned what is React JS and how to server-render the React application. 357 | 358 | In the next chapter we will use this knowledge to build a more advanced application that still follows these core principles. -------------------------------------------------------------------------------- /book/2-basics.md: -------------------------------------------------------------------------------- 1 | In this chapter we will learn very basics of NextJS, like installation, development and production usage, how to create simple pages and add components to them, how to apply styles and add media. 2 | 3 | Installation 4 | Developer mode 5 | Pages 6 | Production mode 7 | Routing 8 | Dynamic routing 9 | SEO-friendly routing 10 | Styles 11 | Media 12 | Graphs 13 | Installation of NextJS 14 | First, create an empty project folder and initialize NPM in it: 15 | $ mkdir next-js-condensed 16 | $ cd next-js-condensed 17 | $ npm init 18 | After that let’s install the NextJS package: 19 | $ npm install nextjs@latest --save-dev 20 | $ npm install react@latest react-dom@latest --save 21 | The reason why we save NextJS to dev dependencies is to clearly separate deps for client and for server. Server-side deps will be in dev, client will be in regular. 22 | 23 | If you're using Git or any other source control it makes sense to add an ignore file that will remove build artifacts folder from the source control. We will show an example .gitignore file here: 24 | 25 | Running Next JS in developer mode 26 | In order to start the server by convention we need to define a start script in package.json so we will add the following there: 27 | 28 | { 29 | "scripts": { 30 | "start": "next" 31 | } 32 | } 33 | Now you can start the server by typing this in console: 34 | 35 | $ npm start 36 | Now if you visit http://localhost:3000 in your browser you will see the running server. 37 | 38 | Creating your first Next JS page 39 | Now let's create the first page and place it in pages folder: 40 | 41 | // pages/index.js 42 | import React from "react"; 43 | export default () => (
Hello, World!
); 44 | Now, if you run the dev server (npm start) and visit http://localhost:3000 you will see the page. 45 | 46 | 47 | Now let's see how NextJS handle errors in your files: 48 | 49 | // pages/index.js 50 | import React from "react"; 51 | export default () => (

Hello, World!

); 52 | // ^ here we purposely not closing this tag 53 | then reload the page and see this: 54 | 55 | 56 | Running Next JS production build 57 | Next JS supports two kinds of production usage: static and dynamic, the main difference is that static build can be served by any static HTTP server as a static web site, whereas dynamic usage means that there will be a NextJS server that executes production build. 58 | 59 | Static mode is best suitable for simple websites with no dynamic content. We need to add a script to package.json: 60 | 61 | { 62 | "scripts": { 63 | "static": "next export" 64 | } 65 | } 66 | Now if we run 67 | 68 | $ npm run static 69 | It will create a static build that we can deploy somewhere. We will cover this in later chapters. 70 | 71 | In order to build & run the site for dynamic production mode we will add more scripts to package.json: 72 | 73 | { 74 | "scripts": { 75 | "build": "next build", 76 | "server": "next start" 77 | } 78 | } 79 | Then in console run 80 | 81 | $ npm run build 82 | $ npm run server 83 | This will make the build and run the production server using that build. 84 | 85 | Making Next JS Routing 86 | Let's make another page: 87 | 88 | // pages/second.js 89 | import React from "react"; 90 | export default () => (
Second
); 91 | This new page is accessible via http://localhost:3000/second. 92 | 93 | Now let's add a link to that second page to the index page. If we use simple tag for this it will work, of course, but it will perform a regular server request instead of client-side navigation, so performance of such nav will be much worse, client will reload all the initialization payloads and will be forced to re-initialize the entire app. 94 | 95 | In order to do a proper client-side navigation we need to import a link component from NextJS: 96 | 97 | // pages/index.js 98 | import React from "react"; 99 | import Link from "next/link"; 100 | export default () => (); 101 | Here we added a new link to page content, notice that we have added an empty tag: 102 | 103 | Second 104 | is a wrapper on top of any component that can accept onClick prop, we will talk about that a bit later. 105 | 106 | Now open http://localhost:3000, click the link and verify that page is not reloading by looking in the network tab of developer tools. 107 | 108 | So what if we'd like to apply styles to the link? We should apply them not on but on component, separation of concerns at it's finest. accepts all nav-related props whereas (or any other component) is used for presentation (styles, look and feel). 109 | 110 | 111 | This code still works as expected. 112 | 113 | Link is also capable of one interesting thing, by default it use lazy loading of underlying nav page, but for maximum performance you may use , which will allow instant transition. 114 | 115 | Now let's code a more complicated case for custom button-like component. In order to pass a href prop to underlying component (in case top level component will not be recognized as link/button) we need to add a passHref prop. 116 | 117 | We also can import withRouter HOC from next/router to allow resolution of URLs in order to highlight if the desired route is already selected: 118 | 119 | // components/Btn.js 120 | import React from 'react'; 121 | import {withRouter} from 'next/router'; 122 | 123 | const Btn = ({href, onClick, children, router}) => ( 124 | 125 | 128 | 129 | ); 130 | 131 | export default withRouter(Btn); 132 | Now let's create a top nav component for all pages: 133 | 134 | // components/Nav.js 135 | import React from "react"; 136 | import Link from 'next/link'; 137 | import Btn from "./Btn"; 138 | 139 | export default () => ( 140 |
141 | Index 142 | Second 143 |
144 | ); 145 | And now let's use it in pages: 146 | 147 | // pages/index.js 148 | import React from 'react'; 149 | import Nav from "../components/Nav"; 150 | 151 | export default () => ( 152 |
153 |
157 | ); 158 | 159 | // pages/second.js 160 | import React from 'react'; 161 | import Nav from "../components/Nav"; 162 | 163 | export default () => ( 164 |
165 |
169 | ); 170 | Dynamic Routing 171 | Of course no real app can live with only static URLs based on just pages, so let's add a bit of dynamic routing to our app. 172 | 173 | Let's start with a small data source stub: 174 | 175 | // data/posts.js 176 | export default [ 177 | {title: 'Foo'}, 178 | {title: 'Bar'}, 179 | {title: 'Baz'}, 180 | {title: 'Qux'} 181 | ]; 182 | Now let's connect it to our index page: 183 | 184 | // pages/index.js 185 | import React from 'react'; 186 | import Link from "next/link"; 187 | import Nav from "../components/Nav"; 188 | import posts from "../data/posts"; 189 | 190 | export default () => ( 191 |
208 | ); 209 | Here we imported the data source and iterated over it to produce some simple nav links, as you see, for convenience we may also use href as URL object, NextJS will serialize it into a standard string. 210 | 211 | Now, let's update the second page: 212 | 213 | // pages/second.js 214 | import React from 'react'; 215 | import Nav from "../components/Nav"; 216 | import posts from "../data/posts"; 217 | 218 | export default ({url: {query: {id}}}) => ( 219 |
220 |
224 | ); 225 | Now if we visit http://localhost:3000 we will see a clickable list of posts, each of them leads to a dedicated dynamic page. 226 | 227 | Unfortunately, now if we visit second page directly from our Nav component (by clicking a top menu button) we will get a nasty error. Let's make it prettier. We should import a special NextJS Error component and return it in case of any errors: 228 | 229 | // pages/second.js 230 | import React from 'react'; 231 | import Error from 'next/error'; 232 | import Nav from "../components/Nav"; 233 | import posts from "../data/posts"; 234 | 235 | export default ({url: {query: {id}}}) => ( 236 | (posts[id]) ? ( 237 |
238 |
242 | ) : ( 243 | 244 | ) 245 | ); 246 | We have added an import: 247 | 248 | import Error from 'next/error'; 249 | And wrapped the component in a ternary operator: 250 | 251 | export default ({url: {query: {id}}}) => ( 252 | (posts[id]) ? (...) : () 253 | ); 254 | This will return a NextJS nice 404 Page not found error page. 255 | 256 | Making Next JS Routing masks: SEO-friendly URLs 257 | If you look at the location bar of the browser when you visit the second page, you'll see something like http://localhost:3000/second?id=0, which is kinda fine, but not pretty enough. We can add some niceness to the URL schema that we use. This is optional, but it's always good to have SEO-friendly URLs instead of Query-String based stuff. 258 | 259 | In order to do that we should use a special as prop of Link component: 260 | 261 | 262 | {post.title} 263 | 264 | But if you visit such link and reload the page you will see 404. Why is that? It's because URL masking (a technology we just used) works on client side in runtime, and when we reload the page we need to teach server to work with such URLs. 265 | 266 | In order to do that we will have to make a custom server. Luckily, NextJS offers useful tools to simplify it. 267 | 268 | Let's start with installing Express: 269 | 270 | $ npm install --save-dev express 271 | The server code should look like this: 272 | 273 | // /server.js 274 | const express = require('express'); 275 | const next = require('next'); 276 | 277 | const port = 3000; 278 | // use default NodeJS environment variable to figure out dev mode 279 | const dev = process.env.NODE_ENV !== 'production'; 280 | const app = next({dev}); 281 | const handle = app.getRequestHandler(); 282 | const server = express(); 283 | 284 | server.get('/post/:id', (req, res) => { 285 | const actualPage = '/second'; 286 | const queryParams = {id: req.params.id}; 287 | app.render(req, res, actualPage, queryParams); 288 | }); 289 | 290 | server.get('*', (req, res) => { // pass through everything to NextJS 291 | return handle(req, res) 292 | }); 293 | 294 | app.prepare().then(() => { 295 | 296 | server.listen(port, (err) => { 297 | if (err) throw err 298 | console.log('NextJS is ready on http://localhost:' + port); 299 | }); 300 | 301 | }).catch(e => { 302 | 303 | console.error(e.stack); 304 | process.exit(1); 305 | 306 | }); 307 | The main thing in this code is the following code block: 308 | 309 | server.get('/post/:id', (req, res) => { 310 | const actualPage = '/second'; 311 | const queryParams = {id: req.params.id}; 312 | app.render(req, res, actualPage, queryParams); 313 | }); 314 | It uses URL parser to figure out URL param and provide it to actual page as a query string param, that is understandable by NextJS server side renderer. 315 | 316 | In order to launch this as usual we need to alter package.json's scripts section: 317 | 318 | { 319 | "scripts": { 320 | "start": "node server.js" 321 | } 322 | } 323 | Now if we run 324 | 325 | $ npm start 326 | As we did before and directly open a post: http://localhost:3000/post/0 it will work as expected. 327 | 328 | Adding styles to application, what is CSS in JS 329 | There are many ways how NextJS app can be styled. 330 | 331 | Simplest way is to use inline styles. Obviously this is the worst possible way, but we'll start small. 332 | 333 | const selectedStyles = { 334 | fontWeight: 'bold' 335 | }; 336 | 337 | const regularStyles = { 338 | fontWeight: 'normal' 339 | }; 340 | 341 | const Btn = ({href, onClick, children, pathname}) => ( 342 | 345 | ); 346 | Obviously this does not scale at all. Luckily, NextJS offers a technique called JSS aka CSS in JS. 347 | 348 | // components/button.js 349 | import React from 'react'; 350 | import {withRouter} from 'next/router'; 351 | 352 | export default withRouter(({href, onClick, children, router}) => ( 353 | 354 | 358 | 373 | 374 | )); 375 | This will create a scoped stylesheet. If you want a global one you should use