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