├── .babelrc ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── screenshot.png ├── server ├── api.js ├── configureStore.js ├── index.js └── render.js ├── src ├── actions │ └── index.js ├── components │ ├── Admin.js │ ├── App.js │ ├── DevTools.js │ ├── Error.js │ ├── Home.js │ ├── List.js │ ├── Loading.js │ ├── Login.js │ ├── NotFound.js │ ├── Player.js │ ├── Sidebar.js │ ├── Switcher.js │ └── Video.js ├── configureStore.js ├── css │ ├── App.css │ ├── DevTools.css │ ├── Home.css │ ├── List.css │ ├── Sidebar.css │ ├── Switcher.css │ └── Video.css ├── index.js ├── options.js ├── reducers │ ├── actions.js │ ├── category.js │ ├── direction.js │ ├── index.js │ ├── jwToken.js │ ├── page.js │ ├── playing.js │ ├── slug.js │ ├── title.js │ ├── user.js │ ├── videosByCategory.js │ └── videosHash.js ├── routesMap.js ├── selectors │ └── isLoading.js └── utils.js ├── webpack ├── client.dev.js ├── client.prod.js ├── server.dev.js └── server.prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react", 5 | "stage-2" 6 | ], 7 | 8 | "plugins": [ 9 | "universal-import" 10 | ], 11 | 12 | "env": { 13 | "development": { 14 | "plugins": [ 15 | "react-hot-loader/babel" 16 | ] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | parserOptions: { 4 | ecmaFeatures: { 5 | generators: true, 6 | experimentalObjectRestSpread: true 7 | }, 8 | sourceType: 'module', 9 | allowImportExportEverywhere: false 10 | }, 11 | plugins: ['flowtype'], 12 | extends: ['airbnb', 'plugin:flowtype/recommended'], 13 | settings: { 14 | flowtype: { 15 | onlyFilesWithFlowAnnotation: true 16 | }, 17 | 'import/resolver': { 18 | node: { 19 | extensions: ['.js', '.json', '.css', '.styl'] 20 | } 21 | } 22 | }, 23 | globals: { 24 | window: true, 25 | document: true, 26 | __dirname: true, 27 | __DEV__: true, 28 | CONFIG: true, 29 | process: true, 30 | jest: true, 31 | describe: true, 32 | test: true, 33 | it: true, 34 | expect: true, 35 | beforeEach: true, 36 | fetch: true, 37 | alert: true 38 | }, 39 | rules: { 40 | 'import/extensions': [ 41 | 'error', 42 | 'always', 43 | { 44 | js: 'never', 45 | jsx: 'never', 46 | styl: 'never', 47 | css: 'never' 48 | } 49 | ], 50 | 'no-shadow': 0, 51 | 'no-use-before-define': 0, 52 | 'no-param-reassign': 0, 53 | 'react/prop-types': 0, 54 | 'react/no-render-return-value': 0, 55 | 'no-confusing-arrow': 0, 56 | 'no-underscore-dangle': 0, 57 | 'no-plusplus': 0, 58 | camelcase: 1, 59 | 'prefer-template': 1, 60 | 'react/no-array-index-key': 1, 61 | 'global-require': 1, 62 | 'react/jsx-indent': 1, 63 | 'dot-notation': 1, 64 | 'import/no-named-default': 1, 65 | 'no-unused-vars': 1, 66 | 'flowtype/no-weak-types': 1, 67 | 'consistent-return': 1, 68 | 'import/prefer-default-export': 1, 69 | 'no-console': 1, 70 | 'jsx-a11y/no-static-element-interactions': 1, 71 | 'no-case-declarations': 1, 72 | semi: [2, 'never'], 73 | 'flowtype/semi': [2, 'never'], 74 | 'jsx-quotes': [2, 'prefer-single'], 75 | 'react/jsx-filename-extension': [2, { extensions: ['.jsx', '.js'] }], 76 | 'spaced-comment': [2, 'always', { markers: ['?'] }], 77 | 'arrow-parens': [2, 'as-needed', { requireForBlockBody: false }], 78 | 'brace-style': [2, 'stroustrup'], 79 | 'import/no-unresolved': [2, { commonjs: true, caseSensitive: true }], 80 | 'no-unused-expressions': [ 81 | 2, 82 | { 83 | allowShortCircuit: true, 84 | allowTernary: true, 85 | allowTaggedTemplates: true 86 | } 87 | ], 88 | 'import/no-extraneous-dependencies': [ 89 | 'error', 90 | { 91 | devDependencies: true, 92 | optionalDependencies: true, 93 | peerDependencies: true 94 | } 95 | ], 96 | 'comma-dangle': [ 97 | 2, 98 | { 99 | arrays: 'never', 100 | objects: 'never', 101 | imports: 'never', 102 | exports: 'never', 103 | functions: 'never' 104 | } 105 | ], 106 | 'max-len': [ 107 | 'error', 108 | { 109 | code: 80, 110 | tabWidth: 2, 111 | ignoreUrls: true, 112 | ignoreComments: true, 113 | ignoreRegExpLiterals: true, 114 | ignoreStrings: true, 115 | ignoreTemplateLiterals: true 116 | } 117 | ], 118 | 'react/sort-comp': [ 119 | 2, 120 | { 121 | order: [ 122 | 'propTypes', 123 | 'props', 124 | 'state', 125 | 'defaultProps', 126 | 'contextTypes', 127 | 'childContextTypes', 128 | 'getChildContext', 129 | 'static-methods', 130 | 'lifecycle', 131 | 'everything-else', 132 | 'render' 133 | ] 134 | } 135 | ], 136 | 'linebreak-style': 0 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | buildClient 4 | buildServer -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | notifications: 5 | email: false 6 | cache: yarn 7 | script: 8 | - node_modules/.bin/travis-github-status lint -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 James Gillmore 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Edit Redux-First Router Demo 3 | 4 | 5 | 6 | # Universal Demonstration of [Redux-First Router](https://github.com/faceyspacey/redux-first-router) 7 | 8 | This demo specializes in SSR and the sort of things like redirecting and authentication you will do on the server. For the simpler example that's easier to start with, check out the [Redux-First Router Boilerplate](https://github.com/faceyspacey/redux-first-router-boilerplate). 9 | 10 | 11 | 12 | ![redux-first-router-demo screenshot](./screenshot.png) 13 | 14 | ## Installation 15 | 16 | ``` 17 | git clone https://github.com/faceyspacey/redux-first-router-demo 18 | cd redux-first-router-demo 19 | yarn 20 | yarn start 21 | ``` 22 | 23 | 24 | ## Files You Should Look At: 25 | 26 | *universal code:* 27 | - [***src/routesMap.js***](./src/routesMap.js) - *(observe thunks and `onBeforeChange`)* 28 | - [***src/utils.js***](./src/utils.js) - *(check `isAllowed` function)* 29 | 30 | *client code:* 31 | - [***src/configureStore.js***](./src/configureStore.js) - *(nothing new here)* 32 | - [***src/components/Switcher.js***](./src/components/Switcher.js) - *(universal component concept)* 33 | - [***src/components/UniversalComponent.js***](./src/components/UniversalComponent.js) - ***(universal component concept continued...)*** 34 | - [***src/components/Sidebar.js***](./src/components/Sidebar.js) - *(look at the different ways to link + dispatch URL-aware actions)* 35 | - [***src/reducers/index.js***](./src/reducers/index.js) - *(observe simplicity of the `page` reducer. Also be cognizant of non-route action types)* 36 | 37 | 38 | *server code:* 39 | - [***server/index.js***](./server/index.js) - *(built-in ajax API + fake cookie handling)* 40 | - [***server/render.js***](./server/render.js) - *(super simple thanks to [webpack-flush-chunks](https://github.com/faceyspacey/webpack-flush-chunks))* 41 | - [***server/configureStore.js***](./server/configureStore.js) - ***(this is the gem of the repo -- observe how to filter authentication)*** 42 | 43 | ## Notes 44 | I comment throughout the code various things you can try. Look out for comments starting with *"TRY:"* and *"TASK:"*. 45 | 46 | For example, there are simple values like the `jwToken` you can toggle to get access to the restricted *admin* area. That showcases a key feature: ***authentication filtering.*** 47 | 48 | In general, this Demo is all about SSR. It shows how to use the `onBeforeChange` to properly authenticate user's and routes using *JSON Web Tokens*. And of course data-fetching via `thunks` is central to it all. **There's even a real API.** 49 | 50 | Lastly, the [***server/configureStore.js***](./server/configureStore.js) file is the absolute most important file of the demo. It essentially brings your ***routing-aware Redux store*** full circle by bringing it server-side in a dead simple yet flexible manner. It works in combination with [***src/routesMap.js***](./src/routesMap.js). Study those and your redux routing dreams have come true 😀 51 | 52 | > As a bonus, it comes with code-splitting thanks to [react-universal-component](https://github.com/faceyspacey/react-universal-component). This setup makes splitting stupid-easy. In the future, ***routing-aware pre-fetching*** will be added to the mix, so the users never know you're only serving partial parts of your app 🚀 53 | 54 | 55 | ## TO DO 56 | 57 | - auth0-based signup/login that replaces current fake cookie/JWToken setup *(PR welcome)* 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-first-router-demo", 3 | "description": "Universal Redux-First Router Demo", 4 | "version": "1.0.0", 5 | "main": "server/index.js", 6 | "author": "James Gillmore ", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "npm run clean && cross-env NODE_ENV=development babel-watch server/index.js", 10 | "start:prod": "npm run build && npm run serve", 11 | "serve": "cross-env NODE_ENV=production node buildServer/index.js", 12 | "build": "npm run build:client && npm run build:server && npm run build:node", 13 | "build:client": "rimraf buildClient && cross-env NODE_ENV=production webpack --progress -p --config webpack/client.prod.js", 14 | "build:server": "rimraf buildServer && cross-env NODE_ENV=production webpack --progress -p --config webpack/server.prod.js", 15 | "build:node": "cross-env NODE_ENV=production babel server -d buildServer --ignore configureStore,render", 16 | "clean": "rimraf buildClient buildServer", 17 | "precommit": "lint-staged", 18 | "cm": "git-cz", 19 | "lint": "eslint --fix src server webpack", 20 | "format": "prettier --single-quote --semi=false --write '{src,server,webpack}/**/*.js' && npm run lint" 21 | }, 22 | "dependencies": { 23 | "babel-polyfill": "^6.23.0", 24 | "cookie-parser": "^1.4.3", 25 | "express": "^4.15.2", 26 | "fetch-everywhere": "^1.0.5", 27 | "history": "^4.6.3", 28 | "react": "^16.0.0", 29 | "react-dom": "^16.0.0", 30 | "react-redux": "^5.0.5", 31 | "react-universal-component": "^2.5.0", 32 | "redux": "^3.7.0", 33 | "redux-devtools-extension": "^2.13.2", 34 | "redux-first-router": "^1.9.15", 35 | "redux-first-router-link": "^1.1.4", 36 | "reselect": "^3.0.1", 37 | "transition-group": "^0.0.2", 38 | "webpack-flush-chunks": "^1.1.22" 39 | }, 40 | "devDependencies": { 41 | "autodll-webpack-plugin": "^0.3.4", 42 | "babel-cli": "^6.24.0", 43 | "babel-core": "^6.24.0", 44 | "babel-eslint": "^8.0.1", 45 | "babel-loader": "^7.1.1", 46 | "babel-plugin-universal-import": "^1.2.2", 47 | "babel-preset-env": "^1.6.0", 48 | "babel-preset-react": "^6.23.0", 49 | "babel-preset-stage-2": "^6.22.0", 50 | "babel-watch": "^2.0.6", 51 | "bluebird": "^3.5.1", 52 | "commitizen": "^2.9.6", 53 | "cross-env": "^5.0.1", 54 | "css-loader": "^0.28.7", 55 | "cz-conventional-changelog": "^2.0.0", 56 | "eslint": "^4.9.0", 57 | "eslint-config-airbnb": "^16.0.0", 58 | "eslint-plugin-flowtype": "^2.32.1", 59 | "eslint-plugin-import": "^2.2.0", 60 | "eslint-plugin-jsx-a11y": "^6.0.2", 61 | "eslint-plugin-react": "^7.4.0", 62 | "extract-css-chunks-webpack-plugin": "^2.0.15", 63 | "flow-bin": "^0.57.2", 64 | "husky": "^0.14.3", 65 | "jest": "^21.2.1", 66 | "lint-staged": "^4.2.3", 67 | "prettier": "^1.4.4", 68 | "react-hot-loader": "^3.0.0", 69 | "rimraf": "^2.6.1", 70 | "stats-webpack-plugin": "^0.6.1", 71 | "travis-github-status": "^1.4.0", 72 | "webpack": "^3.5.4", 73 | "webpack-dev-middleware": "^1.12.0", 74 | "webpack-hot-middleware": "^2.18.2", 75 | "webpack-hot-server-middleware": "^0.1.0", 76 | "write-file-webpack-plugin": "^4.1.0" 77 | }, 78 | "config": { 79 | "commitizen": { 80 | "path": "./node_modules/cz-conventional-changelog" 81 | } 82 | }, 83 | "lint-staged": { 84 | "linters": { 85 | "*.js": [ 86 | "prettier --single-quote --semi=false --write", 87 | "eslint --fix", 88 | "git add" 89 | ] 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faceyspacey/redux-first-router-demo/5214c1983e6b68022bc9d127eb26403ac6b3e4b1/screenshot.png -------------------------------------------------------------------------------- /server/api.js: -------------------------------------------------------------------------------- 1 | export const findVideos = async (category, jwToken) => { 2 | await fakeDelay(1000) 3 | if (!jwToken) return [] // in a real app, you'd authenticate 4 | 5 | switch (category) { 6 | case 'fp': 7 | return fpVideos 8 | case 'react-redux': 9 | return reactReduxVideos 10 | case 'db-graphql': 11 | return dbGraphqlVideos 12 | default: 13 | return [] 14 | } 15 | } 16 | 17 | export const findVideo = async (slug, jwToken) => { 18 | await fakeDelay(500) 19 | if (!jwToken) return null // TRY: set the cookie === '' 20 | 21 | return allVideos.find(video => video.slug === slug) 22 | } 23 | 24 | const fakeDelay = (ms = 1000) => new Promise(res => setTimeout(res, ms)) 25 | 26 | const fpVideos = [ 27 | { 28 | youtubeId: '6mTbuzafcII', 29 | slug: 'transducers', 30 | title: 'Transducers', 31 | by: 'Rich Hickey', 32 | category: 'Functional Programming', 33 | color: 'blue', 34 | tip: `Redux-First Router does not require you to embed actual links into the page 35 | to get the benefit of a synced address bar. Regular actions if matched 36 | will change the URL. That makes it easy to apply to an existing SPA Redux 37 | lacking in routing/URLs!` 38 | }, 39 | { 40 | youtubeId: 'zBHB9i8e3Kc', 41 | slug: 'introduction-to-elm', 42 | title: 'Introduction to Elm', 43 | by: 'Richard Feldman', 44 | category: 'Functional Programming', 45 | color: 'blue', 46 | tip: `Redux reducers programmatically allow you to produce any state you need. 47 | So logically Route Matching components such as in React Reacter only 48 | allow you to do LESS, but with a MORE complicated API.` 49 | }, 50 | { 51 | youtubeId: 'mty0RwkPmE8', 52 | slug: 'next-five-years-of-clojurescript', 53 | title: 'The Next Five Years of ClojureScript ', 54 | by: 'David Nolen', 55 | category: 'Functional Programming', 56 | color: 'blue', 57 | tip: `In your actions.meta.location key passed to your reducers you have all sorts 58 | of information: the previous route, its type and payload, history, whether 59 | the browser back/next buttons were used and if the action was dispatched on load. 60 | Check the "kind" key.` 61 | } 62 | ] 63 | 64 | const reactReduxVideos = [ 65 | { 66 | youtubeId: 'qa72q70gAb4', 67 | slug: 'unraveling-navigation-in-react-native', 68 | title: 'Unraveling Navigation in React Native', 69 | by: 'Adam Miskiewicz', 70 | category: 'React & Redux', 71 | color: 'red', 72 | tip: `Redux-First Router tries in all cases to mirror the Redux API. There is no need 73 | to pass your thunk :params such as in an express request or the like. Just grab it 74 | from the payload stored in the location state.` 75 | }, 76 | { 77 | youtubeId: 'zD_judE-bXk', 78 | slug: 'recomposing-your-react-application', 79 | title: 'Recomposing your React application at react-europe ', 80 | by: 'Andrew Clark', 81 | category: 'React & Redux', 82 | color: 'red', 83 | tip: `Redux-First Router requires your payload to be objects, as its keys are directionally extracted 84 | and from your URLs and passed from payloads to URL path segments. Your free 85 | to use whatever payload you like for redux actions not connected to your routes. Not all 86 | actions need to be connected to routes.` 87 | }, 88 | { 89 | youtubeId: 'uvAXVMwHJXU', 90 | slug: 'the-redux-journey', 91 | title: 'The Redux Journey', 92 | by: 'Dan Abramov', 93 | category: 'React & Redux', 94 | color: 'red', 95 | tip: `The component embeds paths in hrefs for SEO, but you don't need to use it 96 | to get the benefits of a changing address bar. Actions that match routes will 97 | trigger the corresponding URL even if you dispatch them directly.` 98 | } 99 | ] 100 | 101 | const dbGraphqlVideos = [ 102 | { 103 | youtubeId: 'fU9hR3kiOK0', 104 | slug: 'turning-the-db-inside-out', 105 | title: 'Turning the database inside out', 106 | by: 'Martin Kleppmann', 107 | category: 'Database & GraphQL', 108 | color: 'orange', 109 | tip: `The 'thunk' feature is optional, but very useful. Using our 'thunk' feature allows you 110 | to define it in one place while linking to the route from many places without 111 | worrying about getting the data first. It's also very easy to handle server-side.` 112 | }, 113 | { 114 | youtubeId: '_5VShOmnfQ0', 115 | slug: 'normalized-caching-in-apollo-ios', 116 | title: 'Normalized Caching in Apollo iOS', 117 | by: 'Martijn Walraven', 118 | category: 'Database & GraphQL', 119 | color: 'orange', 120 | tip: `Structure your reducers so that less actions are used to trigger the same state. 121 | Your actions will become more 'page-like'. As a result your reducers 122 | will need to do more "tear down" work when leaving corresponding pages. It's also 123 | recommended to set action types as the capitalized noun name of the page.` 124 | }, 125 | { 126 | youtubeId: 'm-hre1tt9C4', 127 | slug: 'first-thoughts-on-apollo-and-graphql', 128 | title: 'First Thoughts On Apollo and GraphQL', 129 | by: 'Sacha Greif', 130 | category: 'Database & GraphQL', 131 | color: 'orange', 132 | tip: `Using a hash of slugs within one of your reducers is the recommended approach to 133 | maintain a normalized set of entities to get the benefits of SEO. This is as opposed 134 | to using IDs. Refrain from using normalizr or Apollo until your app justifies it.` 135 | } 136 | ] 137 | 138 | const allVideos = reactReduxVideos.concat(dbGraphqlVideos, fpVideos) 139 | -------------------------------------------------------------------------------- /server/configureStore.js: -------------------------------------------------------------------------------- 1 | import createHistory from 'history/createMemoryHistory' 2 | import { NOT_FOUND } from 'redux-first-router' 3 | import configureStore from '../src/configureStore' 4 | 5 | export default async (req, res) => { 6 | const jwToken = req.cookies.jwToken // see server/index.js to change jwToken 7 | const preLoadedState = { jwToken } // onBeforeChange will authenticate using this 8 | 9 | const history = createHistory({ initialEntries: [req.path] }) 10 | const { store, thunk } = configureStore(history, preLoadedState) 11 | 12 | // if not using onBeforeChange + jwTokens, you can also async authenticate 13 | // here against your db (i.e. using req.cookies.sessionId) 14 | 15 | let location = store.getState().location 16 | if (doesRedirect(location, res)) return false 17 | 18 | // using redux-thunk perhaps request and dispatch some app-wide state as well, e.g: 19 | // await Promise.all([store.dispatch(myThunkA), store.dispatch(myThunkB)]) 20 | 21 | await thunk(store) // THE PAYOFF BABY! 22 | 23 | location = store.getState().location // remember: state has now changed 24 | if (doesRedirect(location, res)) return false // only do this again if ur thunks have redirects 25 | 26 | const status = location.type === NOT_FOUND ? 404 : 200 27 | res.status(status) 28 | return store 29 | } 30 | 31 | const doesRedirect = ({ kind, pathname }, res) => { 32 | if (kind === 'redirect') { 33 | res.redirect(302, pathname) 34 | return true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import express from 'express' 3 | import cookieParser from 'cookie-parser' 4 | import webpack from 'webpack' 5 | import webpackDevMiddleware from 'webpack-dev-middleware' 6 | import webpackHotMiddleware from 'webpack-hot-middleware' 7 | import webpackHotServerMiddleware from 'webpack-hot-server-middleware' 8 | import clientConfig from '../webpack/client.dev' 9 | import serverConfig from '../webpack/server.dev' 10 | import { findVideos, findVideo } from './api' 11 | 12 | const DEV = process.env.NODE_ENV === 'development' 13 | const publicPath = clientConfig.output.publicPath 14 | const outputPath = clientConfig.output.path 15 | const app = express() 16 | 17 | // JWTOKEN COOKIE - in a real app obviously you set this after signup/login: 18 | 19 | app.use(cookieParser()) 20 | 21 | app.use((req, res, next) => { 22 | const cookie = req.cookies.jwToken 23 | const jwToken = 'fake' // TRY: set to 'real' to authenticate ADMIN route 24 | 25 | if (cookie !== jwToken) { 26 | res.cookie('jwToken', jwToken, { maxAge: 900000 }) 27 | req.cookies.jwToken = jwToken 28 | } 29 | 30 | next() 31 | }) 32 | 33 | // API 34 | 35 | app.get('/api/videos/:category', async (req, res) => { 36 | const jwToken = req.headers.authorization.split(' ')[1] 37 | const data = await findVideos(req.params.category, jwToken) 38 | res.json(data) 39 | }) 40 | 41 | app.get('/api/video/:slug', async (req, res) => { 42 | const jwToken = req.headers.authorization.split(' ')[1] 43 | const data = await findVideo(req.params.slug, jwToken) 44 | res.json(data) 45 | }) 46 | 47 | // UNIVERSAL HMR + STATS HANDLING GOODNESS: 48 | 49 | if (DEV) { 50 | const multiCompiler = webpack([clientConfig, serverConfig]) 51 | const clientCompiler = multiCompiler.compilers[0] 52 | 53 | app.use(webpackDevMiddleware(multiCompiler, { publicPath, stats: { colors: true } })) 54 | app.use(webpackHotMiddleware(clientCompiler)) 55 | app.use( 56 | // keeps serverRender updated with arg: { clientStats, outputPath } 57 | webpackHotServerMiddleware(multiCompiler, { 58 | serverRendererOptions: { outputPath } 59 | }) 60 | ) 61 | } 62 | else { 63 | const clientStats = require('../buildClient/stats.json') // eslint-disable-line import/no-unresolved 64 | const serverRender = require('../buildServer/main.js').default // eslint-disable-line import/no-unresolved 65 | 66 | app.use(publicPath, express.static(outputPath)) 67 | app.use(serverRender({ clientStats, outputPath })) 68 | } 69 | 70 | app.listen(3000, () => { 71 | console.log('Listening @ http://localhost:3000/') 72 | }) 73 | -------------------------------------------------------------------------------- /server/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/server' 3 | import { Provider } from 'react-redux' 4 | import { flushChunkNames } from 'react-universal-component/server' 5 | import flushChunks from 'webpack-flush-chunks' 6 | import configureStore from './configureStore' 7 | import App from '../src/components/App' 8 | 9 | export default ({ clientStats }) => async (req, res, next) => { 10 | const store = await configureStore(req, res) 11 | if (!store) return // no store means redirect was already served 12 | 13 | const app = createApp(App, store) 14 | const appString = ReactDOM.renderToString(app) 15 | const stateJson = JSON.stringify(store.getState()) 16 | const chunkNames = flushChunkNames() 17 | const { js, styles, cssHash } = flushChunks(clientStats, { chunkNames }) 18 | 19 | console.log('REQUESTED PATH:', req.path) 20 | console.log('CHUNK NAMES', chunkNames) 21 | 22 | return res.send( 23 | ` 24 | 25 | 26 | 27 | redux-first-router-demo 28 | ${styles} 29 | 30 | 31 | 32 | 33 |
${appString}
34 | ${cssHash} 35 | 36 | ${js} 37 | 38 | ` 39 | ) 40 | } 41 | 42 | const createApp = (App, store) => 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import { NOT_FOUND } from 'redux-first-router' 2 | 3 | // try dispatching these from the redux devTools 4 | 5 | export const goToPage = (type, category) => ({ 6 | type, 7 | payload: category && { category } 8 | }) 9 | 10 | export const goHome = () => ({ 11 | type: 'HOME' 12 | }) 13 | 14 | export const goToAdmin = () => ({ 15 | type: 'ADMIN' 16 | }) 17 | 18 | export const notFound = () => ({ 19 | type: NOT_FOUND 20 | }) 21 | 22 | export const visitCategory = category => ({ 23 | type: 'LIST', 24 | payload: { category } 25 | }) 26 | 27 | export const visitVideo = slug => ({ 28 | type: 'VIDEO', 29 | payload: { slug } 30 | }) 31 | -------------------------------------------------------------------------------- /src/components/Admin.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { admin } from '../css/Switcher' 3 | 4 | export default () =>
U FIGURED OUT HOW TO DO AUTH!
5 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import DevTools from './DevTools' 4 | import Sidebar from './Sidebar' 5 | import Switcher from './Switcher' 6 | 7 | import styles from '../css/App' 8 | 9 | export default () => 10 |
11 |
12 | 13 | 14 |
15 | 16 | 17 |
18 | -------------------------------------------------------------------------------- /src/components/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import styles from '../css/DevTools' 5 | 6 | const DevTools = () => 7 |
8 |
9 | ACTIONS 10 | DEV-TOOLS 11 | STATE 12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 | 20 | const ActionsBoxComponent = ({ actions }) => 21 |
22 |
{JSON.stringify(actions, null, 1)}
23 |
24 | 25 | const ActionsBox = connect(({ actions }) => ({ actions }))(ActionsBoxComponent) 26 | 27 | const StateBoxComponent = state => 28 |
29 |
{JSON.stringify(state, null, 1)}
30 |
31 | 32 | const StateBox = connect(state => ({ ...state, actions: undefined }))( 33 | StateBoxComponent 34 | ) 35 | 36 | export default DevTools 37 | -------------------------------------------------------------------------------- /src/components/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { notFound } from '../css/Switcher' 3 | 4 | export default error => 5 |
6 | ERROR: {error.message} 7 |
8 | -------------------------------------------------------------------------------- /src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from '../css/Home' 3 | 4 | const Home = () => 5 |
6 |

HOME

7 | 8 |

9 | NOTE: The top set of links are real links made like this: 10 |

11 | 12 | 13 | HREF STRING: 14 | 15 | {"DB & GRAPHQL"} 16 | 17 | 18 | PATH SEGMENTS: 19 | 20 | {"REACT & REDUX"} 21 | 22 | ACTION: 23 | 24 | {"FP"} 25 | 26 | 27 |

EVENT HANDLERS DISPATCH ACTION

28 | 29 |
 30 |       {`
 31 | onClick: () => dispatch({
 32 |   type: 'LIST',
 33 |   payload: { category: 'react-redux' }
 34 | })
 35 |       `}
 36 |     
37 | 38 |
39 | DIRECTIONS: 40 | 41 | {`inspect the sidebar tabs to see the top set are real tag links and the 42 | bottom set not, yet the address bar changes for both. The decision is up to you. 43 | When using the component, if you provide an action as the \`href\` prop, you never 44 | need to worry if you change the static path segments (e.g: \`/list\`) in the routes passed 45 | to \`connectRoutes\`. ALSO: DON'T FORGET TO USE THE BROWSER BACK/NEXT BUTTONS TO SEE THAT WORKING TOO!`} 46 | 47 |
48 | 49 |

LINKS ABOUT REDUX-FIRST ROUTER:

50 | 51 | {'> '} 52 |
58 | Server-Render Like A Pro in 10 Steps /w Redux-First Router 🚀 59 | 60 | 61 |
62 |
63 | 64 | {'> '} 65 | 71 | Things To Pay Attention To In This Demo 72 | 73 | 74 |
75 |
76 | 77 | {'> '} 78 | 84 | Pre Release: Redux-First Router — A Step Beyond Redux-Little-Router 85 | 86 | 87 |
88 |
89 | 90 | {'> '} 91 | 97 | Redux-First Router data-fetching: solving the 80% use case for async 98 | Middleware 99 | 100 |
101 | 102 | export default Home 103 | -------------------------------------------------------------------------------- /src/components/List.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import Link from 'redux-first-router-link' 4 | 5 | import styles from '../css/List' 6 | 7 | const List = ({ videos }) => 8 |
9 | {videos.map((video, key) => )} 10 |
11 | 12 | const Row = ({ slug, title, youtubeId, by, color }) => 13 | 18 |
19 | {initials(by)} 20 |
21 | {title} 22 | 23 |
24 | by: {by} 25 | 26 | 27 | const youtubeBackground = youtubeId => 28 | `url(https://img.youtube.com/vi/${youtubeId}/maxresdefault.jpg)` 29 | 30 | const initials = by => by.split(' ').map(name => name[0]).join('') 31 | 32 | const mapState = ({ category, videosByCategory, videosHash }) => { 33 | const slugs = videosByCategory[category] || [] 34 | const videos = slugs.map(slug => videosHash[slug]) 35 | return { videos } 36 | } 37 | export default connect(mapState)(List) 38 | -------------------------------------------------------------------------------- /src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { spinner } from '../css/Switcher' 3 | 4 | export default () =>
5 | -------------------------------------------------------------------------------- /src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { login } from '../css/Switcher' 3 | 4 | export default () =>
YOU ARE NOT ALLOWED IN!
5 | -------------------------------------------------------------------------------- /src/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { notFound } from '../css/Switcher' 3 | 4 | export default () =>
PAGE NOT FOUND - 404
5 | -------------------------------------------------------------------------------- /src/components/Player.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import Link from 'redux-first-router-link' 4 | 5 | import styles from '../css/Video' 6 | 7 | const Player = ({ playing, youtubeId, slug, color }) => 8 | !playing 9 | ?
13 | 14 | 15 | 16 |
17 | :