From 641ce78ef9afeb9ffddc35d2abe12b760ee365f3 Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Tue, 29 Mar 2016 23:16:06 -0400 Subject: [PATCH 01/15] refactor(app): Fractal Project Structure --- .travis.yml | 2 +- CHANGELOG.md | 15 +++ README.md | 104 ++++++++++++----- build/webpack.config.js | 28 +++-- config/_base.js | 1 - package.json | 109 +++++++++++------- src/components/Counter.js | 37 ++++++ .../HomeView.scss => components/Counter.scss} | 6 +- src/components/Header.js | 18 +++ src/components/Header.scss | 4 + src/components/README.md | 62 ++++++++++ src/containers/DevTools.js | 12 -- src/containers/DevToolsWindow.js | 7 -- src/containers/Root.js | 45 -------- src/layouts/CoreLayout/CoreLayout.js | 30 ++--- src/layouts/CoreLayout/CoreLayout.scss | 3 + src/layouts/CoreLayout/index.js | 1 + src/main.js | 56 ++++++--- src/redux/configureStore.js | 27 ----- src/redux/rootReducer.js | 8 -- src/redux/utils/createDevToolsWindow.js | 30 ----- src/routes/Counter/components/CounterView.js | 24 ++++ .../Counter/containers/CounterContainer.js | 38 ++++++ src/routes/Counter/index.js | 28 +++++ .../Counter}/modules/counter.js | 0 .../HomeView => routes/Home/assets}/Duck.jpg | Bin src/routes/Home/components/HomeView.js | 12 ++ src/routes/Home/components/HomeView.scss | 5 + src/routes/Home/index.js | 5 + src/routes/NotFound/NotFound.js | 18 +++ src/routes/NotFound/index.js | 6 + src/routes/Root.js | 32 +++++ src/routes/index.js | 45 +++++--- src/store/createStore.js | 33 ++++++ src/store/reducers.js | 13 +++ src/views/HomeView/HomeView.js | 65 ----------- src/views/HomeView/index.js | 2 - tests/components/Counter.spec.js | 80 +++++++++++++ tests/components/Header.spec.js | 30 +++++ .../Counter/components/CounterView.spec.js | 0 tests/routes/Counter/index.spec.js | 18 +++ .../Counter}/modules/counter.spec.js | 42 +++---- tests/routes/Home/components/HomeView.spec.js | 24 ++++ tests/routes/Home/index.spec.js | 0 tests/views/HomeView.spec.js | 109 ------------------ 45 files changed, 769 insertions(+), 465 deletions(-) create mode 100644 src/components/Counter.js rename src/{views/HomeView/HomeView.scss => components/Counter.scss} (62%) create mode 100644 src/components/Header.js create mode 100644 src/components/Header.scss create mode 100644 src/components/README.md delete mode 100644 src/containers/DevTools.js delete mode 100644 src/containers/DevToolsWindow.js delete mode 100644 src/containers/Root.js create mode 100644 src/layouts/CoreLayout/CoreLayout.scss delete mode 100644 src/redux/configureStore.js delete mode 100644 src/redux/rootReducer.js delete mode 100644 src/redux/utils/createDevToolsWindow.js create mode 100644 src/routes/Counter/components/CounterView.js create mode 100644 src/routes/Counter/containers/CounterContainer.js create mode 100644 src/routes/Counter/index.js rename src/{redux => routes/Counter}/modules/counter.js (100%) rename src/{views/HomeView => routes/Home/assets}/Duck.jpg (100%) create mode 100644 src/routes/Home/components/HomeView.js create mode 100644 src/routes/Home/components/HomeView.scss create mode 100644 src/routes/Home/index.js create mode 100644 src/routes/NotFound/NotFound.js create mode 100644 src/routes/NotFound/index.js create mode 100644 src/routes/Root.js create mode 100644 src/store/createStore.js create mode 100644 src/store/reducers.js delete mode 100644 src/views/HomeView/HomeView.js delete mode 100644 src/views/HomeView/index.js create mode 100644 tests/components/Counter.spec.js create mode 100644 tests/components/Header.spec.js create mode 100644 tests/routes/Counter/components/CounterView.spec.js create mode 100644 tests/routes/Counter/index.spec.js rename tests/{redux => routes/Counter}/modules/counter.spec.js (78%) create mode 100644 tests/routes/Home/components/HomeView.spec.js create mode 100644 tests/routes/Home/index.spec.js delete mode 100644 tests/views/HomeView.spec.js diff --git a/.travis.yml b/.travis.yml index 64b05f9db..738cf4ef3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,4 +19,4 @@ script: - NODE_ENV=production npm run deploy after_success: - - npm run codecov \ No newline at end of file + - npm run codecov diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c86faf2..75c8d1cbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ Changelog ========= +2.0.0-alpha.3 +------------- + +### Features +* Upgraded `flow-interfaces` to `^0.6.0` + +### Improvements +* Moved dependencies needed for production builds from devDependencies to regular dependencies + +### Fixes +* Production configuration now generates assets with absolute rather than relative paths + +### Deprecations +* Removed `eslint-loader` for performance reasons + 2.0.0-alpha.2 ------------- diff --git a/README.md b/README.md index f9c4bdff8..33e270db5 100644 --- a/README.md +++ b/README.md @@ -38,14 +38,16 @@ Requirements Features -------- -* [React](https://github.com/facebook/react) (`^0.14.0`) +* [React](https://github.com/facebook/react) (`^0.15.0-rc2`) * [Redux](https://github.com/rackt/redux) (`^3.0.0`) * react-redux (`^4.0.0`) - * redux-devtools * redux-thunk middleware * [react-router](https://github.com/rackt/react-router) (`^2.0.0`) + * Asynchronous routes configured with dependencies and reducers * [react-router-redux](https://github.com/rackt/react-router-redux) (`^4.0.0`) * [Webpack](https://github.com/webpack/webpack) + * Vanilla HMR using `module.hot` and `webpack-dev-middleware` + * Code-splitting using `react-router` route configuration * Bundle splitting and CSS extraction * Sass w/ CSS modules, autoprefixer, and minification * [Koa](https://github.com/koajs/koa) (`^2.0.0-alpha`) @@ -55,8 +57,6 @@ Features * Code coverage reports/instrumentation with [isparta](https://github.com/deepsweet/isparta-loader) * [Flow](http://flowtype.org/) (`^0.22.0`) * [Babel](https://github.com/babel/babel) (`^6.3.0`) - * [react-transform-hmr](https://github.com/gaearon/react-transform-hmr) hot reloading for React components - * [redbox-react](https://github.com/KeywordBrain/redbox-react) visible error reporting for React components * [babel-plugin-transform-runtime](https://www.npmjs.com/package/babel-plugin-transform-runtime) so transforms aren't inlined * [babel-plugin-transform-react-constant-elements](https://babeljs.io/docs/plugins/transform-react-constant-elements/) save some memory allocation * [babel-plugin-transform-react-remove-prop-types](https://github.com/oliviertassinari/babel-plugin-transform-react-remove-prop-types) remove `PropTypes` @@ -75,6 +75,18 @@ $ npm install # Install Node modules listed in ./package.json $ npm start # Compile and launch ``` +### Redux DevTools + +Redux DevTools components have been removed from this project in favor of the [Redux DevTools Chrome Extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd), which runs on a separate thread and provides much better performance and functionality. It provides access to the most popular monitors, is easy to configure to filter actions, and doesn’t require installing any packages. + +**We strongly recommend using the chrome extension.** However, adding the DevTools components to your project is simple, first grab the packages from npm: + +``` +npm i --D redux-devtools redux-devtools-log-monitor redux-devtools-dock-monitor +``` + +Then follow the [manual integration walkthrough](https://github.com/gaearon/redux-devtools/blob/master/docs/Walkthrough.md). + ### Starting a New Project First, I highly suggest checking out a new project by @@ -97,6 +109,7 @@ Great, you now have a fresh project! There are a few titles you'll probably want * `~/package.json` - package name * `~/src/index.html` - template title tag +* `~/src/main.js` - Helmet document title Usage ----- @@ -111,19 +124,21 @@ Before delving into the descriptions of each available npm script, here's a brie Great, now that introductions have been made here's everything in full detail: -|Script|Description| +|`npm run...`|Description| |---|---| -|`npm start`|Spins up Koa server to serve your app at `localhost:3000`. HMR will be enabled in development.| -|`npm run compile`|Compiles the application to disk (`~/dist` by default).| -|`npm run dev`|Same as `npm start`, but enables nodemon to automatically restart the server when server-related code is changed.| -|`npm run dev:nw`|Same as `npm run dev`, but opens the redux devtools in a new window.| -|`npm run dev:no-debug`|Same as `npm run dev` but disables redux devtools.| -|`npm run test`|Runs unit tests with Karma and generates a coverage report.| -|`npm run test:dev`|Runs Karma and watches for changes to re-run tests; does not generate coverage reports.| -|`npm run deploy`|Runs linter, tests, and then, on success, compiles your application to disk.| -|`npm run flow:check`|Analyzes the project for type errors.| -|`npm run lint`|Lint all `.js` files.| -|`npm run lint:fix`|Lint and fix all `.js` files. [Read more on this](http://eslint.org/docs/user-guide/command-line-interface.html#fix).| +|`start`|Spins up Koa server to serve your app at `localhost:3000`. HMR will be enabled in development.| +|`compile`|Compiles the application to disk (`~/dist` by default).| +|`dev`|Same as `npm start`, but enables nodemon to automatically restart the server when server-related code is changed.| +|`dev:nw`|Same as `npm run dev`, but opens the redux devtools in a new window.| +|`dev:no-debug`|Same as `npm run dev` but disables redux devtools.| +|`test`|Runs unit tests with Karma and generates a coverage report.| +|`test:dev`|Runs Karma and watches for changes to re-run tests; does not generate coverage reports.| +|`deploy`|Runs linter, tests, and then, on success, compiles your application to disk.| +|`deploy:dev`|Same as `deploy` but overrides `NODE_ENV` to "development".| +|`deploy:prod`|Same as `deploy` but overrides `NODE_ENV` to "production".| +|`flow:check`|Analyzes the project for type errors.| +|`lint`|Lint all `.js` files.| +|`lint:fix`|Lint and fix all `.js` files. [Read more on this](http://eslint.org/docs/user-guide/command-line-interface.html#fix).| **NOTE:** Deploying to a specific environment? Make sure to specify your target `NODE_ENV` so webpack will use the correct configuration. For example: `NODE_ENV=production npm run compile` will compile your application with `~/build/webpack/_production.js`. @@ -170,7 +185,7 @@ make sure to copy over the `blueprints` folder in this project for starter-kit s Structure --------- -The folder structure provided is only meant to serve as a guide, it is by no means prescriptive. It is something that has worked very well for me and my team, but use only what makes sense to you. +The folder structure provided is only meant to serve as a guide, it is by no means prescriptive. The current fractal hierarchy was inspired by [an old angular RFC](https://docs.google.com/document/u/1/d/1XXMvReO8-Awi1EZXAXS4PzDzdNvV6pGcuaF4Q9821Es/pub) and contributed by [Justin Greenberg](https://github.com/justingreenberg). ``` . @@ -183,25 +198,56 @@ The folder structure provided is only meant to serve as a guide, it is by no mea ├── server # Koa application (uses webpack middleware) │ └── main.js # Server application entry point ├── src # Application source code -│ ├── components # Generic React Components (generally Dumb components) -│ ├── containers # Components that provide context (e.g. Redux Provider) +│ ├── components # App-wide Presentational React Components +│ ├── store # Redux-specific pieces +│ │   ├── createStore.js # Create and instrument redux store +│ │   └── reducers.js # Reducer registry and injection │ ├── layouts # Components that dictate major page structure -│ ├── redux # Redux-specific pieces -│ │ ├── modules # Collections of reducers/constants/actions -│ │ └── utils # Redux-specific helpers -│ ├── routes # Application route definitions +│ ├── routes # Main route definitions and async split points +│ │   ├── index.js # Bootstrap main application routes with store +│ │   ├── Home * # Fractal (All Route-specific, as needed) +│ │   │   ├── index.js * # Route definitions and async split points +│ │   │   ├── assets # Assets required to render components +│ │   │   ├── components # Presentational React Components +│ │   │   ├── containers # Connect components to actions and store +│ │   │   ├── modules # Collections of reducers/constants/actions +│ │   │   └── routes # Sub-route definitions and async split points +│ │   └── NotFound # Capture unknown routes in component │ ├── static # Static assets (not imported anywhere in source code) │ ├── styles # Application-wide styles (generally settings) -│ ├── views # Components that live at a route │ └── main.js # Application bootstrap and rendering └── tests # Unit tests ``` -### Components vs. Views vs. Layouts +### Fractal Structure (or, Recursive Route Hierarchy) -**TL;DR:** They're all components. +Small applications can be built using a flat directory structure, with folders for `components`, `containers`, etc. However, this does not scale and can seriously affect production velocity as your project grows. By starting with a fractal structure, you are forced to think about your architecture more strategically from day one. -This distinction may not be important for you, but as an explanation: A **Layout** is something that describes an entire page structure, such as a fixed navigation, viewport, sidebar, and footer. Most applications will probably only have one layout, but keeping these components separate makes their intent clear. **Views** are components that live at routes, and are generally rendered within a **Layout**. What this ends up meaning is that, with this structure, nearly everything inside of **Components** ends up being a dumb component. +We use `react-router` [route definitions](https://github.com/reactjs/react-router/blob/master/docs/API.md#plainroute) (`/index.js`) to define units of logic within our application. + +This provides many benefits which may not immediately be obvious: +- Routes can be be bundled into "chunks" using webpack's [code splitting](https://webpack.github.io/docs/code-splitting.html) and merging algorithm. This means that the entire dependency tree for each route can be omitted from the initial bundle and then loaded *on demand*, only when it is required. +- Since logic is self-contained, routes can easily be broken into separate repositories and referenced with webpack's [DLL plugin)[https://github.com/webpack/docs/wiki/list-of-plugins#dllplugin] for flexible, high-performance development and scalability. + +#### Layouts +- Regular stateless components that dictate major page structure +- Useful for populating structure with named routes + +#### Components +- Components should be stateless and purely presentational +- Prefer functional components ie. `const Cool = ({ who }) =>
${who} is cool
` +- The top-level `components` directory should be thought of as a global common palette + +#### Containers +- Containers **only** `connect` presentational components to actions/state +- One or many container components can be composed in a stateless function component +- Rule of thumb: **no JSX in containers**! + +#### Routes +- A route directory + - *Must* contain an `index.js` that returns route definition + - *May* contain assets, components, containers, modules, and additional child Routes as needed +- Child routes follow the same structure recursively Webpack ------- @@ -295,6 +341,10 @@ Have more questions? Feel free to submit an issue or join the Gitter chat! Troubleshooting --------------- +### Redux DevTools + +We recommend using + ### `npm run dev:nw` produces `cannot read location of undefined.` This is most likely because the new window has been blocked by your popup blocker, so make sure it's disabled before trying again. diff --git a/build/webpack.config.js b/build/webpack.config.js index f669860cc..9d25388a6 100644 --- a/build/webpack.config.js +++ b/build/webpack.config.js @@ -89,6 +89,17 @@ if (!__TEST__) { // ------------------------------------ // Pre-Loaders // ------------------------------------ +/* +[ NOTE ] +We no longer use eslint-loader due to it severely impacting build +times for larger projects. `npm run lint` still exists to aid in +deploy processes (such as with CI), and it's recommended that you +use a linting plugin for your IDE in place of this loader. + +If you do wish to continue using the loader, you can uncomment +the code below and run `npm i --save-dev eslint-loader`. This code +will be removed in a future release. + webpackConfig.module.preLoaders = [{ test: /\.(js|jsx)$/, loader: 'eslint', @@ -99,6 +110,7 @@ webpackConfig.eslint = { configFile: paths.base('.eslintrc'), emitWarning: __DEV__ } +*/ // ------------------------------------ // Loaders @@ -113,20 +125,6 @@ webpackConfig.module.loaders = [{ plugins: ['transform-runtime'], presets: ['es2015', 'react', 'stage-0'], env: { - development: { - plugins: [ - ['react-transform', { - transforms: [{ - transform: 'react-transform-hmr', - imports: ['react'], - locals: ['module'] - }, { - transform: 'react-transform-catch-errors', - imports: ['react', 'redbox-react'] - }] - }] - ] - }, production: { plugins: [ 'transform-react-remove-prop-types', @@ -268,7 +266,7 @@ if (!__DEV__) { ).forEach((loader) => { const [first, ...rest] = loader.loaders loader.loader = ExtractTextPlugin.extract(first, rest.join('!')) - delete loader.loaders + Reflect.deleteProperty(loader, 'loaders') }) webpackConfig.plugins.push( diff --git a/config/_base.js b/config/_base.js index f1c3919b1..22f390c1a 100644 --- a/config/_base.js +++ b/config/_base.js @@ -77,7 +77,6 @@ config.globals = { '__PROD__' : config.env === 'production', '__TEST__' : config.env === 'test', '__DEBUG__' : config.env === 'development' && !argv.no_debug, - '__DEBUG_NEW_WINDOW__' : !!argv.nw, '__BASENAME__' : JSON.stringify(process.env.BASENAME || '') } diff --git a/package.json b/package.json index 8abb644be..9928a8cca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-redux-starter-kit", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.3", "description": "Get started with React, Redux, and React-Router!", "main": "index.js", "engines": { @@ -19,6 +19,8 @@ "test": "better-npm-run test", "test:dev": "npm run test -- --watch", "deploy": "better-npm-run deploy", + "deploy:dev": "better-npm-run deploy:dev", + "deploy:prod": "better-npm-run deploy:prod", "flow:check": "babel-node bin/flow-check", "codecov": "cat coverage/*/lcov.info | codecov" }, @@ -38,6 +40,33 @@ }, "deploy": { "command": "npm run clean && npm run compile", + "env": { + "DEBUG": "app:*" + } + }, + "deploy:dev": { + "command": "npm run deploy", + "env": { + "NODE_ENV": "development", + "DEBUG": "app:*" + } + }, + "deploy:prod": { + "command": "npm run deploy", + "env": { + "NODE_ENV": "production", + "DEBUG": "app:*" + } + }, + "deploy:dev": { + "command": "npm run deploy", + "env": { + "NODE_ENV": "development", + "DEBUG": "app:*" + } + }, + "deploy:prod": { + "command": "npm run deploy", "env": { "NODE_ENV": "production", "DEBUG": "app:*" @@ -64,62 +93,71 @@ "author": "David Zukowski (http://zuko.me)", "license": "MIT", "dependencies": { + "babel-cli": "^6.5.1", + "babel-core": "^6.3.17", + "babel-loader": "^6.2.0", + "babel-plugin-react-transform": "^2.0.0", + "babel-plugin-transform-react-constant-elements": "^6.5.0", + "babel-plugin-transform-react-remove-prop-types": "^0.2.2", + "babel-plugin-transform-runtime": "^6.3.13", + "babel-preset-es2015": "^6.3.13", + "babel-preset-react": "^6.3.13", + "babel-preset-stage-0": "^6.3.13", + "babel-register": "^6.3.13", + "babel-runtime": "^6.3.19", + "better-npm-run": "0.0.8", + "css-loader": "^0.23.0", + "cssnano": "^3.3.2", "debug": "^2.2.0", + "extract-text-webpack-plugin": "^1.0.0", + "file-loader": "^0.8.4", + "fs-extra": "^0.26.3", "history": "^2.0.0", + "html-webpack-plugin": "^2.7.1", + "imports-loader": "^0.6.5", + "json-loader": "^0.5.4", "koa": "^2.0.0-alpha.3", "koa-connect-history-api-fallback": "^0.3.0", "koa-convert": "^1.2.0", "koa-proxy": "^0.5.0", "koa-static": "^2.0.0", - "react": "^0.14.0", - "react-dom": "^0.14.0", + "node-sass": "^3.3.3", + "postcss-loader": "^0.8.0", + "react": "^15.0.0-rc.2", + "react-dom": "^15.0.0-rc.2", + "react-helmet": "^3.0.0", "react-redux": "^4.0.0", "react-router": "^2.0.0", "react-router-redux": "^4.0.0", "redux": "^3.0.0", - "redux-thunk": "^2.0.0" + "redux-thunk": "^2.0.0", + "reselect": "^2.2.1", + "rimraf": "^2.5.1", + "sass-loader": "^3.0.0", + "style-loader": "^0.13.0", + "url-loader": "^0.5.6", + "webpack": "^1.12.14", + "yargs": "^4.0.0" }, "devDependencies": { - "babel-cli": "^6.5.1", - "babel-core": "^6.3.17", "babel-eslint": "^6.0.0-beta.6", - "babel-loader": "^6.2.0", - "babel-plugin-react-transform": "^2.0.0", - "babel-plugin-transform-react-constant-elements": "^6.5.0", - "babel-plugin-transform-react-remove-prop-types": "^0.2.2", - "babel-plugin-transform-runtime": "^6.3.13", - "babel-preset-es2015": "^6.3.13", - "babel-preset-react": "^6.3.13", - "babel-preset-stage-0": "^6.3.13", - "babel-register": "^6.3.13", - "babel-runtime": "^6.3.19", - "better-npm-run": "0.0.8", "chai": "^3.4.1", "chai-as-promised": "^5.1.0", "chai-enzyme": "^0.4.0", "cheerio": "^0.20.0", "codecov": "^1.0.1", - "css-loader": "^0.23.0", - "cssnano": "^3.3.2", "enzyme": "^2.0.0", "eslint": "^2.4.0", "eslint-config-standard": "^5.1.0", "eslint-config-standard-react": "^2.2.0", - "eslint-loader": "^1.1.1", "eslint-plugin-babel": "^3.0.0", "eslint-plugin-flow-vars": "^0.2.0", "eslint-plugin-promise": "^1.0.8", "eslint-plugin-react": "^4.0.0", "eslint-plugin-standard": "^1.3.1", - "extract-text-webpack-plugin": "^1.0.0", - "file-loader": "^0.8.4", "flow-bin": "0.22.1", - "flow-interfaces": "^0.5.0", - "fs-extra": "^0.26.3", - "html-webpack-plugin": "^2.7.1", - "imports-loader": "^0.6.5", + "flow-interfaces": "^0.6.0", "isparta-loader": "^2.0.0", - "json-loader": "^0.5.4", "karma": "^0.13.21", "karma-coverage": "^0.5.0", "karma-mocha": "^0.2.0", @@ -127,27 +165,14 @@ "karma-phantomjs-launcher": "^1.0.0", "karma-webpack-with-fast-source-maps": "^1.9.2", "mocha": "^2.2.5", - "node-sass": "^3.3.3", "nodemon": "^1.8.1", "phantomjs-polyfill": "0.0.2", "phantomjs-prebuilt": "^2.1.3", - "postcss-loader": "^0.8.0", "react-addons-test-utils": "^0.14.0", - "react-transform-catch-errors": "^1.0.2", - "react-transform-hmr": "^1.0.2", "redbox-react": "^1.2.2", - "redux-devtools": "^3.0.0", - "redux-devtools-dock-monitor": "^1.0.1", - "redux-devtools-log-monitor": "^1.0.1", - "rimraf": "^2.5.1", - "sass-loader": "^3.0.0", "sinon": "^1.17.3", "sinon-chai": "^2.8.0", - "style-loader": "^0.13.0", - "url-loader": "^0.5.6", - "webpack": "^1.12.14", - "webpack-dev-middleware": "1.5.1", - "webpack-hot-middleware": "^2.6.0", - "yargs": "^4.0.0" + "webpack-dev-middleware": "^1.6.1", + "webpack-hot-middleware": "^2.6.0" } } diff --git a/src/components/Counter.js b/src/components/Counter.js new file mode 100644 index 000000000..0c2b269a2 --- /dev/null +++ b/src/components/Counter.js @@ -0,0 +1,37 @@ +/* @flow */ +import React from 'react' +import classes from './Counter.scss' + +// FlowType annotations +type Props = { + counter: number, + doubleAsync: Function, + increment: Function +} + +export const Counter = (props: Props) => ( +
+

+ Counter: + {' '} + + {props.counter} + +

+ + {' '} + +
+) + +Counter.propTypes = { + counter: React.PropTypes.number.isRequired, + doubleAsync: React.PropTypes.func.isRequired, + increment: React.PropTypes.func.isRequired +} + +export default Counter diff --git a/src/views/HomeView/HomeView.scss b/src/components/Counter.scss similarity index 62% rename from src/views/HomeView/HomeView.scss rename to src/components/Counter.scss index ed75edf30..ba889938a 100644 --- a/src/views/HomeView/HomeView.scss +++ b/src/components/Counter.scss @@ -7,8 +7,6 @@ color: rgb(25,200,25); } -.duck { - display: block; - width: 100%; - margin-top: 1.5rem; +.counterContainer { + margin: 1em auto; } diff --git a/src/components/Header.js b/src/components/Header.js new file mode 100644 index 000000000..0acbe91d3 --- /dev/null +++ b/src/components/Header.js @@ -0,0 +1,18 @@ +import React from 'react' +import { IndexLink, Link } from 'react-router' +import classes from './Header.scss' + +export const Header = () => ( +
+

React Redux Starter Kit

+ + Home + + {' · '} + + Counter + +
+) + +export default Header diff --git a/src/components/Header.scss b/src/components/Header.scss new file mode 100644 index 000000000..1dcd731c3 --- /dev/null +++ b/src/components/Header.scss @@ -0,0 +1,4 @@ +.activeRoute { + font-weight: bold; + text-decoration: underline; +} diff --git a/src/components/README.md b/src/components/README.md new file mode 100644 index 000000000..c6c06b8a5 --- /dev/null +++ b/src/components/README.md @@ -0,0 +1,62 @@ +## Components + +This is where we keep stateless presentational components that will be used throughout +our application. + +### Super Simple Example + +```js +import React from 'react' + +const today = new Date(); +const year = today.getFullYear(); + +export default const FullYear = () => {year} +``` + +### Counter Example (using FlowType) + +- component is using FlowType annotations to type check props +- we can import + +```js +/* @flow */ +import React, { PropTypes } from 'react' +import classes from './Counter.scss' + +// FlowType annotations +type Props = { + counter: number, + doubleAsync: Function, + increment: Function +}; + +// We also recommend using react PropTypes +const counterPropTypes = { + counter: PropTypes.number.isRequired, + doubleAsync: PropTypes.func.isRequired, + increment: PropTypes.func.isRequired +} + +export const Counter = (props: Props) => ( +
+

+ 'Counter: ' + + {props.counter} + +

+ + {' '} + +
+) + +Counter.propTypes = counterPropTypes + +export default Counter +``` diff --git a/src/containers/DevTools.js b/src/containers/DevTools.js deleted file mode 100644 index d94406385..000000000 --- a/src/containers/DevTools.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -import { createDevTools } from 'redux-devtools' -import LogMonitor from 'redux-devtools-log-monitor' -import DockMonitor from 'redux-devtools-dock-monitor' - -export default createDevTools( - - - -) diff --git a/src/containers/DevToolsWindow.js b/src/containers/DevToolsWindow.js deleted file mode 100644 index 3a8dff3c4..000000000 --- a/src/containers/DevToolsWindow.js +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' -import { createDevTools } from 'redux-devtools' -import LogMonitor from 'redux-devtools-log-monitor' - -export default createDevTools( - -) diff --git a/src/containers/Root.js b/src/containers/Root.js deleted file mode 100644 index fecb1e419..000000000 --- a/src/containers/Root.js +++ /dev/null @@ -1,45 +0,0 @@ -import React, { PropTypes } from 'react' -import { Provider } from 'react-redux' -import { Router } from 'react-router' - -export default class Root extends React.Component { - static propTypes = { - history: PropTypes.object.isRequired, - routes: PropTypes.element.isRequired, - store: PropTypes.object.isRequired - }; - - get content () { - return ( - - {this.props.routes} - - ) - } - - get devTools () { - if (__DEBUG__) { - if (__DEBUG_NEW_WINDOW__) { - if (!window.devToolsExtension) { - require('../redux/utils/createDevToolsWindow').default(this.props.store) - } else { - window.devToolsExtension.open() - } - } else if (!window.devToolsExtension) { - const DevTools = require('containers/DevTools').default - return - } - } - } - - render () { - return ( - -
- {this.content} - {this.devTools} -
-
- ) - } -} diff --git a/src/layouts/CoreLayout/CoreLayout.js b/src/layouts/CoreLayout/CoreLayout.js index a435bcb92..eb1b4833d 100644 --- a/src/layouts/CoreLayout/CoreLayout.js +++ b/src/layouts/CoreLayout/CoreLayout.js @@ -1,27 +1,19 @@ -import React, { PropTypes } from 'react' +import React from 'react' +import Header from '../../components/Header' +import classes from './CoreLayout.scss' import '../../styles/core.scss' -// Note: Stateless/function components *will not* hot reload! -// react-transform *only* works on component classes. -// -// Since layouts rarely change, they are a good place to -// leverage React's new Stateless Functions: -// https://facebook.github.io/react/docs/reusable-components.html#stateless-functions -// -// CoreLayout is a pure function of its props, so we can -// define it with a plain javascript function... -function CoreLayout ({ children }) { - return ( -
-
- {children} -
+export const CoreLayout = ({ children }) => ( +
+
+
+ {children}
- ) -} +
+) CoreLayout.propTypes = { - children: PropTypes.element + children: React.PropTypes.element.isRequired } export default CoreLayout diff --git a/src/layouts/CoreLayout/CoreLayout.scss b/src/layouts/CoreLayout/CoreLayout.scss new file mode 100644 index 000000000..f07d10444 --- /dev/null +++ b/src/layouts/CoreLayout/CoreLayout.scss @@ -0,0 +1,3 @@ +.mainContainer { + padding-top:20px; +} diff --git a/src/layouts/CoreLayout/index.js b/src/layouts/CoreLayout/index.js index cf25b79df..7fe88c1c5 100644 --- a/src/layouts/CoreLayout/index.js +++ b/src/layouts/CoreLayout/index.js @@ -1,2 +1,3 @@ import CoreLayout from './CoreLayout' + export default CoreLayout diff --git a/src/main.js b/src/main.js index fddd49a62..f2a5b2ebd 100644 --- a/src/main.js +++ b/src/main.js @@ -3,9 +3,11 @@ import ReactDOM from 'react-dom' import createBrowserHistory from 'history/lib/createBrowserHistory' import { useRouterHistory } from 'react-router' import { syncHistoryWithStore } from 'react-router-redux' -import makeRoutes from './routes' -import Root from './containers/Root' -import configureStore from './redux/configureStore' +import Root from './routes/Root' +import createStore from './store/createStore' + +const MOUNT_ELEMENT = document.getElementById('root') +const DEFAULT_TITLE = 'React Redux Starter Kit' // Configure history for react-router const browserHistory = useRouterHistory(createBrowserHistory)({ @@ -16,20 +18,42 @@ const browserHistory = useRouterHistory(createBrowserHistory)({ // react-router-redux reducer under the key "router" in src/routes/index.js, // so we need to provide a custom `selectLocationState` to inform // react-router-redux of its location. -const initialState = window.__INITIAL_STATE__ -const store = configureStore(initialState, browserHistory) +const store = createStore(window.__INITIAL_STATE__, browserHistory) const history = syncHistoryWithStore(browserHistory, store, { selectLocationState: (state) => state.router }) -// Now that we have the Redux store, we can create our routes. We provide -// the store to the route definitions so that routes have access to it for -// hooks such as `onEnter`. -const routes = makeRoutes(store) - -// Now that redux and react-router have been configured, we can render the -// React application to the DOM! -ReactDOM.render( - , - document.getElementById('root') -) +// Default titles and nested template (react-helmet) +const title = { + defaultTitle: DEFAULT_TITLE, + titleTemplate: `%s - ${DEFAULT_TITLE}` +} + +let render = () => { + // This syntax will be updated in the near future to use System loader + const createRoutes = require('./routes/index').default + const routes = createRoutes(store) + const props = { history, routes, store, title } + + ReactDOM.render(, MOUNT_ELEMENT) +} + +// If supported, set up hot reloading and overlay for runtime errors +if (module.hot) { + const renderApp = render + const renderError = (error) => { + const RedBox = require('redbox-react') + + ReactDOM.render(, MOUNT_ELEMENT) + } + render = () => { + try { + renderApp() + } catch (error) { + renderError(error) + } + } + module.hot.accept(['./routes/index'], () => render()) +} + +render() diff --git a/src/redux/configureStore.js b/src/redux/configureStore.js deleted file mode 100644 index 6d3e5550a..000000000 --- a/src/redux/configureStore.js +++ /dev/null @@ -1,27 +0,0 @@ -import { applyMiddleware, compose, createStore } from 'redux' -import thunk from 'redux-thunk' -import rootReducer from './rootReducer' -import { routerMiddleware } from 'react-router-redux' - -export default function configureStore (initialState = {}, history) { - // Compose final middleware and use devtools in debug environment - let middleware = applyMiddleware(thunk, routerMiddleware(history)) - if (__DEBUG__) { - const devTools = window.devToolsExtension - ? window.devToolsExtension() - : require('containers/DevTools').default.instrument() - middleware = compose(middleware, devTools) - } - - // Create final store and subscribe router in debug env ie. for devtools - const store = middleware(createStore)(rootReducer, initialState) - - if (module.hot) { - module.hot.accept('./rootReducer', () => { - const nextRootReducer = require('./rootReducer').default - - store.replaceReducer(nextRootReducer) - }) - } - return store -} diff --git a/src/redux/rootReducer.js b/src/redux/rootReducer.js deleted file mode 100644 index 6f99d9eaf..000000000 --- a/src/redux/rootReducer.js +++ /dev/null @@ -1,8 +0,0 @@ -import { combineReducers } from 'redux' -import { routerReducer as router } from 'react-router-redux' -import counter from './modules/counter' - -export default combineReducers({ - counter, - router -}) diff --git a/src/redux/utils/createDevToolsWindow.js b/src/redux/utils/createDevToolsWindow.js deleted file mode 100644 index ecf51af1c..000000000 --- a/src/redux/utils/createDevToolsWindow.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import { Provider } from 'react-redux' -import DevTools from '../../containers/DevToolsWindow' - -export default function createDevToolsWindow (store) { - const win = window.open( - null, - 'redux-devtools', // give it a name so it reuses the same window - `width=400,height=${window.outerHeight},menubar=no,location=no,resizable=yes,scrollbars=no,status=no` - ) - - // reload in case it's reusing the same window with the old content - win.location.reload() - - // wait a little bit for it to reload, then render - setTimeout(() => { - // Wait for the reload to prevent: - // "Uncaught Error: Invariant Violation: _registerComponent(...): Target container is not a DOM element." - win.document.write('
') - win.document.body.style.margin = '0' - - ReactDOM.render( - - - - , win.document.getElementById('react-devtools-root') - ) - }, 10) -} diff --git a/src/routes/Counter/components/CounterView.js b/src/routes/Counter/components/CounterView.js new file mode 100644 index 000000000..2271c90c6 --- /dev/null +++ b/src/routes/Counter/components/CounterView.js @@ -0,0 +1,24 @@ +import React from 'react' +import Helmet from 'react-helmet' +import CounterContainer from '../containers/CounterContainer' + +/* Entry point for Counter route - useful for setting up layout, + document title (see below) as well as rendering named components + Can be removed, and route can render container directly.*/ + +const CounterView = (props) => ( +
+ + +
+) + +/* Note: Since this is a 'top level' route, we are passing the `props` + object to the container component, which incudes some useful helpers + provided by `react-router`, ie `params` and `route` which can then + be accessed using the second param of mapStateToProps(state, *ownProps*) + + In this starter-kit, we are not using this functionality, but it is + a good thing to keep in mind. */ + +export default CounterView diff --git a/src/routes/Counter/containers/CounterContainer.js b/src/routes/Counter/containers/CounterContainer.js new file mode 100644 index 000000000..d13e2727f --- /dev/null +++ b/src/routes/Counter/containers/CounterContainer.js @@ -0,0 +1,38 @@ +import { connect } from 'react-redux' +import { increment, doubleAsync } from '../modules/counter' + +/* This is a container component. Notice it does not contain any JSX, + nor does it import React. This component is **only** responsible for + wiring in the actions and state necessary to implement the presentational + component that it renders - in this case, the counter component: */ + +import Counter from 'components/Counter' + +/* Object of action creators (can also be function that returns object). + Keys will be passed as props to presentational components. Here we are + implementing our wrapper around increment; the component doesn't care */ + +const mapActionCreators = { + increment: () => increment(1), + doubleAsync +} + +const mapStateToProps = (state, ownProps) => ({ + counter: state.counter +}) + +/* Note: mapStateToProps is where you should use `reselect` to create selectors, ie: + + import { createSelector } from 'reselect' + const aboutCount = (state) => state.aboutCounter + const tripleCount = createSelector(aboutCount, (count) => count * 3) + const mapStateToProps = (state) => ({ + counter: tripleCount(state) + }) + + Selectors can compute derived data, allowing Redux to store the minimal possible state. + Selectors are efficient. A selector is not recomputed unless one of its arguments change. + Selectors are composable. They can be used as input to other selectors. + https://github.com/reactjs/reselect */ + +export default connect(mapStateToProps, mapActionCreators)(Counter) diff --git a/src/routes/Counter/index.js b/src/routes/Counter/index.js new file mode 100644 index 000000000..2a5ef244d --- /dev/null +++ b/src/routes/Counter/index.js @@ -0,0 +1,28 @@ +import { injectReducer } from '../../store/reducers' + +export default (store) => ({ + path: 'counter', + getComponents (location, next) { + // Define route dependencies for bundling + require.ensure([ + './containers/CounterContainer', + './components/CounterView', + './modules/counter' + ], (require) => { + // Now these modules can lazily evaluated using require hook provided. + // They will not loaded until the router invokes this callback. This is + // possible due to webpack's code splitting and chunking features. + const CounterView = require('./components/CounterView').default + const counterReducer = require('./modules/counter').default + + // Inject our reducer into the store + injectReducer(store, 'counter', counterReducer) + + // Render The CounterView (our 'main' route component) + next(null, CounterView) + + // Note: If you do not need any route-specific layout (or helpers, + // ie. doc title), you can pass a container directly to `next`. + }) + } +}) diff --git a/src/redux/modules/counter.js b/src/routes/Counter/modules/counter.js similarity index 100% rename from src/redux/modules/counter.js rename to src/routes/Counter/modules/counter.js diff --git a/src/views/HomeView/Duck.jpg b/src/routes/Home/assets/Duck.jpg similarity index 100% rename from src/views/HomeView/Duck.jpg rename to src/routes/Home/assets/Duck.jpg diff --git a/src/routes/Home/components/HomeView.js b/src/routes/Home/components/HomeView.js new file mode 100644 index 000000000..c8ccf6901 --- /dev/null +++ b/src/routes/Home/components/HomeView.js @@ -0,0 +1,12 @@ +import React from 'react' +import DuckImage from '../assets/Duck.jpg' +import classes from './HomeView.scss' + +export const HomeView = () => ( +
+

Welcome!

+ This is a duck, because Redux! +
+) + +export default HomeView diff --git a/src/routes/Home/components/HomeView.scss b/src/routes/Home/components/HomeView.scss new file mode 100644 index 000000000..10de960f1 --- /dev/null +++ b/src/routes/Home/components/HomeView.scss @@ -0,0 +1,5 @@ +.duck { + display: block; + width: 120px; + margin: 1.5rem auto; +} diff --git a/src/routes/Home/index.js b/src/routes/Home/index.js new file mode 100644 index 000000000..55c554089 --- /dev/null +++ b/src/routes/Home/index.js @@ -0,0 +1,5 @@ +import HomeView from './components/HomeView' + +export default { + component: HomeView +} diff --git a/src/routes/NotFound/NotFound.js b/src/routes/NotFound/NotFound.js new file mode 100644 index 000000000..e6c3900b4 --- /dev/null +++ b/src/routes/NotFound/NotFound.js @@ -0,0 +1,18 @@ +import React from 'react' +import { browserHistory } from 'react-router' +import Helmet from 'react-helmet' + +const goBack = (e) => { + e.preventDefault() + return browserHistory.goBack() +} + +const NotFound = () => ( +
+ +

Page Not Found!

+ Go Back +
+) + +export default NotFound diff --git a/src/routes/NotFound/index.js b/src/routes/NotFound/index.js new file mode 100644 index 000000000..ce065b5cb --- /dev/null +++ b/src/routes/NotFound/index.js @@ -0,0 +1,6 @@ +import NotFound from './NotFound' + +export default { + path: '*', + component: NotFound +} diff --git a/src/routes/Root.js b/src/routes/Root.js new file mode 100644 index 000000000..d7961b2c0 --- /dev/null +++ b/src/routes/Root.js @@ -0,0 +1,32 @@ +import React from 'react' +import { Provider } from 'react-redux' +import { Router } from 'react-router' +import Helmet from 'react-helmet' + +const Root = (props) => ( + +
+ + + {props.routes} + +
+
+) + +Root.propTypes = { + history: React.PropTypes.object.isRequired, + routes: React.PropTypes.object.isRequired, + store: React.PropTypes.object.isRequired, + title: React.PropTypes.shape({ + defaultTitle: React.PropTypes.string.isRequired, + titleTemplate: React.PropTypes.string.isRequired + }).isRequired +} + +// Use Redux DevTools chrome extension +if (__DEBUG__) { + if (!window.devToolsExtension) window.devToolsExtension.open() +} + +export default Root diff --git a/src/routes/index.js b/src/routes/index.js index dd12474c6..5419a064f 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,16 +1,33 @@ -import React from 'react' -import { Route, IndexRoute } from 'react-router' +import CoreLayout from '../layouts/CoreLayout/CoreLayout' +import Home from './Home' -// NOTE: here we're making use of the `resolve.root` configuration -// option in webpack, which allows us to specify import paths as if -// they were from the root of the ~/src directory. This makes it -// very easy to navigate to files regardless of how deeply nested -// your current file is. -import CoreLayout from 'layouts/CoreLayout/CoreLayout' -import HomeView from 'views/HomeView/HomeView' +/* We only need to import the bundles necessary for rendering the `indexRoute` + The rest will be loaded asynchronously */ -export default (store) => ( - - - -) +export const createRoutes = (store) => { + return { + path: '/', + component: CoreLayout, + indexRoute: Home, + getChildRoutes (location, next) { + // AMD require.ensure syntax will be updated in the near future to use + // System loader, but is currently required by webpack to create split points + require.ensure([/* Do not ensure! (load 'em async) */], (require) => { + next(null, [ + // Provide store for async reducers and middleware + require('./Counter').default(store), + require('./NotFound').default + ]) + }) + } + } +} + +/* Note: Instead of JSX, we are using react-router's PlainRoute, which uses + a simple javascript object to provide route definitions. This keeps things + very explicit, proves there's no magic happening, and is the preferred method + for code splitting. When creating a new route, we provide the instantiated + store so that it can continue to inject dyamically loaded reducers and leverage + tools such as `redux-saga` and `rereduce` */ + +export default createRoutes diff --git a/src/store/createStore.js b/src/store/createStore.js new file mode 100644 index 000000000..b04931483 --- /dev/null +++ b/src/store/createStore.js @@ -0,0 +1,33 @@ +import { applyMiddleware, compose, createStore } from 'redux' +import { routerMiddleware } from 'react-router-redux' +import thunk from 'redux-thunk' + +import reducers from './reducers' + +export default (initialState = {}, history) => { + // Compose final middleware and use devtools in debug environment + let middleware = applyMiddleware(thunk, routerMiddleware(history)) + + if (__DEBUG__) { + const devToolsExtension = window.devToolsExtension + + if (typeof devToolsExtension === 'function') { + middleware = compose(middleware, devToolsExtension()) + } + } + + // Create final store and subscribe router in debug env ie. for devtools + const store = middleware(createStore)(reducers(), initialState) + + store.asyncReducers = {} + + if (module.hot) { + module.hot.accept('./reducers', () => { + const reducers = require('./reducers').default + + store.replaceReducer(reducers) + }) + } + + return store +} diff --git a/src/store/reducers.js b/src/store/reducers.js new file mode 100644 index 000000000..d66806310 --- /dev/null +++ b/src/store/reducers.js @@ -0,0 +1,13 @@ +import { combineReducers } from 'redux' +import { routerReducer as router } from 'react-router-redux' + +export const reducers = (asyncReducers) => { + return combineReducers({ router, ...asyncReducers }) +} + +export const injectReducer = (store, name, asyncReducer) => { + store.asyncReducers[name] = asyncReducer + store.replaceReducer(reducers(store.asyncReducers)) +} + +export default reducers diff --git a/src/views/HomeView/HomeView.js b/src/views/HomeView/HomeView.js deleted file mode 100644 index 35f932fbe..000000000 --- a/src/views/HomeView/HomeView.js +++ /dev/null @@ -1,65 +0,0 @@ -/* @flow */ -import React, { PropTypes } from 'react' -import { connect } from 'react-redux' -import { increment, doubleAsync } from '../../redux/modules/counter' -import DuckImage from './Duck.jpg' -import classes from './HomeView.scss' - -// We can use Flow (http://flowtype.org/) to type our component's props -// and state. For convenience we've included both regular propTypes and -// Flow types, but if you want to try just using Flow you'll want to -// disable the eslint rule `react/prop-types`. -// NOTE: You can run `npm run flow:check` to check for any errors in your -// code, or `npm i -g flow-bin` to have access to the binary globally. -// Sorry Windows users :(. -type Props = { - counter: number, - doubleAsync: Function, - increment: Function -}; - -// We avoid using the `@connect` decorator on the class definition so -// that we can export the undecorated component for testing. -// See: http://rackt.github.io/redux/docs/recipes/WritingTests.html -export class HomeView extends React.Component { - static propTypes = { - counter: PropTypes.number.isRequired, - doubleAsync: PropTypes.func.isRequired, - increment: PropTypes.func.isRequired - }; - - render () { - return ( -
-
-
- This is a duck, because Redux. -
-
-

Welcome to the React Redux Starter Kit

-

- Sample Counter: - {' '} - {this.props.counter} -

- - {' '} - -
- ) - } -} - -const mapStateToProps = (state) => ({ - counter: state.counter -}) -export default connect((mapStateToProps), { - increment: () => increment(1), - doubleAsync -})(HomeView) diff --git a/src/views/HomeView/index.js b/src/views/HomeView/index.js deleted file mode 100644 index 9b2b8cf0c..000000000 --- a/src/views/HomeView/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import HomeView from './HomeView' -export default HomeView diff --git a/tests/components/Counter.spec.js b/tests/components/Counter.spec.js new file mode 100644 index 000000000..7f1477820 --- /dev/null +++ b/tests/components/Counter.spec.js @@ -0,0 +1,80 @@ +import React from 'react' +import { bindActionCreators } from 'redux' +import { Counter } from 'components/Counter' +import { shallow } from 'enzyme' + +describe('(Component) Counter', () => { + let _props, _spies, _wrapper + + beforeEach(() => { + _spies = {} + _props = { + counter: 5, + ...bindActionCreators({ + doubleAsync: (_spies.doubleAsync = sinon.spy()), + increment: (_spies.increment = sinon.spy()) + }, _spies.dispatch = sinon.spy()) + } + _wrapper = shallow() + }) + + it('Should render as a
.', () => { + expect(_wrapper.is('div')).to.equal(true) + }) + + it('Should render with an

that includes Sample Counter text.', () => { + expect(_wrapper.find('h2').text()).to.match(/Counter:/) + }) + + it('Should render props.counter at the end of the sample counter

.', () => { + expect(_wrapper.find('h2').text()).to.match(/5$/) + _wrapper.setProps({ counter: 8 }) + expect(_wrapper.find('h2').text()).to.match(/8$/) + }) + + it('Should render exactly two buttons.', () => { + expect(_wrapper).to.have.descendants('.btn') + }) + // + describe('An increment button...', () => { + let _button + + beforeEach(() => { + _button = _wrapper.find('button').filterWhere(a => a.text() === 'Increment') + }) + + it('has bootstrap classes', () => { + expect(_button.hasClass('btn btn-default')).to.be.true + }) + + it('Should dispatch a `increment` action when clicked', () => { + _spies.dispatch.should.have.not.been.called + + _button.simulate('click') + + _spies.dispatch.should.have.been.called + _spies.increment.should.have.been.called + }); + }) + + describe('A Double (Async) button...', () => { + let _button + + beforeEach(() => { + _button = _wrapper.find('button').filterWhere(a => a.text() === 'Double (Async)') + }) + + it('has bootstrap classes', () => { + expect(_button.hasClass('btn btn-default')).to.be.true + }) + + it('Should dispatch a `doubleAsync` action when clicked', () => { + _spies.dispatch.should.have.not.been.called + + _button.simulate('click') + + _spies.dispatch.should.have.been.called + _spies.doubleAsync.should.have.been.called + }); + }) +}) diff --git a/tests/components/Header.spec.js b/tests/components/Header.spec.js new file mode 100644 index 000000000..dbf37306c --- /dev/null +++ b/tests/components/Header.spec.js @@ -0,0 +1,30 @@ +import React from 'react' +import { Header } from 'components/Header' +import { IndexLink, Link } from 'react-router' +import { shallow } from 'enzyme' + +describe('(Component) Header', () => { + let _wrapper + + beforeEach(() => { + _wrapper = shallow(
) + }) + + it('Renders a welcome message', () => { + const welcome = _wrapper.find('h1') + expect(welcome).to.exist + expect(welcome.text()).to.match(/React Redux Starter Kit/) + }) + + describe('Navigation links...', () => { + + it('Should render an IndexLink to Home route', () => { + expect(_wrapper.contains()).to.equal.true + }) + + it('Should render an Link to Counter route)', () => { + expect(_wrapper.contains()).to.equal.true + }) + + }) +}) diff --git a/tests/routes/Counter/components/CounterView.spec.js b/tests/routes/Counter/components/CounterView.spec.js new file mode 100644 index 000000000..e69de29bb diff --git a/tests/routes/Counter/index.spec.js b/tests/routes/Counter/index.spec.js new file mode 100644 index 000000000..c2d78dbfb --- /dev/null +++ b/tests/routes/Counter/index.spec.js @@ -0,0 +1,18 @@ +import CounterRoute from 'routes/Counter' + +describe('(Route) Counter', () => { + let _route + + beforeEach(() => { + _route = CounterRoute({}) + }) + + it('Should return a route configuration object', () => { + expect(typeof(_route)).to.equal('object') + }) + + it('Configuration should contain path `counter`', () => { + expect(_route.path).to.equal('counter') + }) + +}) diff --git a/tests/redux/modules/counter.spec.js b/tests/routes/Counter/modules/counter.spec.js similarity index 78% rename from tests/redux/modules/counter.spec.js rename to tests/routes/Counter/modules/counter.spec.js index 58757ff12..885a20b2a 100644 --- a/tests/redux/modules/counter.spec.js +++ b/tests/routes/Counter/modules/counter.spec.js @@ -3,23 +3,23 @@ import { increment, doubleAsync, default as counterReducer -} from 'redux/modules/counter' +} from 'routes/Counter/modules/counter' -describe('(Redux Module) Counter', function () { - it('Should export a constant COUNTER_INCREMENT.', function () { +describe('(Redux Module) Counter', () => { + it('Should export a constant COUNTER_INCREMENT.', () => { expect(COUNTER_INCREMENT).to.equal('COUNTER_INCREMENT') }) - describe('(Reducer)', function () { - it('Should be a function.', function () { + describe('(Reducer)', () => { + it('Should be a function.', () => { expect(counterReducer).to.be.a('function') }) - it('Should initialize with a state of 0 (Number).', function () { + it('Should initialize with a state of 0 (Number).', () => { expect(counterReducer(undefined, {})).to.equal(0) }) - it('Should return the previous state if an action was not matched.', function () { + it('Should return the previous state if an action was not matched.', () => { let state = counterReducer(undefined, {}) expect(state).to.equal(0) state = counterReducer(state, {type: '@@@@@@@'}) @@ -31,30 +31,30 @@ describe('(Redux Module) Counter', function () { }) }) - describe('(Action Creator) increment', function () { - it('Should be exported as a function.', function () { + describe('(Action Creator) increment', () => { + it('Should be exported as a function.', () => { expect(increment).to.be.a('function') }) - it('Should return an action with type "COUNTER_INCREMENT".', function () { + it('Should return an action with type "COUNTER_INCREMENT".', () => { expect(increment()).to.have.property('type', COUNTER_INCREMENT) }) - it('Should assign the first argument to the "payload" property.', function () { + it('Should assign the first argument to the "payload" property.', () => { expect(increment(5)).to.have.property('payload', 5) }) - it('Should default the "payload" property to 1 if not provided.', function () { + it('Should default the "payload" property to 1 if not provided.', () => { expect(increment()).to.have.property('payload', 1) }) }) - describe('(Action Creator) doubleAsync', function () { + describe('(Action Creator) doubleAsync', () => { let _globalState let _dispatchSpy let _getStateSpy - beforeEach(function () { + beforeEach(() => { _globalState = { counter: counterReducer(undefined, {}) } @@ -69,19 +69,19 @@ describe('(Redux Module) Counter', function () { }) }) - it('Should be exported as a function.', function () { + it('Should be exported as a function.', () => { expect(doubleAsync).to.be.a('function') }) - it('Should return a function (is a thunk).', function () { + it('Should return a function (is a thunk).', () => { expect(doubleAsync()).to.be.a('function') }) - it('Should return a promise from that thunk that gets fulfilled.', function () { + it('Should return a promise from that thunk that gets fulfilled.', () => { return doubleAsync()(_dispatchSpy, _getStateSpy).should.eventually.be.fulfilled }) - it('Should call dispatch and getState exactly once.', function () { + it('Should call dispatch and getState exactly once.', () => { return doubleAsync()(_dispatchSpy, _getStateSpy) .then(() => { _dispatchSpy.should.have.been.calledOnce @@ -89,7 +89,7 @@ describe('(Redux Module) Counter', function () { }) }) - it('Should produce a state that is double the previous state.', function () { + it('Should produce a state that is double the previous state.', () => { _globalState = { counter: 2 } return doubleAsync()(_dispatchSpy, _getStateSpy) @@ -110,8 +110,8 @@ describe('(Redux Module) Counter', function () { // NOTE: if you have a more complex state, you will probably want to verify // that you did not mutate the state. In this case our state is just a number // (which cannot be mutated). - describe('(Action Handler) COUNTER_INCREMENT', function () { - it('Should increment the state by the action payload\'s "value" property.', function () { + describe('(Action Handler) COUNTER_INCREMENT', () => { + it('Should increment the state by the action payload\'s "value" property.', () => { let state = counterReducer(undefined, {}) expect(state).to.equal(0) state = counterReducer(state, increment(1)) diff --git a/tests/routes/Home/components/HomeView.spec.js b/tests/routes/Home/components/HomeView.spec.js new file mode 100644 index 000000000..0193f1316 --- /dev/null +++ b/tests/routes/Home/components/HomeView.spec.js @@ -0,0 +1,24 @@ +import React from 'react' +import { HomeView } from 'routes/Home/components/HomeView' +import { render } from 'enzyme' + +describe('(View) Home', function () { + let _component + + beforeEach(() => { + _component = render() + }) + + it('Renders a welcome message', () => { + const welcome = _component.find('h4') + expect(welcome).to.exist + expect(welcome.text()).to.match(/Welcome!/) + }) + + it('Renders an awesome duck image', () => { + const duck = _component.find('img') + expect(duck).to.exist + expect(duck.attr('alt')).to.match(/This is a duck, because Redux!/) + }) + +}) diff --git a/tests/routes/Home/index.spec.js b/tests/routes/Home/index.spec.js new file mode 100644 index 000000000..e69de29bb diff --git a/tests/views/HomeView.spec.js b/tests/views/HomeView.spec.js deleted file mode 100644 index 45a95deea..000000000 --- a/tests/views/HomeView.spec.js +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react' -import TestUtils from 'react-addons-test-utils' -import { bindActionCreators } from 'redux' -import { HomeView } from 'views/HomeView/HomeView' -import { mount } from 'enzyme' - -function shallowRender (component) { - const renderer = TestUtils.createRenderer() - - renderer.render(component) - return renderer.getRenderOutput() -} - -function renderWithProps (props = {}) { - return TestUtils.renderIntoDocument() -} - -function shallowRenderWithProps (props = {}) { - return shallowRender() -} - -describe('(View) Home', function () { - let _component, _rendered, _props, _spies - - beforeEach(function () { - _spies = {} - _props = { - counter: 0, - ...bindActionCreators({ - doubleAsync: (_spies.doubleAsync = sinon.spy()), - increment: (_spies.increment = sinon.spy()) - }, _spies.dispatch = sinon.spy()) - } - - _component = shallowRenderWithProps(_props) - _rendered = renderWithProps(_props) - }) - - it('Should render as a
.', function () { - expect(_component.type).to.equal('div') - }) - - it('Should include an

with welcome text.', function () { - const h1 = TestUtils.findRenderedDOMComponentWithTag(_rendered, 'h1') - - expect(h1).to.exist - expect(h1.textContent).to.match(/Welcome to the React Redux Starter Kit/) - }) - - it('Should render with an

that includes Sample Counter text.', function () { - const h2 = TestUtils.findRenderedDOMComponentWithTag(_rendered, 'h2') - - expect(h2).to.exist - expect(h2.textContent).to.match(/Sample Counter/) - }) - - it('Should render props.counter at the end of the sample counter

.', function () { - const h2 = TestUtils.findRenderedDOMComponentWithTag( - renderWithProps({ ..._props, counter: 5 }), 'h2' - ) - - expect(h2).to.exist - expect(h2.textContent).to.match(/5$/) - }) - - it('Should render exactly two buttons.', function () { - const wrapper = mount() - - expect(wrapper).to.have.descendants('.btn') - }) - - describe('An increment button...', function () { - let _btn - - beforeEach(() => { - _btn = TestUtils.scryRenderedDOMComponentsWithTag(_rendered, 'button') - .filter(a => /Increment/.test(a.textContent))[0] - }) - - it('should be rendered.', function () { - expect(_btn).to.exist - }) - - it('should dispatch an action when clicked.', function () { - _spies.dispatch.should.have.not.been.called - TestUtils.Simulate.click(_btn) - _spies.dispatch.should.have.been.called - }) - }) - - describe('A Double (Async) button...', function () { - let _btn - - beforeEach(() => { - _btn = TestUtils.scryRenderedDOMComponentsWithTag(_rendered, 'button') - .filter(a => /Double/.test(a.textContent))[0] - }) - - it('should be rendered.', function () { - expect(_btn).to.exist - }) - - it('should dispatch an action when clicked.', function () { - _spies.dispatch.should.have.not.been.called - TestUtils.Simulate.click(_btn) - _spies.dispatch.should.have.been.called - }) - }) -}) From 33b256a79a5ebf43ebb95c46730c5a28a67fc067 Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Mon, 4 Apr 2016 03:01:14 -0400 Subject: [PATCH 02/15] chore(docs): Update documentation --- README.md | 4 ++-- .../Counter/containers/CounterContainer.js | 8 ++++---- src/routes/Counter/index.js | 6 +++--- src/routes/index.js | 20 ++++++++----------- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 33e270db5..3f44f5ea6 100644 --- a/README.md +++ b/README.md @@ -226,8 +226,8 @@ Small applications can be built using a flat directory structure, with folders f We use `react-router` [route definitions](https://github.com/reactjs/react-router/blob/master/docs/API.md#plainroute) (`/index.js`) to define units of logic within our application. This provides many benefits which may not immediately be obvious: -- Routes can be be bundled into "chunks" using webpack's [code splitting](https://webpack.github.io/docs/code-splitting.html) and merging algorithm. This means that the entire dependency tree for each route can be omitted from the initial bundle and then loaded *on demand*, only when it is required. -- Since logic is self-contained, routes can easily be broken into separate repositories and referenced with webpack's [DLL plugin)[https://github.com/webpack/docs/wiki/list-of-plugins#dllplugin] for flexible, high-performance development and scalability. +- Routes can be be bundled into "chunks" using webpack's [code splitting](https://webpack.github.io/docs/code-splitting.html) and merging algorithm. This means that the entire dependency tree for each route can be omitted from the initial bundle and then loaded *on demand*. +- Since logic is self-contained, routes can easily be broken into separate repositories and referenced with webpack's [DLL plugin](https://github.com/webpack/docs/wiki/list-of-plugins#dllplugin) for flexible, high-performance development and scalability. #### Layouts - Regular stateless components that dictate major page structure diff --git a/src/routes/Counter/containers/CounterContainer.js b/src/routes/Counter/containers/CounterContainer.js index d13e2727f..c04388a73 100644 --- a/src/routes/Counter/containers/CounterContainer.js +++ b/src/routes/Counter/containers/CounterContainer.js @@ -3,8 +3,8 @@ import { increment, doubleAsync } from '../modules/counter' /* This is a container component. Notice it does not contain any JSX, nor does it import React. This component is **only** responsible for - wiring in the actions and state necessary to implement the presentational - component that it renders - in this case, the counter component: */ + wiring in the actions and state necessary to render a presentational + component - in this case, the counter: */ import Counter from 'components/Counter' @@ -24,8 +24,8 @@ const mapStateToProps = (state, ownProps) => ({ /* Note: mapStateToProps is where you should use `reselect` to create selectors, ie: import { createSelector } from 'reselect' - const aboutCount = (state) => state.aboutCounter - const tripleCount = createSelector(aboutCount, (count) => count * 3) + const counter = (state) => state.counter + const tripleCount = createSelector(counter, (count) => count * 3) const mapStateToProps = (state) => ({ counter: tripleCount(state) }) diff --git a/src/routes/Counter/index.js b/src/routes/Counter/index.js index 2a5ef244d..c1da2a9d0 100644 --- a/src/routes/Counter/index.js +++ b/src/routes/Counter/index.js @@ -9,9 +9,9 @@ export default (store) => ({ './components/CounterView', './modules/counter' ], (require) => { - // Now these modules can lazily evaluated using require hook provided. - // They will not loaded until the router invokes this callback. This is - // possible due to webpack's code splitting and chunking features. + // These modules can be lazily evaluated using require hook, and + // will not loaded until the router invokes this callback. Webpack + // detects this and creates a split point. const CounterView = require('./components/CounterView').default const counterReducer = require('./modules/counter').default diff --git a/src/routes/index.js b/src/routes/index.js index 5419a064f..77048529b 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,8 +1,13 @@ +// We only need to import the modules necessary for initial render import CoreLayout from '../layouts/CoreLayout/CoreLayout' import Home from './Home' -/* We only need to import the bundles necessary for rendering the `indexRoute` - The rest will be loaded asynchronously */ +/* Note: Instead of JSX, we are using react-router's PlainRoute, which uses + a simple javascript object to provide route definitions. This keeps things + very explicit, proves there's no magic happening, and is the preferred method + for code splitting. When creating a new route, we provide the instantiated + store so that it can continue to inject dyamically loaded reducers and leverage + tools such as `redux-saga` and `rereduce` */ export const createRoutes = (store) => { return { @@ -10,9 +15,7 @@ export const createRoutes = (store) => { component: CoreLayout, indexRoute: Home, getChildRoutes (location, next) { - // AMD require.ensure syntax will be updated in the near future to use - // System loader, but is currently required by webpack to create split points - require.ensure([/* Do not ensure! (load 'em async) */], (require) => { + require.ensure([], (require) => { next(null, [ // Provide store for async reducers and middleware require('./Counter').default(store), @@ -23,11 +26,4 @@ export const createRoutes = (store) => { } } -/* Note: Instead of JSX, we are using react-router's PlainRoute, which uses - a simple javascript object to provide route definitions. This keeps things - very explicit, proves there's no magic happening, and is the preferred method - for code splitting. When creating a new route, we provide the instantiated - store so that it can continue to inject dyamically loaded reducers and leverage - tools such as `redux-saga` and `rereduce` */ - export default createRoutes From 6a30c23b68711a2a1822ba4eb30f39b403af8f23 Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Mon, 4 Apr 2016 14:07:44 -0400 Subject: [PATCH 03/15] refactor(store): update injectReducer api --- src/routes/Counter/index.js | 6 +++++- src/store/reducers.js | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/routes/Counter/index.js b/src/routes/Counter/index.js index c1da2a9d0..d213a2baf 100644 --- a/src/routes/Counter/index.js +++ b/src/routes/Counter/index.js @@ -16,7 +16,11 @@ export default (store) => ({ const counterReducer = require('./modules/counter').default // Inject our reducer into the store - injectReducer(store, 'counter', counterReducer) + injectReducer({ + store, + key: 'counter', + reducer: counterReducer + }) // Render The CounterView (our 'main' route component) next(null, CounterView) diff --git a/src/store/reducers.js b/src/store/reducers.js index d66806310..ef52b73dc 100644 --- a/src/store/reducers.js +++ b/src/store/reducers.js @@ -5,8 +5,8 @@ export const reducers = (asyncReducers) => { return combineReducers({ router, ...asyncReducers }) } -export const injectReducer = (store, name, asyncReducer) => { - store.asyncReducers[name] = asyncReducer +export const injectReducer = ({ store, key, reducer }) => { + store.asyncReducers[key] = reducer store.replaceReducer(reducers(store.asyncReducers)) } From 0965e8c7d4784ac66d281cf991caa880c35d4e9e Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Mon, 4 Apr 2016 20:52:13 -0400 Subject: [PATCH 04/15] Update README and createStore --- README.md | 84 ++++++++++++++++++++++++---------------- src/store/createStore.js | 2 +- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 3f44f5ea6..495336044 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ make sure to copy over the `blueprints` folder in this project for starter-kit s Structure --------- -The folder structure provided is only meant to serve as a guide, it is by no means prescriptive. The current fractal hierarchy was inspired by [an old angular RFC](https://docs.google.com/document/u/1/d/1XXMvReO8-Awi1EZXAXS4PzDzdNvV6pGcuaF4Q9821Es/pub) and contributed by [Justin Greenberg](https://github.com/justingreenberg). +The folder structure provided is only meant to serve as a guide, it is by no means prescriptive. ``` . @@ -198,56 +198,74 @@ The folder structure provided is only meant to serve as a guide, it is by no mea ├── server # Koa application (uses webpack middleware) │ └── main.js # Server application entry point ├── src # Application source code -│ ├── components # App-wide Presentational React Components -│ ├── store # Redux-specific pieces -│ │   ├── createStore.js # Create and instrument redux store -│ │   └── reducers.js # Reducer registry and injection +│ ├── main.js # Application bootstrap and rendering +│ ├── components # Reusable Presentational Components +│ ├── containers # Reusable Container Components │ ├── layouts # Components that dictate major page structure -│ ├── routes # Main route definitions and async split points -│ │   ├── index.js # Bootstrap main application routes with store -│ │   ├── Home * # Fractal (All Route-specific, as needed) -│ │   │   ├── index.js * # Route definitions and async split points -│ │   │   ├── assets # Assets required to render components -│ │   │   ├── components # Presentational React Components -│ │   │   ├── containers # Connect components to actions and store -│ │   │   ├── modules # Collections of reducers/constants/actions -│ │   │   └── routes # Sub-route definitions and async split points -│ │   └── NotFound # Capture unknown routes in component │ ├── static # Static assets (not imported anywhere in source code) │ ├── styles # Application-wide styles (generally settings) -│ └── main.js # Application bootstrap and rendering +│ ├── store # Redux-specific pieces +│ │   ├── createStore.js # Create and instrument redux store +│ │   └── reducers.js # Reducer registry and injection +│ └── routes # Main route definitions and async split points +│    ├── index.js # Bootstrap main application routes with store +│    ├── Root.js # Wrapper component for context-aware providers +│    ├── Home * # Fractal route +│    │   ├── index.js * # Route definitions and async split points +│    │   ├── assets # Assets required to render components +│    │   ├── components # Presentational React Components +│    │   ├── containers # Connect components to actions and store +│    │   ├── modules # Collections of reducers/constants/actions +│    │   └── routes # Fractal sub-routes, optional +│    └── NotFound # Capture unknown routes in component └── tests # Unit tests ``` ### Fractal Structure (or, Recursive Route Hierarchy) -Small applications can be built using a flat directory structure, with folders for `components`, `containers`, etc. However, this does not scale and can seriously affect production velocity as your project grows. By starting with a fractal structure, you are forced to think about your architecture more strategically from day one. +Small applications can be built using a flat directory structure, with folders for `components`, `containers`, etc. However, this does not scale and can seriously affect development velocity as your project grows. By starting with a fractal structure, your application drives it's own architecture from day one. -We use `react-router` [route definitions](https://github.com/reactjs/react-router/blob/master/docs/API.md#plainroute) (`/index.js`) to define units of logic within our application. +We use `react-router` [route definitions](https://github.com/reactjs/react-router/blob/master/docs/API.md#plainroute) (`/index.js`) to define units of logic within our application. *Additional child routes can be nested in a fractal hierarchy.* -This provides many benefits which may not immediately be obvious: +This provides many benefits that may not be immediately obvious: - Routes can be be bundled into "chunks" using webpack's [code splitting](https://webpack.github.io/docs/code-splitting.html) and merging algorithm. This means that the entire dependency tree for each route can be omitted from the initial bundle and then loaded *on demand*. - Since logic is self-contained, routes can easily be broken into separate repositories and referenced with webpack's [DLL plugin](https://github.com/webpack/docs/wiki/list-of-plugins#dllplugin) for flexible, high-performance development and scalability. -#### Layouts -- Regular stateless components that dictate major page structure -- Useful for populating structure with named routes +##### Layouts +- Stateless components that dictate major page structure +- Useful for composing `react-router` [named components](https://github.com/reactjs/react-router/blob/master/docs/API.md#components-1) into views -#### Components -- Components should be stateless and purely presentational -- Prefer functional components ie. `const Cool = ({ who }) =>
${who} is cool
` -- The top-level `components` directory should be thought of as a global common palette +##### Components +- Prefer [stateless function components](https://facebook.github.io/react/docs/reusable-components.html#stateless-functions) + - example: `const HelloMessage = props =>
Hello {props.name}
` +- Top-level `components` and `containers` directories contain reusable component palette -#### Containers +##### Containers - Containers **only** `connect` presentational components to actions/state + - Rule of thumb: **no JSX in containers**! - One or many container components can be composed in a stateless function component -- Rule of thumb: **no JSX in containers**! - -#### Routes -- A route directory +- Props injected by `react-router` can be accessed in containers using `connect`: + ```js + // CounterContainerWithName.js + import { connect } from 'react-redux' + import Counter from 'components/Counter' + export const mapStateToProps = (state, ownProps) => ({ + counter: state.counter, + music: ownProps.location.query.music // why not + }) + export default connect(mapStateToProps)(Counter) + + // Location -> 'local.dev/counter?music=reggae' + // Counter.props = { counter: 0, music: 'reggae' } + ``` + +##### Routes +- A route directory... - *Must* contain an `index.js` that returns route definition - - *May* contain assets, components, containers, modules, and additional child Routes as needed -- Child routes follow the same structure recursively + - **Optional:** assets, components, containers, redux modules, nested child routes + - Additional child routes can be nested within `routes` directory in a fractal hierarchy. + +Again, this is by no means prescriptive. This setup provides a flexible foundation for module bundling and dynamic loading. **Using a fractal structure is optional—smaller apps might benefit from a flat routes directory**, which is totally cool! Code splitting is currently based on static analysis of require and will still work with this setup. The folder structure itself is simply for organizational purposes. Webpack ------- diff --git a/src/store/createStore.js b/src/store/createStore.js index b04931483..3ed857953 100644 --- a/src/store/createStore.js +++ b/src/store/createStore.js @@ -17,7 +17,7 @@ export default (initialState = {}, history) => { } // Create final store and subscribe router in debug env ie. for devtools - const store = middleware(createStore)(reducers(), initialState) + const store = createStore(reducers(), initialState, middleware) store.asyncReducers = {} From a4e1d2a69e7c19cea53f5d8bf6dc02ad979a3988 Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Mon, 11 Apr 2016 22:35:58 -0400 Subject: [PATCH 05/15] refactor(utils): Use path helpers, remove IIFE --- build/webpack.config.js | 16 +++++++++------- config/_base.js | 21 +++++++++------------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/build/webpack.config.js b/build/webpack.config.js index 9d25388a6..c358eb0b3 100644 --- a/build/webpack.config.js +++ b/build/webpack.config.js @@ -15,7 +15,7 @@ const webpackConfig = { target: 'web', devtool: config.compiler_devtool, resolve: { - root: paths.base(config.dir_client), + root: paths.client(), extensions: ['', '.js', '.jsx', '.json'] }, module: {} @@ -23,7 +23,7 @@ const webpackConfig = { // ------------------------------------ // Entry Points // ------------------------------------ -const APP_ENTRY_PATH = paths.base(config.dir_client) + '/main.js' +const APP_ENTRY_PATH = paths.client('main.js') webpackConfig.entry = { app: __DEV__ @@ -37,7 +37,7 @@ webpackConfig.entry = { // ------------------------------------ webpackConfig.output = { filename: `[name].[${config.compiler_hash_type}].js`, - path: paths.base(config.dir_dist), + path: paths.dist(), publicPath: config.compiler_public_path } @@ -81,9 +81,11 @@ if (__DEV__) { // Don't split bundles during testing, since we only want import one bundle if (!__TEST__) { - webpackConfig.plugins.push(new webpack.optimize.CommonsChunkPlugin({ - names: ['vendor'] - })) + webpackConfig.plugins.push( + new webpack.optimize.CommonsChunkPlugin({ + names: ['vendor'] + }) + ) } // ------------------------------------ @@ -155,7 +157,7 @@ const PATHS_TO_TREAT_AS_CSS_MODULES = [ // If config has CSS modules enabled, treat this project's styles as CSS modules. if (config.compiler_css_modules) { PATHS_TO_TREAT_AS_CSS_MODULES.push( - paths.base(config.dir_client).replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&') + paths.client().replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&') ) } diff --git a/config/_base.js b/config/_base.js index ebdcae854..d8d7953af 100644 --- a/config/_base.js +++ b/config/_base.js @@ -99,17 +99,14 @@ config.compiler_vendor = config.compiler_vendor // ------------------------------------ // Utilities // ------------------------------------ -config.utils_paths = (() => { - const resolve = path.resolve - - const base = (...args) => - resolve.apply(resolve, [config.path_base, ...args]) - - return { - base : base, - client : base.bind(null, config.dir_client), - dist : base.bind(null, config.dir_dist) - } -})() +const resolve = path.resolve +const base = (...args) => + Reflect.apply(resolve, null, [config.path_base, ...args]) + +config.utils_paths = { + base : base, + client : base.bind(null, config.dir_client), + dist : base.bind(null, config.dir_dist) +} export default config From 811fdfe37f5c751040b37e52992365270438c7f4 Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Mon, 11 Apr 2016 22:36:25 -0400 Subject: [PATCH 06/15] refactor(deps): remove helmet and reselect --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index 4562ee828..ca7f08a9f 100644 --- a/package.json +++ b/package.json @@ -125,15 +125,11 @@ "postcss-loader": "^0.8.0", "react": "^15.0.0", "react-dom": "^15.0.0", - "react-helmet": "^3.0.0", - "react": "^15.0.0", - "react-dom": "^15.0.0", "react-redux": "^4.0.0", "react-router": "^2.0.0", "react-router-redux": "^4.0.0", "redux": "^3.0.0", "redux-thunk": "^2.0.0", - "reselect": "^2.2.1", "rimraf": "^2.5.1", "sass-loader": "^3.0.0", "style-loader": "^0.13.0", From 24d134f67883d5f947f85dcf7434008bc68ef8ee Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Mon, 11 Apr 2016 22:37:03 -0400 Subject: [PATCH 07/15] chore(docs): update main README --- README.md | 21 ++++++++------ src/components/README.md | 62 ---------------------------------------- 2 files changed, 12 insertions(+), 71 deletions(-) delete mode 100644 src/components/README.md diff --git a/README.md b/README.md index 159728485..d8a4c9cb1 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,6 @@ Great, you now have a fresh project! There are a few titles you'll probably want * `~/package.json` - package name * `~/src/index.html` - template title tag -* `~/src/main.js` - Helmet document title Usage ----- @@ -219,14 +218,16 @@ The folder structure provided is only meant to serve as a guide, it is by no mea │    │   ├── components # Presentational React Components │    │   ├── container # Connect components to actions and store │    │   ├── modules # Collections of reducers/constants/actions -│    │   └── routes ** # Fractal sub-routes (optional) +│    │   └── routes ** # Fractal sub-routes (** optional) │    └── NotFound # Capture unknown routes in component └── tests # Unit tests ``` -#### Fractal App Structure (aka Self-Contained Apps, Recursive Route Hierarchy, Modules, Providers) +#### Fractal App Structure -Small applications can be built using a flat directory structure, with folders for `components`, `containers`, etc. However, this structure does not scale and can seriously affect development velocity as your project grows. By starting with a fractal structure, your application drives it's own architecture from day one. +_Also known as: Self-Contained Apps, Recursive Route Hierarchy, Providers, etc_ + +Small applications can be built using a flat directory structure, with folders for `components`, `containers`, etc. However, this structure does not scale and can seriously affect development velocity as your project grows. Starting with a fractal structure allows your application to organically drive it's own architecture from day one. We use `react-router` [route definitions](https://github.com/reactjs/react-router/blob/master/docs/API.md#plainroute) (`/index.js`) to define units of logic within our application. *Additional child routes can be nested in a fractal hierarchy.* @@ -234,22 +235,24 @@ This provides many benefits that may not be immediately obvious: - Routes can be be bundled into "chunks" using webpack's [code splitting](https://webpack.github.io/docs/code-splitting.html) and merging algorithm. This means that the entire dependency tree for each route can be omitted from the initial bundle and then loaded *on demand*. - Since logic is self-contained, routes can easily be broken into separate repositories and referenced with webpack's [DLL plugin](https://github.com/webpack/docs/wiki/list-of-plugins#dllplugin) for flexible, high-performance development and scalability. +Large, mature apps tend to naturally organize themselves in this way—analogous to large, mature trees (as in actual trees :evergreen_tree:). The trunk is the router, branches are route bundles, and leaves are views composed of common/shared components/containers. Global application and UI state should be placed on or close to the trunk (or perhaps at the base of a huge branch, eg. `/app` route). + ##### Layouts - Stateless components that dictate major page structure - Useful for composing `react-router` [named components](https://github.com/reactjs/react-router/blob/master/docs/API.md#components-1) into views ##### Components - Prefer [stateless function components](https://facebook.github.io/react/docs/reusable-components.html#stateless-functions) - - example: `const HelloMessage = ({ name }) =>
Hello {name}
` + - eg: `const HelloMessage = ({ name }) =>
Hello {name}
` - Top-level `components` and `containers` directories contain reusable components ##### Containers - Containers **only** `connect` presentational components to actions/state - Rule of thumb: **no JSX in containers**! - One or many container components can be composed in a stateless function component -- Props injected by `react-router` can be accessed in containers using `connect`: +- Tip: props injected by `react-router` can be accessed using `connect`: ```js - // CounterContainerWithName.js + // CounterWithMusicContainer.js import { connect } from 'react-redux' import Counter from 'components/Counter' export const mapStateToProps = (state, ownProps) => ({ @@ -258,7 +261,7 @@ This provides many benefits that may not be immediately obvious: }) export default connect(mapStateToProps)(Counter) - // Location -> 'local.dev/counter?music=reggae' + // Location -> 'localhost:3000/counter?music=reggae' // Counter.props = { counter: 0, music: 'reggae' } ``` @@ -268,7 +271,7 @@ This provides many benefits that may not be immediately obvious: - **Optional:** assets, components, containers, redux modules, nested child routes - Additional child routes can be nested within `routes` directory in a fractal hierarchy. -Again, this is by no means prescriptive. This setup provides a flexible foundation for module bundling and dynamic loading. **Using a fractal structure is optional—smaller apps might benefit from a flat routes directory**, which is totally cool! Code splitting is currently based on static analysis of require and will still work with this setup. The folder structure itself is simply for organizational purposes. +Note: This structure is designed to provide a flexible foundation for module bundling and dynamic loading. **Using a fractal structure is optional—smaller apps might benefit from a flat routes directory**, which is totally cool! Webpack creates split points based on static analysis of `require` during compilation; the recursive hierarchy folder structure is simply for organizational purposes. Webpack ------- diff --git a/src/components/README.md b/src/components/README.md deleted file mode 100644 index c6c06b8a5..000000000 --- a/src/components/README.md +++ /dev/null @@ -1,62 +0,0 @@ -## Components - -This is where we keep stateless presentational components that will be used throughout -our application. - -### Super Simple Example - -```js -import React from 'react' - -const today = new Date(); -const year = today.getFullYear(); - -export default const FullYear = () => {year} -``` - -### Counter Example (using FlowType) - -- component is using FlowType annotations to type check props -- we can import - -```js -/* @flow */ -import React, { PropTypes } from 'react' -import classes from './Counter.scss' - -// FlowType annotations -type Props = { - counter: number, - doubleAsync: Function, - increment: Function -}; - -// We also recommend using react PropTypes -const counterPropTypes = { - counter: PropTypes.number.isRequired, - doubleAsync: PropTypes.func.isRequired, - increment: PropTypes.func.isRequired -} - -export const Counter = (props: Props) => ( -
-

- 'Counter: ' - - {props.counter} - -

- - {' '} - -
-) - -Counter.propTypes = counterPropTypes - -export default Counter -``` From 11533897853cb8e43b4ec1763549e20bc2ae61d7 Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Mon, 11 Apr 2016 22:37:58 -0400 Subject: [PATCH 08/15] refactor(server): Use config path helpers --- server/main.js | 2 +- server/middleware/webpack-dev.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/main.js b/server/main.js index 1a0116ab4..5634833ac 100644 --- a/server/main.js +++ b/server/main.js @@ -54,7 +54,7 @@ if (config.env === 'development') { // Serving ~/dist by default. Ideally these files should be served by // the web server and not the app server, but this helps to demo the // server in production. - app.use(convert(serve(paths.base(config.dir_dist)))) + app.use(convert(serve(paths.dist()))) } export default app diff --git a/server/middleware/webpack-dev.js b/server/middleware/webpack-dev.js index 806996f6b..8187ba646 100644 --- a/server/middleware/webpack-dev.js +++ b/server/middleware/webpack-dev.js @@ -11,7 +11,7 @@ export default function (compiler, publicPath) { const middleware = WebpackDevMiddleware(compiler, { publicPath, - contentBase: paths.base(config.dir_client), + contentBase: paths.client(), hot: true, quiet: config.compiler_quiet, noInfo: config.compiler_quiet, From 16fc80102f0848ca9e2ab9908f3200b5e6be4b4c Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Mon, 11 Apr 2016 22:39:26 -0400 Subject: [PATCH 09/15] chore(components): Own directories --- src/components/.gitkeep | 0 src/components/{ => Counter}/Counter.js | 0 src/components/{ => Counter}/Counter.scss | 0 src/components/Counter/index.js | 3 +++ src/components/{ => Header}/Header.js | 0 src/components/{ => Header}/Header.scss | 0 src/components/Header/index.js | 3 +++ tests/components/{ => Counter}/Counter.spec.js | 2 +- tests/components/{ => Header}/Header.spec.js | 2 +- 9 files changed, 8 insertions(+), 2 deletions(-) delete mode 100644 src/components/.gitkeep rename src/components/{ => Counter}/Counter.js (100%) rename src/components/{ => Counter}/Counter.scss (100%) create mode 100644 src/components/Counter/index.js rename src/components/{ => Header}/Header.js (100%) rename src/components/{ => Header}/Header.scss (100%) create mode 100644 src/components/Header/index.js rename tests/components/{ => Counter}/Counter.spec.js (97%) rename tests/components/{ => Header}/Header.spec.js (93%) diff --git a/src/components/.gitkeep b/src/components/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/components/Counter.js b/src/components/Counter/Counter.js similarity index 100% rename from src/components/Counter.js rename to src/components/Counter/Counter.js diff --git a/src/components/Counter.scss b/src/components/Counter/Counter.scss similarity index 100% rename from src/components/Counter.scss rename to src/components/Counter/Counter.scss diff --git a/src/components/Counter/index.js b/src/components/Counter/index.js new file mode 100644 index 000000000..ef982b942 --- /dev/null +++ b/src/components/Counter/index.js @@ -0,0 +1,3 @@ +import Counter from './Counter' + +export default Counter diff --git a/src/components/Header.js b/src/components/Header/Header.js similarity index 100% rename from src/components/Header.js rename to src/components/Header/Header.js diff --git a/src/components/Header.scss b/src/components/Header/Header.scss similarity index 100% rename from src/components/Header.scss rename to src/components/Header/Header.scss diff --git a/src/components/Header/index.js b/src/components/Header/index.js new file mode 100644 index 000000000..7cd29d7ab --- /dev/null +++ b/src/components/Header/index.js @@ -0,0 +1,3 @@ +import Header from './Header' + +export default Header diff --git a/tests/components/Counter.spec.js b/tests/components/Counter/Counter.spec.js similarity index 97% rename from tests/components/Counter.spec.js rename to tests/components/Counter/Counter.spec.js index 7f1477820..266fe8564 100644 --- a/tests/components/Counter.spec.js +++ b/tests/components/Counter/Counter.spec.js @@ -1,6 +1,6 @@ import React from 'react' import { bindActionCreators } from 'redux' -import { Counter } from 'components/Counter' +import { Counter } from 'components/Counter/Counter' import { shallow } from 'enzyme' describe('(Component) Counter', () => { diff --git a/tests/components/Header.spec.js b/tests/components/Header/Header.spec.js similarity index 93% rename from tests/components/Header.spec.js rename to tests/components/Header/Header.spec.js index dbf37306c..00c5c77b3 100644 --- a/tests/components/Header.spec.js +++ b/tests/components/Header/Header.spec.js @@ -1,5 +1,5 @@ import React from 'react' -import { Header } from 'components/Header' +import { Header } from 'components/Header/Header' import { IndexLink, Link } from 'react-router' import { shallow } from 'enzyme' From 99dd0b773a581b2a89da884903697a094692925a Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Mon, 11 Apr 2016 22:41:54 -0400 Subject: [PATCH 10/15] refactor(counter): remove CounterView --- src/routes/Counter/components/CounterView.js | 24 ------------------- .../Counter/containers/CounterContainer.js | 2 +- src/routes/Counter/index.js | 24 +++++-------------- 3 files changed, 7 insertions(+), 43 deletions(-) delete mode 100644 src/routes/Counter/components/CounterView.js diff --git a/src/routes/Counter/components/CounterView.js b/src/routes/Counter/components/CounterView.js deleted file mode 100644 index 2271c90c6..000000000 --- a/src/routes/Counter/components/CounterView.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react' -import Helmet from 'react-helmet' -import CounterContainer from '../containers/CounterContainer' - -/* Entry point for Counter route - useful for setting up layout, - document title (see below) as well as rendering named components - Can be removed, and route can render container directly.*/ - -const CounterView = (props) => ( -
- - -
-) - -/* Note: Since this is a 'top level' route, we are passing the `props` - object to the container component, which incudes some useful helpers - provided by `react-router`, ie `params` and `route` which can then - be accessed using the second param of mapStateToProps(state, *ownProps*) - - In this starter-kit, we are not using this functionality, but it is - a good thing to keep in mind. */ - -export default CounterView diff --git a/src/routes/Counter/containers/CounterContainer.js b/src/routes/Counter/containers/CounterContainer.js index c04388a73..d0fe41a32 100644 --- a/src/routes/Counter/containers/CounterContainer.js +++ b/src/routes/Counter/containers/CounterContainer.js @@ -17,7 +17,7 @@ const mapActionCreators = { doubleAsync } -const mapStateToProps = (state, ownProps) => ({ +const mapStateToProps = (state) => ({ counter: state.counter }) diff --git a/src/routes/Counter/index.js b/src/routes/Counter/index.js index d213a2baf..f7e7435e0 100644 --- a/src/routes/Counter/index.js +++ b/src/routes/Counter/index.js @@ -3,30 +3,18 @@ import { injectReducer } from '../../store/reducers' export default (store) => ({ path: 'counter', getComponents (location, next) { - // Define route dependencies for bundling require.ensure([ './containers/CounterContainer', - './components/CounterView', './modules/counter' ], (require) => { - // These modules can be lazily evaluated using require hook, and - // will not loaded until the router invokes this callback. Webpack - // detects this and creates a split point. - const CounterView = require('./components/CounterView').default - const counterReducer = require('./modules/counter').default + /* These modules are lazily evaluated using require hook, and + will not loaded until the router invokes this callback. */ + const Counter = require('./containers/CounterContainer').default + const reducer = require('./modules/counter').default - // Inject our reducer into the store - injectReducer({ - store, - key: 'counter', - reducer: counterReducer - }) + injectReducer(store, { key: 'counter', reducer }) - // Render The CounterView (our 'main' route component) - next(null, CounterView) - - // Note: If you do not need any route-specific layout (or helpers, - // ie. doc title), you can pass a container directly to `next`. + next(null, Counter) }) } }) From f8f9770ca4ab78ff7a8212c24f8ce52bd273de9c Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Mon, 11 Apr 2016 22:43:58 -0400 Subject: [PATCH 11/15] refactor(main): Move routes into app entry --- src/main.js | 42 ++++++++++++++------------ src/routes/Counter/modules/counter.js | 28 ++++++++++------- src/routes/Home/components/HomeView.js | 5 ++- src/routes/NotFound/NotFound.js | 8 ++--- src/routes/Root.js | 32 -------------------- src/routes/index.js | 15 +++++---- 6 files changed, 53 insertions(+), 77 deletions(-) delete mode 100644 src/routes/Root.js diff --git a/src/main.js b/src/main.js index f2a5b2ebd..10e6c20d5 100644 --- a/src/main.js +++ b/src/main.js @@ -1,13 +1,12 @@ import React from 'react' import ReactDOM from 'react-dom' import createBrowserHistory from 'history/lib/createBrowserHistory' -import { useRouterHistory } from 'react-router' +import { Router, useRouterHistory } from 'react-router' import { syncHistoryWithStore } from 'react-router-redux' -import Root from './routes/Root' import createStore from './store/createStore' +import { Provider } from 'react-redux' const MOUNT_ELEMENT = document.getElementById('root') -const DEFAULT_TITLE = 'React Redux Starter Kit' // Configure history for react-router const browserHistory = useRouterHistory(createBrowserHistory)({ @@ -23,32 +22,30 @@ const history = syncHistoryWithStore(browserHistory, store, { selectLocationState: (state) => state.router }) -// Default titles and nested template (react-helmet) -const title = { - defaultTitle: DEFAULT_TITLE, - titleTemplate: `%s - ${DEFAULT_TITLE}` +let render = (key = null) => { + const routes = require('./routes/index').default(store) + const App = ( + +
+ +
+
+ ) + ReactDOM.render(App, MOUNT_ELEMENT) } -let render = () => { - // This syntax will be updated in the near future to use System loader - const createRoutes = require('./routes/index').default - const routes = createRoutes(store) - const props = { history, routes, store, title } - - ReactDOM.render(, MOUNT_ELEMENT) -} - -// If supported, set up hot reloading and overlay for runtime errors -if (module.hot) { +// Enable HMR and catch runtime errors in RedBox +// This code is excluded from production bundle +if (__DEV__ && module.hot) { const renderApp = render const renderError = (error) => { const RedBox = require('redbox-react') - ReactDOM.render(, MOUNT_ELEMENT) + ReactDOM.render(, MOUNT_ELEMENT) } render = () => { try { - renderApp() + renderApp(Math.random()) } catch (error) { renderError(error) } @@ -56,4 +53,9 @@ if (module.hot) { module.hot.accept(['./routes/index'], () => render()) } +// Use Redux DevTools chrome extension +if (__DEBUG__) { + if (!window.devToolsExtension) window.devToolsExtension.open() +} + render() diff --git a/src/routes/Counter/modules/counter.js b/src/routes/Counter/modules/counter.js index 423a11cec..9cd0e2a8a 100644 --- a/src/routes/Counter/modules/counter.js +++ b/src/routes/Counter/modules/counter.js @@ -7,11 +7,15 @@ export const COUNTER_INCREMENT = 'COUNTER_INCREMENT' // ------------------------------------ // Actions // ------------------------------------ -// NOTE: "Action" is a Flow interface defined in https://github.com/TechnologyAdvice/flow-interfaces -// If you're unfamiliar with Flow, you are completely welcome to avoid annotating your code, but -// if you'd like to learn more you can check out: flowtype.org. -// DOUBLE NOTE: there is currently a bug with babel-eslint where a `space-infix-ops` error is -// incorrectly thrown when using arrow functions, hence the oddity. + +/* NOTE: "Action" is a Flow interface defined in https://github.com/TechnologyAdvice/flow-interfaces + If you're unfamiliar with Flow, you are completely welcome to avoid + annotating your code, but if you'd like to learn more you can + check out: flowtype.org. + + NOTE: There is currently a bug with babel-eslint where a `space-infix-ops` + error is incorrectly thrown when using arrow functions, hence the oddity. */ + export function increment (value: number = 1): Action { return { type: COUNTER_INCREMENT, @@ -19,12 +23,14 @@ export function increment (value: number = 1): Action { } } -// This is a thunk, meaning it is a function that immediately -// returns a function for lazy evaluation. It is incredibly useful for -// creating async actions, especially when combined with redux-thunk! -// NOTE: This is solely for demonstration purposes. In a real application, -// you'd probably want to dispatch an action of COUNTER_DOUBLE and let the -// reducer take care of this logic. +/* This is a thunk, meaning it is a function that immediately + returns a function for lazy evaluation. It is incredibly useful for + creating async actions, especially when combined with redux-thunk! + + NOTE: This is solely for demonstration purposes. In a real application, + you'd probably want to dispatch an action of COUNTER_DOUBLE and let the + reducer take care of this logic. */ + export const doubleAsync = (): Function => { return (dispatch: Function, getState: Function): Promise => { return new Promise((resolve: Function): void => { diff --git a/src/routes/Home/components/HomeView.js b/src/routes/Home/components/HomeView.js index c8ccf6901..f1f6b96ac 100644 --- a/src/routes/Home/components/HomeView.js +++ b/src/routes/Home/components/HomeView.js @@ -5,7 +5,10 @@ import classes from './HomeView.scss' export const HomeView = () => (

Welcome!

- This is a duck, because Redux! + This is a duck, because Redux!
) diff --git a/src/routes/NotFound/NotFound.js b/src/routes/NotFound/NotFound.js index e6c3900b4..44b4bf267 100644 --- a/src/routes/NotFound/NotFound.js +++ b/src/routes/NotFound/NotFound.js @@ -1,17 +1,15 @@ import React from 'react' import { browserHistory } from 'react-router' -import Helmet from 'react-helmet' const goBack = (e) => { e.preventDefault() return browserHistory.goBack() } -const NotFound = () => ( +export const NotFound = () => (
- -

Page Not Found!

- Go Back +

Page not found!

+

← Back

) diff --git a/src/routes/Root.js b/src/routes/Root.js deleted file mode 100644 index d7961b2c0..000000000 --- a/src/routes/Root.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react' -import { Provider } from 'react-redux' -import { Router } from 'react-router' -import Helmet from 'react-helmet' - -const Root = (props) => ( - -
- - - {props.routes} - -
-
-) - -Root.propTypes = { - history: React.PropTypes.object.isRequired, - routes: React.PropTypes.object.isRequired, - store: React.PropTypes.object.isRequired, - title: React.PropTypes.shape({ - defaultTitle: React.PropTypes.string.isRequired, - titleTemplate: React.PropTypes.string.isRequired - }).isRequired -} - -// Use Redux DevTools chrome extension -if (__DEBUG__) { - if (!window.devToolsExtension) window.devToolsExtension.open() -} - -export default Root diff --git a/src/routes/index.js b/src/routes/index.js index 77048529b..3ac1bf84d 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -2,15 +2,12 @@ import CoreLayout from '../layouts/CoreLayout/CoreLayout' import Home from './Home' -/* Note: Instead of JSX, we are using react-router's PlainRoute, which uses - a simple javascript object to provide route definitions. This keeps things - very explicit, proves there's no magic happening, and is the preferred method - for code splitting. When creating a new route, we provide the instantiated - store so that it can continue to inject dyamically loaded reducers and leverage - tools such as `redux-saga` and `rereduce` */ - export const createRoutes = (store) => { - return { +/* Note: Instead of using JSX, we are using react-router PlainRoute, + a simple javascript object to provide route definitions. + When creating a new async route, pass the instantiated store! */ + + const routes = { path: '/', component: CoreLayout, indexRoute: Home, @@ -24,6 +21,8 @@ export const createRoutes = (store) => { }) } } + + return routes } export default createRoutes From bc7f539eea0bff9d42aad394326f43b515bd708d Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Mon, 11 Apr 2016 22:45:15 -0400 Subject: [PATCH 12/15] test(routes): Add some tests for Home --- src/store/createStore.js | 3 +-- src/store/reducers.js | 7 +++++-- tests/routes/Home/components/HomeView.spec.js | 2 +- tests/routes/Home/index.spec.js | 17 +++++++++++++++++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/store/createStore.js b/src/store/createStore.js index 3ed857953..96309fb0f 100644 --- a/src/store/createStore.js +++ b/src/store/createStore.js @@ -5,9 +5,9 @@ import thunk from 'redux-thunk' import reducers from './reducers' export default (initialState = {}, history) => { - // Compose final middleware and use devtools in debug environment let middleware = applyMiddleware(thunk, routerMiddleware(history)) + // Use DevTools chrome extension in development if (__DEBUG__) { const devToolsExtension = window.devToolsExtension @@ -16,7 +16,6 @@ export default (initialState = {}, history) => { } } - // Create final store and subscribe router in debug env ie. for devtools const store = createStore(reducers(), initialState, middleware) store.asyncReducers = {} diff --git a/src/store/reducers.js b/src/store/reducers.js index ef52b73dc..be35915d5 100644 --- a/src/store/reducers.js +++ b/src/store/reducers.js @@ -2,10 +2,13 @@ import { combineReducers } from 'redux' import { routerReducer as router } from 'react-router-redux' export const reducers = (asyncReducers) => { - return combineReducers({ router, ...asyncReducers }) + return combineReducers({ + // Add sync reducers here + router, + ...asyncReducers }) } -export const injectReducer = ({ store, key, reducer }) => { +export const injectReducer = (store, { key, reducer }) => { store.asyncReducers[key] = reducer store.replaceReducer(reducers(store.asyncReducers)) } diff --git a/tests/routes/Home/components/HomeView.spec.js b/tests/routes/Home/components/HomeView.spec.js index 0193f1316..c59d3c69c 100644 --- a/tests/routes/Home/components/HomeView.spec.js +++ b/tests/routes/Home/components/HomeView.spec.js @@ -2,7 +2,7 @@ import React from 'react' import { HomeView } from 'routes/Home/components/HomeView' import { render } from 'enzyme' -describe('(View) Home', function () { +describe('(View) Home', () => { let _component beforeEach(() => { diff --git a/tests/routes/Home/index.spec.js b/tests/routes/Home/index.spec.js index e69de29bb..60f635417 100644 --- a/tests/routes/Home/index.spec.js +++ b/tests/routes/Home/index.spec.js @@ -0,0 +1,17 @@ +import HomeRoute from 'routes/Home' + +describe('(Route) Home', () => { + let _component + + beforeEach(() => { + _component = HomeRoute.component() + }) + + it('Should return a route configuration object', () => { + expect(typeof(HomeRoute)).to.equal('object') + }) + + it('Should define a route component', () => { + expect(_component.type).to.equal('div') + }) +}) From 735fc9b3e42d2ae92c14e23f6171c9b8139bcb00 Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Mon, 11 Apr 2016 22:48:23 -0400 Subject: [PATCH 13/15] fix(misc): remove duplicate deploy --- package.json | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/package.json b/package.json index ca7f08a9f..8215b0ac5 100644 --- a/package.json +++ b/package.json @@ -58,20 +58,6 @@ "DEBUG": "app:*" } }, - "deploy:dev": { - "command": "npm run deploy", - "env": { - "NODE_ENV": "development", - "DEBUG": "app:*" - } - }, - "deploy:prod": { - "command": "npm run deploy", - "env": { - "NODE_ENV": "production", - "DEBUG": "app:*" - } - }, "start": { "command": "babel-node bin/server", "env": { From 5950392f67cae5ef95a0b5c781e8a6265d8ce786 Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Wed, 13 Apr 2016 20:18:08 -0400 Subject: [PATCH 14/15] chore(router): Update router/async API --- package.json | 2 +- src/routes/Counter/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8215b0ac5..6ca9e13f8 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "react": "^15.0.0", "react-dom": "^15.0.0", "react-redux": "^4.0.0", - "react-router": "^2.0.0", + "react-router": "^2.2.0", "react-router-redux": "^4.0.0", "redux": "^3.0.0", "redux-thunk": "^2.0.0", diff --git a/src/routes/Counter/index.js b/src/routes/Counter/index.js index f7e7435e0..5716563d1 100644 --- a/src/routes/Counter/index.js +++ b/src/routes/Counter/index.js @@ -2,7 +2,7 @@ import { injectReducer } from '../../store/reducers' export default (store) => ({ path: 'counter', - getComponents (location, next) { + getComponent (nextState, next) { require.ensure([ './containers/CounterContainer', './modules/counter' From 80b3d3544d7f96339661828fd0cd9de490df3722 Mon Sep 17 00:00:00 2001 From: Justin Greenberg Date: Tue, 19 Apr 2016 18:42:03 -0400 Subject: [PATCH 15/15] fix(ci): remove redux dir --- src/redux/configureStore.js | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 src/redux/configureStore.js diff --git a/src/redux/configureStore.js b/src/redux/configureStore.js deleted file mode 100644 index 262e7a6ec..000000000 --- a/src/redux/configureStore.js +++ /dev/null @@ -1,27 +0,0 @@ -import { applyMiddleware, compose, createStore } from 'redux' -import thunk from 'redux-thunk' -import rootReducer from './rootReducer' -import { routerMiddleware } from 'react-router-redux' - -export default function configureStore (initialState = {}, history) { - // Compose final middleware and use devtools in debug environment - let middleware = applyMiddleware(thunk, routerMiddleware(history)) - if (__DEBUG__) { - const devTools = window.devToolsExtension - ? window.devToolsExtension() - : require('containers/DevTools').default.instrument() - middleware = compose(middleware, devTools) - } - - // Create final store and subscribe router in debug env ie. for devtools - const store = createStore(rootReducer, initialState, middleware) - - if (module.hot) { - module.hot.accept('./rootReducer', () => { - const nextRootReducer = require('./rootReducer').default - - store.replaceReducer(nextRootReducer) - }) - } - return store -}