├── .babelrc ├── .gitignore ├── .prettierrc ├── README.md ├── components ├── organisms │ ├── Header.tsx │ └── __tests__ │ │ ├── Header.test.tsx │ │ └── __snapshots__ │ │ └── Header.test.tsx.snap └── utils │ └── withStore.tsx ├── decls.d.ts ├── jest.config.js ├── lib └── __tests__ │ └── example.test.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── about.tsx ├── connected.tsx ├── dynamic.tsx ├── index.tsx ├── item.tsx └── lazy.tsx ├── reducers ├── foo.ts ├── index.ts └── router.ts ├── routes.d.ts ├── routes.js ├── server.js ├── serviceWorker └── index.js ├── store └── create.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel", "@zeit/next-typescript/babel"], 3 | "plugins": [ 4 | [ 5 | "babel-plugin-styled-components", 6 | { 7 | "ssr": true, 8 | "displayName": true, 9 | "preprocess": false 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/967cd6479319efde70a6fa44fa1bfa02020f2357/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # vuepress build output 72 | .vuepress/dist 73 | 74 | # Serverless directories 75 | .serverless 76 | 77 | 78 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # next.js boilerplate 2018 late 2 | 3 | ## What's this? 4 | 5 | - typescript 6 | - next-routes 7 | - jest 8 | - prettier 9 | - redux / next-redux-wrapper / withStore helper 10 | - styled-components 11 | - Async Loading Example 12 | - ServiceWorker 13 | - Show loading spinner on transition 14 | - TODO: Handle Redirect in Server 15 | - TODO: Auth 16 | - TODO: Scroll Position Restore 17 | - TODO: Add types to next 18 | - TODO: Express Middleware 19 | - TODO: API Server 20 | 21 | ## How to use redux on SSR 22 | 23 | ```tsx 24 | import * as React from "react"; 25 | import { connect } from "react-redux"; 26 | import { RootState } from "reducers"; 27 | import withStore from "../components/utils/withStore"; 28 | 29 | export default withStore(async (store: any) => { 30 | store.dispatch({ type: "FOO", payload: "foo-on-connected" }); 31 | })((_props: any) => { 32 | return ( 33 |
34 |

Connceted

35 | 36 |
37 | ); 38 | }); 39 | 40 | const Connected = connect((s: RootState) => s.foo)(props => { 41 | return ( 42 |
43 | store state 44 |
45 |         {JSON.stringify(props)}
46 |       
47 |
48 | ); 49 | }); 50 | ``` 51 | 52 | `withStore` run in `getInitialProps` on server and client 53 | 54 | ## Deploy 55 | 56 | ```sh 57 | npm i -g now 58 | now 59 | ``` 60 | 61 | Example https://newnext-vqakgeaqus.now.sh/ 62 | 63 | ## LICENSE 64 | 65 | MIT 66 | -------------------------------------------------------------------------------- /components/organisms/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { Link } from "../../routes"; 4 | 5 | export default () => ( 6 |
7 |

Next.js boilerplate 2018 late

8 | 9 | index 10 | 11 | | 12 | 13 | about 14 | 15 | | 16 | 17 | dynamic 18 | 19 | | 20 | 21 | lazy 22 | 23 | | 24 | 25 | /item/a 26 | 27 | | 28 | 29 | /item/b 30 | 31 | | 32 | 33 | /connected 34 | 35 |
36 | ); 37 | -------------------------------------------------------------------------------- /components/organisms/__tests__/Header.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as renderer from "react-test-renderer"; 3 | import Header from "../Header"; 4 | 5 | test("renders correctly", () => { 6 | const tree = renderer.create(
).toJSON(); 7 | expect(tree).toMatchSnapshot(); 8 | }); 9 | -------------------------------------------------------------------------------- /components/organisms/__tests__/__snapshots__/Header.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |
5 |

6 | Next.js boilerplate 2018 late 7 |

8 | 12 | index 13 | 14 | | 15 | 19 | about 20 | 21 | | 22 | 26 | dynamic 27 | 28 | | 29 | 33 | lazy 34 | 35 | | 36 | 40 | /item/a 41 | 42 | | 43 | 47 | /item/b 48 | 49 | | 50 | 54 | /connected 55 | 56 |
57 | `; 58 | -------------------------------------------------------------------------------- /components/utils/withStore.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const withStore = (fn: any) => { 4 | return (Wrapped: any) => { 5 | return class extends React.Component { 6 | static async getInitialProps(ctx: any) { 7 | await fn(ctx.store); 8 | } 9 | render() { 10 | return ; 11 | } 12 | }; 13 | }; 14 | }; 15 | 16 | export default withStore; 17 | -------------------------------------------------------------------------------- /decls.d.ts: -------------------------------------------------------------------------------- 1 | declare module "next-redux-wrapper"; 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/lib", "/components"], 3 | transform: { 4 | "^.+\\.tsx?$": "ts-jest" 5 | }, 6 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", 7 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"] 8 | }; 9 | -------------------------------------------------------------------------------- /lib/__tests__/example.test.ts: -------------------------------------------------------------------------------- 1 | test("example", () => { 2 | // do nothing 3 | }); 4 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | // next.config.js 2 | const withTypescript = require("@zeit/next-typescript"); 3 | module.exports = withTypescript({ 4 | webpack(config, options) { 5 | return config; 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "newnext", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "author": "mizchi ", 7 | "scripts": { 8 | "dev": "node server.js", 9 | "build": "next build", 10 | "start": "NODE_ENV=production node server.js", 11 | "test": "jest", 12 | "test-cov": "jest --coverage", 13 | "test:types": "tsc -p . --noEmit" 14 | }, 15 | "dependencies": { 16 | "@zeit/next-typescript": "^1.1.1", 17 | "express": "^4.16.3", 18 | "next": "^7.0.1", 19 | "next-redux-wrapper": "^2.0.0", 20 | "next-routes": "^1.4.2", 21 | "react": "^16.5.2", 22 | "react-dom": "^16.5.2", 23 | "react-redux": "^5.0.7", 24 | "redux": "^4.0.0", 25 | "styled-components": "^3.4.9" 26 | }, 27 | "devDependencies": { 28 | "@types/jest": "^23.3.3", 29 | "@types/next": "^7.0.0", 30 | "@types/react": "^16.4.14", 31 | "@types/react-redux": "^6.0.9", 32 | "@types/react-test-renderer": "^16.0.3", 33 | "@types/redux": "^3.6.0", 34 | "@types/redux-logger": "^3.0.6", 35 | "@types/redux-promise": "^0.5.28", 36 | "@types/styled-components": "^3.0.1", 37 | "babel-plugin-styled-components": "^1.8.0", 38 | "eslint": "^5.6.1", 39 | "eslint-config-prettier": "^3.1.0", 40 | "eslint-plugin-prettier": "^3.0.0", 41 | "jest": "^23.6.0", 42 | "react-test-renderer": "^16.5.2", 43 | "redux-logger": "^3.0.6", 44 | "redux-promise": "^0.6.0", 45 | "ts-jest": "^23.10.3", 46 | "typescript": "^3.1.1", 47 | "typescript-eslint-parser": "^19.0.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import withRedux from "next-redux-wrapper"; 2 | import App, { Container } from "next/app"; 3 | import * as React from "react"; 4 | import { Provider, connect } from "react-redux"; 5 | import createStore from "../store/create"; 6 | import Header from "../components/organisms/Header"; 7 | import Router from "next/router"; 8 | import { RootState } from "reducers"; 9 | 10 | const RouteController = connect((s: RootState) => ({ 11 | routerLoading: s.router.loading 12 | }))( 13 | class RouteController extends React.PureComponent<{ 14 | dispatch: any; 15 | routerLoading: boolean; 16 | }> { 17 | _routeChangeStart = () => { 18 | this.props.dispatch({ type: "router:routing-started" }); 19 | // TODO: Use suspense 20 | }; 21 | _routeChangeComplete = () => { 22 | this.props.dispatch({ type: "router:routing-complete" }); 23 | }; 24 | componentDidMount() { 25 | Router.events.on("routeChangeStart", this._routeChangeStart); 26 | Router.events.on("routeChangeComplete", this._routeChangeComplete); 27 | } 28 | 29 | componentWillUnmount() { 30 | Router.events.off("routeChangeStart", this._routeChangeStart); 31 | Router.events.off("routeChangeComplete", this._routeChangeComplete); 32 | } 33 | 34 | render() { 35 | if (this.props.routerLoading) { 36 | return Loading...; 37 | } 38 | return this.props.children; 39 | } 40 | } 41 | ); 42 | 43 | class MyApp extends App { 44 | static async getInitialProps({ Component, ctx }: any) { 45 | const pageProps = Component.getInitialProps 46 | ? await Component.getInitialProps(ctx) 47 | : {}; 48 | return { pageProps }; 49 | } 50 | 51 | componentDidMount = () => { 52 | if ("serviceWorker" in navigator) { 53 | navigator.serviceWorker 54 | .register("/sw.js") 55 | .catch(err => console.error("Service worker registration failed", err)); 56 | } else { 57 | console.log("Service worker not supported"); 58 | } 59 | }; 60 | 61 | render() { 62 | const { Component, pageProps } = this.props; 63 | 64 | // TODO: Cast correctly 65 | const { store } = this.props as any; 66 | 67 | return ( 68 | 69 | 70 |
71 |
72 |
73 | 74 | 75 | 76 |
77 |
78 |
79 | ); 80 | } 81 | } 82 | 83 | export default withRedux(createStore)(MyApp); 84 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Document, { Head, Main, NextScript } from "next/document"; 3 | import { ServerStyleSheet } from "styled-components"; 4 | 5 | export default class MyDocument extends Document<{ styleTags: any }> { 6 | static getInitialProps({ renderPage }: any) { 7 | const sheet = new ServerStyleSheet(); 8 | const page = renderPage((App: any) => (props: any) => 9 | sheet.collectStyles() 10 | ); 11 | const styleTags = sheet.getStyleElement(); 12 | return { ...page, styleTags }; 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 | {this.props.styleTags} 19 | 20 |
21 | 22 | 23 | 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pages/about.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export default () =>

about

; 4 | -------------------------------------------------------------------------------- /pages/connected.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { connect } from "react-redux"; 3 | import { RootState } from "reducers"; 4 | import withStore from "../components/utils/withStore"; 5 | 6 | export default withStore(async (store: any) => { 7 | store.dispatch({ type: "FOO", payload: "foo-on-connected" }); 8 | })((_props: any) => { 9 | return ( 10 |
11 |

Connceted

12 | 13 |
14 | ); 15 | }); 16 | 17 | const Connected = connect((s: RootState) => s.foo)(props => { 18 | return ( 19 |
20 | store state 21 |
22 |         {JSON.stringify(props)}
23 |       
24 |
25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /pages/dynamic.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import dynamic from "next/dynamic"; 3 | 4 | const DynamicComponent = dynamic((() => import("./lazy")) as any, { 5 | loading: () =>

...

6 | }); 7 | 8 | export default () => ( 9 |
10 | Load About 11 | 12 |
13 | ); 14 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import styled from "styled-components"; 3 | 4 | export default () => Index; 5 | 6 | const Title = styled.h1` 7 | color: red; 8 | font-size: 50px; 9 | `; 10 | -------------------------------------------------------------------------------- /pages/item.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export default class Item extends React.Component { 4 | static async getInitialProps(ctx: any) { 5 | return ctx.query; 6 | } 7 | render() { 8 | return ( 9 |
10 |

Item

11 |
{JSON.stringify(this.props)}
12 |
13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pages/lazy.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export default class Lazy extends React.Component { 4 | static async getInitialProps(_ctx: any) { 5 | await new Promise(r => setTimeout(r, 1000)); 6 | return { p: 1 }; 7 | } 8 | render() { 9 | return ( 10 |
11 |

lazy loaded: {this.props.p}

12 |
13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /reducers/foo.ts: -------------------------------------------------------------------------------- 1 | export type State = { 2 | foo: string; 3 | }; 4 | 5 | const initialState: State = { 6 | foo: "" 7 | }; 8 | 9 | const foo = (state = initialState, action: any) => { 10 | switch (action.type) { 11 | case "FOO": 12 | return { ...state, foo: action.payload }; 13 | default: 14 | return state; 15 | } 16 | }; 17 | 18 | export default foo; 19 | -------------------------------------------------------------------------------- /reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import router, { State as RouterState } from "./router"; 3 | import foo, { State as FooState } from "./foo"; 4 | 5 | export type RootState = { 6 | router: RouterState; 7 | foo: FooState; 8 | }; 9 | 10 | export default combineReducers({ 11 | router, 12 | foo 13 | }); 14 | -------------------------------------------------------------------------------- /reducers/router.ts: -------------------------------------------------------------------------------- 1 | export type State = { 2 | loading: boolean; 3 | error: null | Error; 4 | }; 5 | 6 | const initialState: State = { 7 | loading: false, 8 | error: null 9 | }; 10 | 11 | const router = (state = initialState, action: any) => { 12 | switch (action.type) { 13 | case "router:routing-started": { 14 | return { ...state, loading: true }; 15 | } 16 | case "router:routing-complete": { 17 | return { ...state, loading: false }; 18 | } 19 | case "router:routing-error": { 20 | return { ...state, loading: false, error: new Error("...") }; 21 | } 22 | default: { 23 | return state; 24 | } 25 | } 26 | }; 27 | 28 | export default router; 29 | -------------------------------------------------------------------------------- /routes.d.ts: -------------------------------------------------------------------------------- 1 | export const Link: React.ComponentType; 2 | -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | const routes = require("next-routes"); 2 | 3 | module.exports = routes() 4 | .add("index", "/") 5 | .add("connected", "/connected") 6 | .add("about", "/about") 7 | .add("dynamic", "/dynamic") 8 | .add("item", "/item/:id"); 9 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const next = require("next"); 2 | const url = require("url"); 3 | const { createServer } = require("http"); 4 | const { createReadStream } = require("fs"); 5 | const routes = require("./routes"); 6 | 7 | const app = next({ dev: process.env.NODE_ENV !== "production" }); 8 | const handle = routes.getRequestHandler(app); 9 | 10 | app.prepare().then(() => { 11 | createServer((req, res) => { 12 | const parsedUrl = url.parse(req.url, true); 13 | const { pathname } = parsedUrl; 14 | if (pathname === "/sw.js") { 15 | res.setHeader("content-type", "text/javascript"); 16 | createReadStream("./serviceWorker/index.js").pipe(res); 17 | } else { 18 | handle(req, res, parsedUrl); 19 | } 20 | }).listen(3000, err => { 21 | if (err) throw err; 22 | console.log("> Ready on http://localhost:3000"); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /serviceWorker/index.js: -------------------------------------------------------------------------------- 1 | console.log("sw started"); 2 | -------------------------------------------------------------------------------- /store/create.ts: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from "redux"; 2 | import logger from "redux-logger"; 3 | import promise from "redux-promise"; 4 | import reducer, { RootState } from "../reducers"; 5 | 6 | const configureStore = (state: RootState | undefined, _options: any) => { 7 | return createStore(reducer, state as any, applyMiddleware(logger, promise)); 8 | }; 9 | 10 | export default configureStore; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "baseUrl": ".", 6 | "jsx": "react", 7 | "lib": ["dom", "es2017"], 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "noEmit": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "preserveConstEnums": true, 14 | "removeComments": false, 15 | "skipLibCheck": true, 16 | "sourceMap": true, 17 | "strict": true, 18 | "target": "esnext" 19 | } 20 | } 21 | --------------------------------------------------------------------------------