├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── Procfile ├── README.md ├── index.js ├── jest.config.js ├── jsconfig.json ├── nodemon.json ├── package.json ├── public ├── favicon.ico ├── manifest.json ├── robots.txt └── static │ └── images │ ├── svg │ ├── menu-icon.svg │ └── oct-icon.svg │ └── touch │ ├── launcher-icon-2x.png │ ├── launcher-icon-4x.png │ └── launcher-icon.png ├── src ├── actions │ ├── ui.js │ └── user.js ├── client.js ├── components │ ├── App │ │ └── index.js │ ├── Container │ │ └── index.js │ ├── Error │ │ ├── index.js │ │ ├── index.spec.js │ │ └── index.stories.js │ ├── Footer │ │ ├── index.js │ │ ├── index.spec.js │ │ └── index.stories.js │ ├── Header │ │ ├── index.js │ │ ├── index.spec.js │ │ └── index.stories.js │ ├── Logo │ │ ├── index.js │ │ └── index.stories.js │ ├── Main │ │ └── index.js │ ├── Menu │ │ ├── index.js │ │ ├── index.spec.js │ │ └── index.stories.js │ ├── MenuIcon │ │ ├── index.js │ │ ├── index.spec.js │ │ └── index.stories.js │ ├── OctIcon │ │ ├── index.js │ │ ├── index.spec.js │ │ └── index.stories.js │ ├── PreloadLink │ │ └── index.js │ ├── StackList │ │ ├── index.js │ │ ├── index.spec.js │ │ └── index.stories.js │ ├── SubTitle │ │ ├── index.js │ │ ├── index.spec.js │ │ └── index.stories.js │ ├── Title │ │ ├── index.js │ │ ├── index.spec.js │ │ └── index.stories.js │ ├── UserDetail │ │ ├── data.js │ │ ├── index.js │ │ ├── index.spec.js │ │ └── index.stories.js │ ├── UserList │ │ ├── data.js │ │ ├── index.js │ │ └── index.stories.js │ └── withExtendRouter │ │ └── index.js ├── config │ ├── env.js │ └── url.js ├── pages │ ├── about │ │ └── index.js │ ├── home │ │ └── index.js │ ├── notFound │ │ └── index.js │ ├── redirectAbout │ │ └── index.js │ └── userDetail │ │ └── index.js ├── reducers │ ├── index.js │ ├── ui.js │ └── user.js ├── routes.js ├── server.js ├── styles │ ├── index.js │ ├── media.js │ └── variables.js ├── sw.js └── utils │ ├── configureStore.js │ ├── customPush.js │ ├── getHtmlString.js │ ├── getPreloadResorceElement.js │ ├── helpers.js │ ├── link.js │ ├── meta.js │ └── path.js └── tools ├── jest └── setup.js ├── storybook ├── backgroundDecorator.js ├── config.js └── routerDecorator.js └── webpack ├── getClientPlugins.js ├── getModule.js ├── getResolve.js ├── getServerPlugins.js ├── webpack.client.babel.js └── webpack.server.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "plugins": [ 5 | "react-hot-loader/babel", 6 | [ 7 | "babel-plugin-styled-components", 8 | { 9 | "ssr": true 10 | } 11 | ] 12 | ] 13 | } 14 | }, 15 | "plugins": [ 16 | [ 17 | "module-resolver", 18 | { 19 | "root": [ 20 | "./", 21 | "./src" 22 | ] 23 | } 24 | ], 25 | "add-module-exports", 26 | "@loadable/babel-plugin", 27 | [ 28 | "@babel/plugin-transform-runtime", 29 | { 30 | "helpers": false 31 | } 32 | ] 33 | ], 34 | "presets": [ 35 | [ 36 | "@babel/preset-env", 37 | { 38 | "targets": { 39 | "browsers": "last 2 versions", 40 | "node": "current" 41 | } 42 | } 43 | ], 44 | "@babel/preset-react" 45 | ] 46 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | coverage/* 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "eslint:recommended", 5 | "plugin:react/recommended", 6 | "plugin:prettier/recommended", 7 | "plugin:jest/recommended", 8 | "prettier/react" 9 | ], 10 | "rules": { 11 | "global-require": 0, 12 | "class-methods-use-this": 0, 13 | "import/no-extraneous-dependencies": 0, 14 | "import/prefer-default-export": 0, 15 | "react/no-danger": 0, 16 | "react/jsx-filename-extension": 0, 17 | "react/jsx-props-no-spreading": 0, 18 | "react/no-array-index-key": 0, 19 | "react/require-default-props": 0, 20 | "react/prop-types": 0 21 | }, 22 | "parser": "babel-eslint", 23 | "env": { 24 | "browser": true, 25 | "node": true, 26 | "es2020": true 27 | }, 28 | "settings": { 29 | "import/resolver": { 30 | "babel-module": { 31 | "extensions": [".js"] 32 | } 33 | } 34 | }, 35 | "globals": { 36 | "shallow": true, 37 | "render": true, 38 | "mount": true, 39 | "toJson": true, 40 | "importScripts": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/* 3 | public/static/javascripts/loadable-stats.json 4 | coverage/* 5 | *.log 6 | yarn.lock 7 | package-lock.json 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-styled-components", 5 | "stylelint-config-prettier" 6 | ], 7 | "syntax": "css-in-js" 8 | } 9 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-ssr-starter 2 | 3 | All have been introduced React environment. 4 | 5 | ## Features 6 | 7 | - [react](https://reactjs.org/) 8 | - [react-router](https://reacttraining.com/react-router/) 9 | - [react-helmet-async](https://github.com/staylor/react-helmet-async/) 10 | - [react-hot-loader](http://gaearon.github.io/react-hot-loader/) 11 | - [redux](https://rackt.github.io/redux/) 12 | - [styled-components](https://www.styled-components.com/) 13 | - [loadable-components](https://www.smooth-code.com/open-source/loadable-components/) 14 | - [express](http://expressjs.com/) 15 | - [workbox](https://developers.google.com/web/tools/workbox/) 16 | - [eslint](https://eslint.org/) 17 | - [stylelint](https://stylelint.io/) 18 | - [prettier](https://prettier.io/) 19 | - [jest](https://facebook.github.io/jest/) 20 | - [enzyme](http://airbnb.io/enzyme/) 21 | - [storybook](https://storybook.js.org/) 22 | - [webpack](https://webpack.js.org/) 23 | - [babel](https://babeljs.io/) 24 | 25 | ## Install 26 | 27 | ``` 28 | $ git clone https://github.com/osamu38/react-ssr-starter.git 29 | $ cd react-ssr-starter 30 | $ npm i 31 | ``` 32 | 33 | ## Run 34 | 35 | ``` 36 | $ npm run dev 37 | ``` 38 | 39 | Go to `http://localhost:2525/`. 40 | 41 | ## Build 42 | 43 | ``` 44 | $ npm run build 45 | $ npm run build:client (Only build client) 46 | $ npm run build:server (Only build server) 47 | ``` 48 | 49 | ## Build and analyze 50 | 51 | ``` 52 | $ npm run build:analyze 53 | $ npm run build:client:analyze 54 | $ npm run build:server:analyze 55 | ``` 56 | 57 | ## Run for production 58 | 59 | ``` 60 | npm start 61 | ``` 62 | 63 | Go to `http://localhost:2525/`. 64 | 65 | ## Adding pages 66 | 67 | Basically page component is implemented using Functional Component. 68 | 69 | `pages/home/index.js` 70 | 71 | ```jsx 72 | import React from 'react'; 73 | import { Helmet } from 'react-helmet-async'; 74 | import Title from 'components/Title'; 75 | import SubTitle from 'components/SubTitle'; 76 | import UserList from 'components/UserList'; 77 | import { fetchUsers } from 'actions/user'; 78 | 79 | const HomePage = (props) => { 80 | const { 81 | state: { 82 | user: { userList }, 83 | }, 84 | } = props; 85 | 86 | return ( 87 | <> 88 | 89 | Home Page 90 | User List 91 | 92 | 93 | ); 94 | }; 95 | 96 | HomePage.loadData = async (ctx) => { 97 | const { 98 | dispatch, 99 | state: { 100 | user: { userList }, 101 | }, 102 | } = ctx; 103 | 104 | if (!userList.length) { 105 | return dispatch(fetchUsers()); 106 | } 107 | return Promise.resolve(); 108 | }; 109 | 110 | export default HomePage; 111 | ``` 112 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | require('./dist/server'); 3 | } else { 4 | require('@babel/register'); 5 | require('@babel/polyfill'); 6 | require('./src/server'); 7 | } 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: ['raf/polyfill', '/tools/jest/setup.js'], 3 | transform: { 4 | '.js': '/node_modules/babel-jest', 5 | }, 6 | snapshotSerializers: ['enzyme-to-json/serializer'], 7 | }; 8 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "*": ["./*", "src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src/server.js", "src/routes.js", "src/utils/getHtmlString.js"] 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-ssr-starter", 3 | "version": "1.0.0", 4 | "description": "All have been introduced React environment", 5 | "bugs": { 6 | "url": "https://github.com/osamu38/react-ssr-starter/issues" 7 | }, 8 | "license": "MIT", 9 | "author": "osamu38", 10 | "main": "index.js", 11 | "sideEffects": false, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/osamu38/react-ssr-starter.git" 15 | }, 16 | "scripts": { 17 | "build": "npm run build:client && npm run build:server", 18 | "build:analyze": "npm run build:client:analyze & npm run build:server:analyze", 19 | "build:client": "NODE_ENV=production webpack --progress --env --config ./tools/webpack/webpack.client.babel.js", 20 | "build:client:analyze": "NODE_ENV=production webpack --progress --env.analyze --config ./tools/webpack/webpack.client.babel.js", 21 | "build:server": "NODE_ENV=production webpack --progress --env --config ./tools/webpack/webpack.server.babel.js", 22 | "build:server:analyze": "NODE_ENV=production webpack --progress --env.analyze --config ./tools/webpack/webpack.server.babel.js", 23 | "lint": "eslint . --fix", 24 | "lint:css": "stylelint './src/**/*.js'", 25 | "dev": "nodemon index", 26 | "start": "NODE_ENV=production node index", 27 | "storybook": "start-storybook -s ./public -p 2626 -c ./tools/storybook", 28 | "test": "NODE_ENV=test jest", 29 | "test:update": "npm test -- -u", 30 | "test:coverage": "npm test -- --coverage", 31 | "heroku-postbuild": "npm run build" 32 | }, 33 | "dependencies": { 34 | "@loadable/component": "^5.13.1", 35 | "@loadable/server": "^5.13.1", 36 | "axios": "^0.19.2", 37 | "body-parser": "^1.19.0", 38 | "compression": "^1.7.4", 39 | "cookie-parser": "^1.4.5", 40 | "express": "^5.0.0-alpha.7", 41 | "helmet": "^3.23.3", 42 | "hpp": "^0.2.3", 43 | "html-minifier": "^4.0.0", 44 | "morgan": "^1.10.0", 45 | "path-to-regexp": "^3.1.0", 46 | "preact": "^10.4.5", 47 | "query-string": "^6.13.1", 48 | "react": "^16.13.1", 49 | "react-dom": "^16.13.1", 50 | "react-helmet-async": "^1.0.6", 51 | "react-redux": "5.1.1", 52 | "react-router-config": "5.1.1", 53 | "react-router-dom": "^5.2.0", 54 | "redux": "^4.0.5", 55 | "redux-thunk": "^2.3.0", 56 | "serialize-javascript": "^4.0.0", 57 | "serve-favicon": "^2.5.0", 58 | "styled-components": "^4.4.1", 59 | "styled-reset": "^4.2.0" 60 | }, 61 | "devDependencies": { 62 | "@babel/core": "^7.10.4", 63 | "@babel/plugin-transform-runtime": "^7.10.4", 64 | "@babel/polyfill": "^7.10.4", 65 | "@babel/preset-env": "^7.10.4", 66 | "@babel/preset-react": "^7.10.4", 67 | "@babel/register": "^7.10.4", 68 | "@loadable/babel-plugin": "^5.13.0", 69 | "@loadable/webpack-plugin": "^5.13.0", 70 | "@storybook/addon-links": "^5.3.19", 71 | "@storybook/react": "^5.3.19", 72 | "babel-eslint": "^10.1.0", 73 | "babel-jest": "^26.1.0", 74 | "babel-loader": "^8.1.0", 75 | "babel-plugin-add-module-exports": "^1.0.2", 76 | "babel-plugin-module-resolver": "^4.0.0", 77 | "babel-plugin-styled-components": "^1.10.7", 78 | "clean-webpack-plugin": "1.0.1", 79 | "copy-webpack-plugin": "^5.1.1", 80 | "enzyme": "^3.11.0", 81 | "enzyme-adapter-react-16": "^1.15.2", 82 | "enzyme-to-json": "^3.5.0", 83 | "eslint": "^7.4.0", 84 | "eslint-config-airbnb": "^18.2.0", 85 | "eslint-config-prettier": "^6.11.0", 86 | "eslint-import-resolver-babel-module": "^5.1.2", 87 | "eslint-plugin-import": "^2.22.0", 88 | "eslint-plugin-jest": "^23.18.0", 89 | "eslint-plugin-jsx-a11y": "^6.3.1", 90 | "eslint-plugin-prettier": "^3.1.4", 91 | "eslint-plugin-react": "^7.20.3", 92 | "eslint-plugin-react-hooks": "^4.0.6", 93 | "friendly-errors-webpack-plugin": "^1.7.0", 94 | "jest": "^26.1.0", 95 | "jest-styled-components": "^7.0.2", 96 | "json-loader": "^0.5.7", 97 | "nodemon": "^2.0.4", 98 | "prettier": "^2.0.5", 99 | "raf": "^3.4.1", 100 | "react-hot-loader": "^4.12.21", 101 | "redux-logger": "^3.0.6", 102 | "stylelint": "^13.6.1", 103 | "stylelint-config-prettier": "^8.0.2", 104 | "stylelint-config-standard": "^20.0.0", 105 | "stylelint-config-styled-components": "^0.1.1", 106 | "terser-webpack-plugin": "^3.0.6", 107 | "webpack": "^4.43.0", 108 | "webpack-bundle-analyzer": "^3.8.0", 109 | "webpack-cli": "^3.3.12", 110 | "webpack-dev-middleware": "^3.7.2", 111 | "webpack-hot-middleware": "^2.25.0", 112 | "webpack-manifest-plugin": "^2.2.0", 113 | "webpack-node-externals": "^1.7.2", 114 | "workbox-sw": "^5.1.3", 115 | "workbox-webpack-plugin": "^5.1.3" 116 | }, 117 | "engines": { 118 | "node": "12.14.1", 119 | "npm": "6.13.4" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osamu38/react-ssr-starter/03ae982346abeb4c24e6858239fb6b94a80aa9e0/public/favicon.ico -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "lang": "ja", 3 | "display": "standalone", 4 | "short_name": "RSSRS", 5 | "name": "react-ssr-starter", 6 | "icons": [ 7 | { 8 | "src": "/static/images/touch/launcher-icon.png", 9 | "type": "image/png", 10 | "sizes": "48x48" 11 | }, 12 | { 13 | "src": "/static/images/touch/launcher-icon-2x.png", 14 | "type": "image/png", 15 | "sizes": "96x96" 16 | }, 17 | { 18 | "src": "/static/images/touch/launcher-icon-4x.png", 19 | "type": "image/png", 20 | "sizes": "192x192" 21 | } 22 | ], 23 | "start_url": "/", 24 | "theme_color": "#f68084", 25 | "background_color": "#f68084" 26 | } 27 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | -------------------------------------------------------------------------------- /public/static/images/svg/menu-icon.svg: -------------------------------------------------------------------------------- 1 | 7 | 11 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /public/static/images/svg/oct-icon.svg: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/static/images/touch/launcher-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osamu38/react-ssr-starter/03ae982346abeb4c24e6858239fb6b94a80aa9e0/public/static/images/touch/launcher-icon-2x.png -------------------------------------------------------------------------------- /public/static/images/touch/launcher-icon-4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osamu38/react-ssr-starter/03ae982346abeb4c24e6858239fb6b94a80aa9e0/public/static/images/touch/launcher-icon-4x.png -------------------------------------------------------------------------------- /public/static/images/touch/launcher-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osamu38/react-ssr-starter/03ae982346abeb4c24e6858239fb6b94a80aa9e0/public/static/images/touch/launcher-icon.png -------------------------------------------------------------------------------- /src/actions/ui.js: -------------------------------------------------------------------------------- 1 | export const openMenu = () => { 2 | return (dispatch) => { 3 | dispatch({ type: 'OPEN_MENU' }); 4 | }; 5 | }; 6 | export const closeMenu = () => { 7 | return (dispatch) => { 8 | dispatch({ type: 'CLOSE_MENU' }); 9 | }; 10 | }; 11 | export const showError = (error) => { 12 | return (dispatch) => { 13 | dispatch({ 14 | type: 'SHOW_ERROR', 15 | payload: { 16 | error, 17 | }, 18 | }); 19 | }; 20 | }; 21 | export const hideError = () => { 22 | return (dispatch) => { 23 | dispatch({ type: 'HIDE_ERROR' }); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/actions/user.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const fetchUser = (id) => { 4 | return (dispatch) => 5 | axios 6 | .get(`https://jsonplaceholder.typicode.com/users/${id}`) 7 | .then((res) => { 8 | dispatch({ 9 | type: 'FETCH_USER', 10 | payload: res.data, 11 | }); 12 | }) 13 | .catch((err) => { 14 | // eslint-disable-next-line no-console 15 | console.error(err); 16 | }); 17 | }; 18 | export const fetchUsers = () => { 19 | return (dispatch) => 20 | axios 21 | .get('https://jsonplaceholder.typicode.com/users') 22 | .then((res) => { 23 | dispatch({ 24 | type: 'FETCH_USERS', 25 | payload: res.data, 26 | }); 27 | }) 28 | .catch((err) => { 29 | // eslint-disable-next-line no-console 30 | console.error(err); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import { HelmetProvider } from 'react-helmet-async'; 6 | import { loadableReady } from '@loadable/component'; 7 | import configureStore from 'utils/configureStore'; 8 | import App from 'components/App'; 9 | import { isDevelopment } from 'config/env'; 10 | 11 | // eslint-disable-next-line no-underscore-dangle 12 | const initialState = window.__INITIAL_STATE__; 13 | const store = configureStore(initialState); 14 | const root = document.getElementById('root'); 15 | 16 | loadableReady().then(() => { 17 | if (root) { 18 | ReactDOM.hydrate( 19 | 20 | 21 | 22 | 23 | 24 | 25 | , 26 | root 27 | ); 28 | } 29 | }); 30 | 31 | window.addEventListener('load', () => { 32 | if ('serviceWorker' in navigator && navigator.serviceWorker) { 33 | if (!isDevelopment) { 34 | navigator.serviceWorker 35 | .register('/static/javascripts/sw.js', { scope: '/' }) 36 | .then(() => { 37 | // SW registered 38 | }) 39 | .catch(() => { 40 | // SW registration failed 41 | }); 42 | } else { 43 | navigator.serviceWorker.getRegistrations().then((registrations) => { 44 | registrations.forEach((registration) => { 45 | registration.unregister(); 46 | }); 47 | }); 48 | caches.keys().then((keys) => { 49 | const promises = []; 50 | 51 | keys.forEach((cacheName) => { 52 | if (cacheName) { 53 | promises.push(caches.delete(cacheName)); 54 | } 55 | }); 56 | }); 57 | } 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /src/components/App/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { bindActionCreators, compose } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import { Route, Switch } from 'react-router-dom'; 5 | import { Helmet } from 'react-helmet-async'; 6 | import Container from 'components/Container'; 7 | import Footer from 'components/Footer'; 8 | import Header from 'components/Header'; 9 | import Main from 'components/Main'; 10 | import Error from 'components/Error'; 11 | import withExtendRouter from 'components/withExtendRouter'; 12 | import * as userActions from 'actions/user'; 13 | import * as uiActions from 'actions/ui'; 14 | import meta from 'utils/meta'; 15 | import link from 'utils/link'; 16 | import routes from 'routes'; 17 | import { isDevelopment } from 'config/env'; 18 | import GlobalStyle from 'styles'; 19 | 20 | class App extends React.PureComponent { 21 | componentDidUpdate(prevProps) { 22 | const { 23 | history: { action }, 24 | location: { pathname: prevPathname }, 25 | state: { 26 | ui: { isOpenMenu }, 27 | }, 28 | } = prevProps; 29 | const { 30 | location: { pathname: nextPathname }, 31 | actions: { 32 | uiActions: { closeMenu }, 33 | }, 34 | } = this.props; 35 | const isNotPop = action !== 'POP'; 36 | const isChengedPathname = prevPathname !== nextPathname; 37 | 38 | if (isNotPop && isChengedPathname) { 39 | window.scrollTo(0, 0); 40 | if (isOpenMenu) { 41 | closeMenu(); 42 | } 43 | } 44 | } 45 | 46 | render() { 47 | const { 48 | location: { pathname }, 49 | state: { 50 | ui: { isOpenMenu, error }, 51 | }, 52 | actions: { 53 | uiActions: { openMenu, closeMenu, hideError }, 54 | }, 55 | } = this.props; 56 | const metaData = meta.get(pathname); 57 | const linkData = link.get(pathname); 58 | 59 | return ( 60 | 61 | 62 | 63 | {error ? {error} : null} 64 |
69 |
70 | 71 | {routes.map((route, i) => ( 72 | ( 77 | 78 | )} 79 | /> 80 | ))} 81 | 82 |
83 |