├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .reduxrc ├── README.md ├── bin ├── compile.js ├── flow-check.js └── server.js ├── blueprints ├── .eslintrc ├── blueprint │ ├── files │ │ └── blueprints │ │ │ └── __name__ │ │ │ ├── files │ │ │ └── .gitkeep │ │ │ └── index.js │ └── index.js ├── duck │ ├── files │ │ ├── __root__ │ │ │ └── redux │ │ │ │ └── modules │ │ │ │ └── __name__.js │ │ └── __test__ │ │ │ └── redux │ │ │ └── modules │ │ │ └── __name__.spec.js │ └── index.js ├── dumb │ ├── files │ │ ├── __root__ │ │ │ └── components │ │ │ │ └── __name__ │ │ │ │ ├── __name__.js │ │ │ │ └── index.js │ │ └── __test__ │ │ │ └── components │ │ │ └── __name__.spec.js │ └── index.js ├── form │ ├── files │ │ ├── __root__ │ │ │ └── forms │ │ │ │ └── __name__Form │ │ │ │ ├── __name__Form.js │ │ │ │ └── index.js │ │ └── __test__ │ │ │ └── forms │ │ │ └── __name__Form.spec.js │ └── index.js ├── layout │ ├── files │ │ ├── __root__ │ │ │ └── layouts │ │ │ │ └── __name__Layout │ │ │ │ ├── __name__Layout.js │ │ │ │ └── index.js │ │ └── __test__ │ │ │ └── layouts │ │ │ └── __name__Layout.spec.js │ └── index.js ├── smart │ ├── files │ │ ├── __root__ │ │ │ └── containers │ │ │ │ ├── __name__.js │ │ │ │ └── index.js │ │ └── __test__ │ │ │ └── containers │ │ │ └── __name__.spec.js │ └── index.js └── view │ ├── files │ ├── __root__ │ │ └── views │ │ │ └── __name__View │ │ │ ├── __name__View.js │ │ │ └── index.js │ └── __test__ │ │ └── views │ │ └── __name__View.spec.js │ └── index.js ├── build ├── karma.conf.js ├── webpack-compiler.js └── webpack.config.js ├── config ├── _base.js ├── _development.js ├── _production.js └── index.js ├── interfaces ├── image.d.js └── webpack-globals.d.js ├── jsconfig.json ├── nodemon.json ├── package.json ├── server ├── api │ ├── index.js │ ├── todos.js │ └── utils.js ├── lib │ └── apply-express-middleware.js ├── main.js └── middleware │ ├── webpack-dev.js │ └── webpack-hmr.js ├── src ├── components │ └── .gitkeep ├── containers │ ├── DevTools.js │ ├── DevToolsWindow.js │ └── Root.js ├── index.html ├── layouts │ └── CoreLayout │ │ ├── CoreLayout.js │ │ └── index.js ├── main.js ├── redux │ ├── configureStore.js │ ├── modules │ │ ├── asyncTodo │ │ │ ├── asyncTodo.js │ │ │ ├── index.js │ │ │ └── others.js │ │ ├── counter.js │ │ └── todo.js │ ├── rootReducer.js │ └── utils │ │ └── createDevToolsWindow.js ├── routes │ └── index.js ├── static │ ├── favicon.ico │ ├── humans.txt │ └── robots.txt ├── styles │ ├── _base.scss │ ├── core.scss │ └── vendor │ │ └── _normalize.scss └── views │ ├── HomeView │ ├── Duck.jpg │ ├── HomeView.js │ ├── HomeView.scss │ └── index.js │ ├── ReactSample │ ├── TodoItem.js │ ├── TodoList.js │ └── index.js │ ├── ReduxAsyncSample │ ├── ReduxAsyncSample.js │ └── index.js │ └── ReduxSample │ ├── ReduxSample.js │ └── index.js └── tests ├── .eslintrc ├── framework.spec.js ├── layouts └── CoreLayout.spec.js ├── redux └── modules │ └── counter.spec.js ├── test-bundler.js └── views ├── HomeView.spec.js └── ReactSample.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | // NOTE: These options are overriden by the babel-loader configuration 2 | // for webpack, which can be found in ~/build/webpack.config. 3 | // 4 | // Why? The react-transform-hmr plugin depends on HMR (and throws if 5 | // module.hot is disabled), so keeping it and related plugins contained 6 | // within webpack helps prevent unexpected errors. 7 | { 8 | "presets": ["es2015", "react", "stage-0", "babel-preset-power-assert"], 9 | "plugins": ["transform-runtime"] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | [*] 8 | # Indentation style 9 | # Possible values - tab, space 10 | indent_style = space 11 | 12 | # Indentation size in single-spaced characters 13 | # Possible values - an integer, tab 14 | indent_size = 2 15 | 16 | # Line ending file format 17 | # Possible values - lf, crlf, cr 18 | end_of_line = lf 19 | 20 | # File character encoding 21 | # Possible values - latin1, utf-8, utf-16be, utf-16le 22 | charset = utf-8 23 | 24 | # Denotes whether to trim whitespace at the end of lines 25 | # Possible values - true, false 26 | trim_trailing_whitespace = true 27 | 28 | # Denotes whether file should end with a newline 29 | # Possible values - true, false 30 | insert_final_newline = true 31 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | blueprints/**/files/** 2 | coverage/** 3 | node_modules/** 4 | dist/** 5 | *.spec.js 6 | src/index.html 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser" : "babel-eslint", 3 | "extends" : [ 4 | "standard", 5 | "standard-react" 6 | ], 7 | "plugins" : [ 8 | "flow-vars" 9 | ], 10 | "env" : { 11 | "browser" : true 12 | }, 13 | "globals" : { 14 | "Action" : false, 15 | "__DEV__" : false, 16 | "__PROD__" : false, 17 | "__DEBUG__" : false, 18 | "__DEBUG_NEW_WINDOW__" : false, 19 | "__BASENAME__" : false 20 | }, 21 | "rules": { 22 | "semi" : [2, "never"], 23 | "max-len": [2, 120, 2], 24 | "flow-vars/define-flow-type": 1, 25 | "flow-vars/use-flow-type": 1 26 | }, 27 | "ecmaFeatures": { 28 | "experimentalObjectRestSpread": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [version] 2 | 0.23.0 3 | 4 | [ignore] 5 | .*/bin/.* 6 | .*/build/.* 7 | .*/config/.* 8 | .*/coverage/.* 9 | .*/node_modules/.* 10 | .*/src/styles/.* 11 | 12 | [include] 13 | ./node_modules/ 14 | 15 | [libs] 16 | ./node_modules/flow-interfaces/interfaces/ 17 | ./interfaces/ 18 | 19 | [options] 20 | module.system=haste 21 | esproposal.class_static_fields=enable 22 | esproposal.class_instance_fields=enable 23 | esproposal.decorators=ignore 24 | 25 | module.name_mapper='.*\.\(scss\|css\)$' -> 'CSSModule' 26 | module.name_mapper='.*\.\(jpg\|jpeg\|gif\|svg\|png\)$' -> 'Image' 27 | module.name_mapper='^components\/\(.*\)$' -> '/src/components/\1' 28 | module.name_mapper='^components$' -> '/src/components' 29 | module.name_mapper='^containers\/\(.*\)$' -> '/src/containers/\1' 30 | module.name_mapper='^containers$' -> '/src/containers' 31 | module.name_mapper='^layouts\/\(.*\)$' -> '/src/layouts/\1' 32 | module.name_mapper='^layouts$' -> '/src/layouts' 33 | module.name_mapper='^redux\/\(.*\)$' -> '/src/redux/\1' 34 | module.name_mapper='^routes\/\(.*\)$' -> '/src/routes/\1' 35 | module.name_mapper='^routes$' -> '/src/routes' 36 | module.name_mapper='^styles\/\(.*\)$' -> '/src/styles/\1' 37 | module.name_mapper='^utils\/\(.*\)$' -> '/src/utils/\1' 38 | module.name_mapper='^utils$' -> '/src/utils' 39 | module.name_mapper='^views\/\(.*\)$' -> '/src/views/\1' 40 | module.name_mapper='^views$' -> '/src/views' 41 | 42 | munge_underscores=true 43 | 44 | suppress_type=$FlowIssue 45 | suppress_type=$FlowFixMe 46 | suppress_type=$FixMe 47 | 48 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-6]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*www[a-z,_]*\\)?)\\) 49 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-6]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*www[a-z,_]*\\)?)\\)? #[0-9]+ 50 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.log 3 | 4 | node_modules 5 | 6 | dist 7 | coverage 8 | -------------------------------------------------------------------------------- /.reduxrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceBase":"src", 3 | "testBase":"tests", 4 | "smartPath":"containers", 5 | "dumbPath":"components", 6 | "fileCasing":"pascal" 7 | } 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React Redux Introduction 2 | ======================= 3 | 4 | forked from https://github.com/davezuko/react-redux-starter-kit, v2.0.0-alpha5 5 | 6 | Usage 7 | ----- 8 | 9 | ``` 10 | cd your/working/directory 11 | git clone https://github.com/adwd/react-redux-introduction 12 | npm i 13 | npm run dev 14 | open http://localhost:3000 15 | ``` 16 | -------------------------------------------------------------------------------- /bin/compile.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import _debug from 'debug' 3 | import webpackCompiler from '../build/webpack-compiler' 4 | import webpackConfig from '../build/webpack.config' 5 | import config from '../config' 6 | 7 | const debug = _debug('app:bin:compile') 8 | const paths = config.utils_paths 9 | 10 | ;(async function () { 11 | try { 12 | debug('Run compiler') 13 | const stats = await webpackCompiler(webpackConfig) 14 | if (stats.warnings.length && config.compiler_fail_on_warning) { 15 | debug('Config set to fail on warning, exiting with status code "1".') 16 | process.exit(1) 17 | } 18 | debug('Copy static assets to dist folder.') 19 | fs.copySync(paths.client('static'), paths.dist()) 20 | } catch (e) { 21 | debug('Compiler encountered an error.', e) 22 | process.exit(1) 23 | } 24 | })() 25 | -------------------------------------------------------------------------------- /bin/flow-check.js: -------------------------------------------------------------------------------- 1 | import cp from 'child_process' 2 | import flowBin from 'flow-bin' 3 | 4 | try { 5 | cp.execFileSync(flowBin, ['check'], {stdio: 'inherit'}) 6 | } catch (e) { 7 | process.exit(1) 8 | } 9 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | import config from '../config' 2 | import server from '../server/main' 3 | import _debug from 'debug' 4 | 5 | const debug = _debug('app:bin:server') 6 | const port = config.server_port 7 | const host = config.server_host 8 | 9 | server.listen(port) 10 | debug(`Server is now running at http://${host}:${port}.`) 11 | -------------------------------------------------------------------------------- /blueprints/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../.eslintrc", 3 | "env" : { 4 | "mocha" : true 5 | }, 6 | "globals" : { 7 | "expect" : false, 8 | "should" : false, 9 | "sinon" : false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /blueprints/blueprint/files/blueprints/__name__/files/.gitkeep: -------------------------------------------------------------------------------- 1 | put your files here 2 | -------------------------------------------------------------------------------- /blueprints/blueprint/files/blueprints/__name__/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | locals: function (options) { 5 | // Return custom template variables here. 6 | return {} 7 | }, 8 | 9 | fileMapTokens: function (options) { 10 | // Return custom tokens to be replaced in your files 11 | return { 12 | __token__: function (options) { 13 | // logic to determine value goes here 14 | return 'value' 15 | } 16 | } 17 | }, 18 | 19 | // Should probably never need to be overriden 20 | 21 | filesPath: function () { 22 | return path.join(this.path, 'files') 23 | }, 24 | 25 | beforeInstall: function (options) {}, 26 | afterInstall: function (options) {} 27 | } 28 | -------------------------------------------------------------------------------- /blueprints/blueprint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description () { 3 | return 'generates a blueprint and definition' 4 | }, 5 | 6 | beforeInstall () { 7 | console.log('Before installation hook!') 8 | }, 9 | 10 | afterInstall () { 11 | console.log('After installation hook!') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /blueprints/duck/files/__root__/redux/modules/__name__.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | // export const constants = { } 3 | 4 | // Action Creators 5 | // export const actions = { } 6 | 7 | // Reducer 8 | export const initialState = {} 9 | export default function (state = initialState, action) { 10 | switch (action.type) { 11 | default: 12 | return state 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /blueprints/duck/files/__test__/redux/modules/__name__.spec.js: -------------------------------------------------------------------------------- 1 | import reducer, { initialState } from 'redux/modules/<%= pascalEntityName %>' 2 | 3 | describe('(Redux) <%= pascalEntityName %>', () => { 4 | describe('(Reducer)', () => { 5 | it('sets up initial state', () => { 6 | expect(reducer(undefined, {})).to.eql(initialState) 7 | }) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /blueprints/duck/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description () { 3 | return 'generates a redux duck' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /blueprints/dumb/files/__root__/components/__name__/__name__.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = { 4 | 5 | }; 6 | export class <%= pascalEntityName %> extends React.Component { 7 | props: Props; 8 | 9 | render () { 10 | return ( 11 |
12 | ) 13 | } 14 | } 15 | 16 | export default <%= pascalEntityName %> 17 | 18 | -------------------------------------------------------------------------------- /blueprints/dumb/files/__root__/components/__name__/index.js: -------------------------------------------------------------------------------- 1 | import <%= pascalEntityName %> from './<%= pascalEntityName %>' 2 | export default <%= pascalEntityName %> 3 | -------------------------------------------------------------------------------- /blueprints/dumb/files/__test__/components/__name__.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import <%= pascalEntityName %> from 'components/<%= pascalEntityName %>/<%= pascalEntityName %>' 3 | 4 | describe('(Component) <%= pascalEntityName %>', () => { 5 | it('should exist', () => { 6 | 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /blueprints/dumb/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description () { 3 | return 'generates a dumb (pure) component' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /blueprints/form/files/__root__/forms/__name__Form/__name__Form.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { reduxForm } from 'redux-form' 3 | 4 | export const fields = [] 5 | 6 | const validate = (values) => { 7 | const errors = {} 8 | return errors 9 | } 10 | 11 | type Props = { 12 | handleSubmit: Function, 13 | fields: Object, 14 | } 15 | export class <%= pascalEntityName %> extends React.Component { 16 | props: Props; 17 | 18 | defaultProps = { 19 | fields: {}, 20 | } 21 | 22 | render() { 23 | const { fields, handleSubmit } = this.props 24 | 25 | return ( 26 |
27 |
28 | ) 29 | } 30 | } 31 | 32 | <%= pascalEntityName %> = reduxForm({ 33 | form: '<%= pascalEntityName %>', 34 | fields, 35 | validate 36 | })(<%= pascalEntityName %>) 37 | 38 | export default <%= pascalEntityName %> 39 | -------------------------------------------------------------------------------- /blueprints/form/files/__root__/forms/__name__Form/index.js: -------------------------------------------------------------------------------- 1 | import <%= pascalEntityName %>Form from './<%= pascalEntityName %>Form' 2 | export default <%= pascalEntityName %>Form 3 | -------------------------------------------------------------------------------- /blueprints/form/files/__test__/forms/__name__Form.spec.js: -------------------------------------------------------------------------------- 1 | describe('(Form) <%= pascalEntityName %>', () => { 2 | it('exists', () => { 3 | 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /blueprints/form/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description () { 3 | return 'generates a connected redux-form form component' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /blueprints/layout/files/__root__/layouts/__name__Layout/__name__Layout.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | function <%= pascalEntityName %> ({ children }) { 4 | return ( 5 |
6 | {children} 7 |
8 | ) 9 | } 10 | 11 | <%= pascalEntityName %>.propTypes = { 12 | children: PropTypes.element 13 | } 14 | 15 | export default <%= pascalEntityName %> 16 | -------------------------------------------------------------------------------- /blueprints/layout/files/__root__/layouts/__name__Layout/index.js: -------------------------------------------------------------------------------- 1 | import <%= pascalEntityName %>Layout from './<%= pascalEntityName %>Layout' 2 | export default <%= pascalEntityName %>Layout 3 | -------------------------------------------------------------------------------- /blueprints/layout/files/__test__/layouts/__name__Layout.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | describe('(Layout) <%= pascalEntityName %>', () => { 4 | it('should exist', () => { 5 | 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /blueprints/layout/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description () { 3 | return 'generates a functional layout component' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /blueprints/smart/files/__root__/containers/__name__.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | 5 | type Props = { 6 | 7 | } 8 | export class <%= pascalEntityName %> extends React.Component { 9 | props: Props; 10 | 11 | render() { 12 | return ( 13 |
14 | ) 15 | } 16 | } 17 | 18 | const mapStateToProps = (state) => { 19 | return {} 20 | } 21 | const mapDispatchToProps = (dispatch) => { 22 | return {} 23 | } 24 | 25 | export default connect( 26 | mapStateToProps, 27 | mapDispatchToProps 28 | )(<%= pascalEntityName %>) 29 | -------------------------------------------------------------------------------- /blueprints/smart/files/__root__/containers/index.js: -------------------------------------------------------------------------------- 1 | import <%= pascalEntityName %> from './<%= pascalEntityName %>' 2 | export default <%= pascalEntityName %> 3 | -------------------------------------------------------------------------------- /blueprints/smart/files/__test__/containers/__name__.spec.js: -------------------------------------------------------------------------------- 1 | describe('(Component) <%= pascalEntityName %>', () => { 2 | it('exists', () => { 3 | 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /blueprints/smart/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description () { 3 | return 'generates a smart (container) component' 4 | }, 5 | 6 | fileMapTokens () { 7 | return { 8 | __smart__: (options) => { 9 | return options.settings.getSetting('smartPath') 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /blueprints/view/files/__root__/views/__name__View/__name__View.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = { 4 | 5 | }; 6 | export class <%= pascalEntityName %> extends React.Component { 7 | props: Props; 8 | 9 | render () { 10 | return ( 11 |
12 | ) 13 | } 14 | } 15 | 16 | export default <%= pascalEntityName %> 17 | -------------------------------------------------------------------------------- /blueprints/view/files/__root__/views/__name__View/index.js: -------------------------------------------------------------------------------- 1 | import <%= pascalEntityName %>View from './<%= pascalEntityName %>View' 2 | export default <%= pascalEntityName %>View 3 | -------------------------------------------------------------------------------- /blueprints/view/files/__test__/views/__name__View.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | describe('(View) <%= pascalEntityName %>', () => { 4 | it('should exist', () => { 5 | 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /blueprints/view/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description () { 3 | return 'generates a view component' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /build/karma.conf.js: -------------------------------------------------------------------------------- 1 | import { argv } from 'yargs' 2 | import config from '../config' 3 | import webpackConfig from './webpack.config' 4 | import _debug from 'debug' 5 | 6 | const debug = _debug('app:karma') 7 | debug('Create configuration.') 8 | 9 | const karmaConfig = { 10 | basePath: '../', // project root in relation to bin/karma.js 11 | files: [ 12 | './node_modules/phantomjs-polyfill/bind-polyfill.js', 13 | { 14 | pattern: `./${config.dir_test}/test-bundler.js`, 15 | watched: false, 16 | served: true, 17 | included: true 18 | } 19 | ], 20 | singleRun: !argv.watch, 21 | frameworks: ['mocha'], 22 | reporters: ['mocha'], 23 | preprocessors: { 24 | [`${config.dir_test}/test-bundler.js`]: ['webpack'] 25 | }, 26 | browsers: ['PhantomJS'], 27 | webpack: { 28 | devtool: 'cheap-module-source-map', 29 | resolve: { 30 | ...webpackConfig.resolve, 31 | alias: { 32 | ...webpackConfig.resolve.alias, 33 | sinon: 'sinon/pkg/sinon.js' 34 | } 35 | }, 36 | plugins: webpackConfig.plugins, 37 | module: { 38 | noParse: [ 39 | /\/sinon\.js/ 40 | ], 41 | loaders: webpackConfig.module.loaders.concat([ 42 | { 43 | test: /sinon(\\|\/)pkg(\\|\/)sinon\.js/, 44 | loader: 'imports?define=>false,require=>false' 45 | } 46 | ]) 47 | }, 48 | // Enzyme fix, see: 49 | // https://github.com/airbnb/enzyme/issues/47 50 | externals: { 51 | ...webpackConfig.externals, 52 | 'react/addons': true, 53 | 'react/lib/ExecutionEnvironment': true, 54 | 'react/lib/ReactContext': 'window' 55 | }, 56 | sassLoader: webpackConfig.sassLoader 57 | }, 58 | webpackMiddleware: { 59 | noInfo: true 60 | }, 61 | coverageReporter: { 62 | reporters: config.coverage_reporters 63 | } 64 | } 65 | 66 | if (config.coverage_enabled) { 67 | karmaConfig.reporters.push('coverage') 68 | karmaConfig.webpack.module.preLoaders = [{ 69 | test: /\.(js|jsx)$/, 70 | include: new RegExp(config.dir_client), 71 | loader: 'isparta', 72 | exclude: /node_modules/ 73 | }] 74 | } 75 | 76 | // cannot use `export default` because of Karma. 77 | module.exports = (cfg) => cfg.set(karmaConfig) 78 | -------------------------------------------------------------------------------- /build/webpack-compiler.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import _debug from 'debug' 3 | import config from '../config' 4 | 5 | const debug = _debug('app:build:webpack-compiler') 6 | const DEFAULT_STATS_FORMAT = config.compiler_stats 7 | 8 | export default function webpackCompiler (webpackConfig, statsFormat = DEFAULT_STATS_FORMAT) { 9 | return new Promise((resolve, reject) => { 10 | const compiler = webpack(webpackConfig) 11 | 12 | compiler.run((err, stats) => { 13 | const jsonStats = stats.toJson() 14 | 15 | debug('Webpack compile completed.') 16 | debug(stats.toString(statsFormat)) 17 | 18 | if (err) { 19 | debug('Webpack compiler encountered a fatal error.', err) 20 | return reject(err) 21 | } else if (jsonStats.errors.length > 0) { 22 | debug('Webpack compiler encountered errors.') 23 | debug(jsonStats.errors.join('\n')) 24 | return reject(new Error('Webpack compiler encountered errors')) 25 | } else if (jsonStats.warnings.length > 0) { 26 | debug('Webpack compiler encountered warnings.') 27 | debug(jsonStats.warnings.join('\n')) 28 | } else { 29 | debug('No errors or warnings encountered.') 30 | } 31 | resolve(jsonStats) 32 | }) 33 | }) 34 | } 35 | 36 | -------------------------------------------------------------------------------- /build/webpack.config.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import cssnano from 'cssnano' 3 | import HtmlWebpackPlugin from 'html-webpack-plugin' 4 | import ExtractTextPlugin from 'extract-text-webpack-plugin' 5 | import config from '../config' 6 | import _debug from 'debug' 7 | 8 | const debug = _debug('app:webpack:config') 9 | const paths = config.utils_paths 10 | const {__DEV__, __PROD__, __TEST__} = config.globals 11 | 12 | debug('Create configuration.') 13 | const webpackConfig = { 14 | name: 'client', 15 | target: 'web', 16 | devtool: config.compiler_devtool, 17 | resolve: { 18 | root: paths.base(config.dir_client), 19 | extensions: ['', '.js', '.jsx', '.json'] 20 | }, 21 | module: {} 22 | } 23 | // ------------------------------------ 24 | // Entry Points 25 | // ------------------------------------ 26 | const APP_ENTRY_PATH = paths.base(config.dir_client) + '/main.js' 27 | 28 | webpackConfig.entry = { 29 | app: __DEV__ 30 | ? [APP_ENTRY_PATH, `webpack-hot-middleware/client?path=${config.compiler_public_path}__webpack_hmr`] 31 | : [APP_ENTRY_PATH], 32 | vendor: config.compiler_vendor 33 | } 34 | 35 | // ------------------------------------ 36 | // Bundle Output 37 | // ------------------------------------ 38 | webpackConfig.output = { 39 | filename: `[name].[${config.compiler_hash_type}].js`, 40 | path: paths.base(config.dir_dist), 41 | publicPath: config.compiler_public_path 42 | } 43 | 44 | // ------------------------------------ 45 | // Plugins 46 | // ------------------------------------ 47 | webpackConfig.plugins = [ 48 | new webpack.DefinePlugin(config.globals), 49 | new HtmlWebpackPlugin({ 50 | template: paths.client('index.html'), 51 | hash: false, 52 | favicon: paths.client('static/favicon.ico'), 53 | filename: 'index.html', 54 | inject: 'body', 55 | minify: { 56 | collapseWhitespace: true 57 | } 58 | }) 59 | ] 60 | 61 | if (__DEV__) { 62 | debug('Enable plugins for live development (HMR, NoErrors).') 63 | webpackConfig.plugins.push( 64 | new webpack.HotModuleReplacementPlugin(), 65 | new webpack.NoErrorsPlugin() 66 | ) 67 | } else if (__PROD__) { 68 | debug('Enable plugins for production (OccurenceOrder, Dedupe & UglifyJS).') 69 | webpackConfig.plugins.push( 70 | new webpack.optimize.OccurrenceOrderPlugin(), 71 | new webpack.optimize.DedupePlugin(), 72 | new webpack.optimize.UglifyJsPlugin({ 73 | compress: { 74 | unused: true, 75 | dead_code: true, 76 | warnings: false 77 | } 78 | }) 79 | ) 80 | } 81 | 82 | // Don't split bundles during testing, since we only want import one bundle 83 | if (!__TEST__) { 84 | webpackConfig.plugins.push(new webpack.optimize.CommonsChunkPlugin({ 85 | names: ['vendor'] 86 | })) 87 | } 88 | 89 | // ------------------------------------ 90 | // Pre-Loaders 91 | // ------------------------------------ 92 | /* 93 | [ NOTE ] 94 | We no longer use eslint-loader due to it severely impacting build 95 | times for larger projects. `npm run lint` still exists to aid in 96 | deploy processes (such as with CI), and it's recommended that you 97 | use a linting plugin for your IDE in place of this loader. 98 | 99 | If you do wish to continue using the loader, you can uncomment 100 | the code below and run `npm i --save-dev eslint-loader`. This code 101 | will be removed in a future release. 102 | 103 | webpackConfig.module.preLoaders = [{ 104 | test: /\.(js|jsx)$/, 105 | loader: 'eslint', 106 | exclude: /node_modules/ 107 | }] 108 | 109 | webpackConfig.eslint = { 110 | configFile: paths.base('.eslintrc'), 111 | emitWarning: __DEV__ 112 | } 113 | */ 114 | 115 | // ------------------------------------ 116 | // Loaders 117 | // ------------------------------------ 118 | // JavaScript / JSON 119 | webpackConfig.module.loaders = [{ 120 | test: /\.(js|jsx)$/, 121 | exclude: /node_modules/, 122 | loader: 'babel', 123 | query: { 124 | cacheDirectory: true, 125 | plugins: ['transform-runtime'], 126 | presets: ['es2015', 'react', 'stage-0'], 127 | env: { 128 | development: { 129 | plugins: [ 130 | ['react-transform', { 131 | transforms: [{ 132 | transform: 'react-transform-hmr', 133 | imports: ['react'], 134 | locals: ['module'] 135 | }, { 136 | transform: 'react-transform-catch-errors', 137 | imports: ['react', 'redbox-react'] 138 | }] 139 | }] 140 | ] 141 | }, 142 | production: { 143 | plugins: [ 144 | 'transform-react-remove-prop-types', 145 | 'transform-react-constant-elements' 146 | ] 147 | } 148 | } 149 | } 150 | }, 151 | { 152 | test: /\.json$/, 153 | loader: 'json' 154 | }] 155 | 156 | // ------------------------------------ 157 | // Style Loaders 158 | // ------------------------------------ 159 | // We use cssnano with the postcss loader, so we tell 160 | // css-loader not to duplicate minimization. 161 | const BASE_CSS_LOADER = 'css?sourceMap&-minimize' 162 | 163 | // Add any packge names here whose styles need to be treated as CSS modules. 164 | // These paths will be combined into a single regex. 165 | const PATHS_TO_TREAT_AS_CSS_MODULES = [ 166 | // 'react-toolbox', (example) 167 | ] 168 | 169 | // If config has CSS modules enabled, treat this project's styles as CSS modules. 170 | if (config.compiler_css_modules) { 171 | PATHS_TO_TREAT_AS_CSS_MODULES.push( 172 | paths.base(config.dir_client).replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&') 173 | ) 174 | } 175 | 176 | const isUsingCSSModules = !!PATHS_TO_TREAT_AS_CSS_MODULES.length 177 | const cssModulesRegex = new RegExp(`(${PATHS_TO_TREAT_AS_CSS_MODULES.join('|')})`) 178 | 179 | // Loaders for styles that need to be treated as CSS modules. 180 | if (isUsingCSSModules) { 181 | const cssModulesLoader = [ 182 | BASE_CSS_LOADER, 183 | 'modules', 184 | 'importLoaders=1', 185 | 'localIdentName=[name]__[local]___[hash:base64:5]' 186 | ].join('&') 187 | 188 | webpackConfig.module.loaders.push({ 189 | test: /\.scss$/, 190 | include: cssModulesRegex, 191 | loaders: [ 192 | 'style', 193 | cssModulesLoader, 194 | 'postcss', 195 | 'sass?sourceMap' 196 | ] 197 | }) 198 | 199 | webpackConfig.module.loaders.push({ 200 | test: /\.css$/, 201 | include: cssModulesRegex, 202 | loaders: [ 203 | 'style', 204 | cssModulesLoader, 205 | 'postcss' 206 | ] 207 | }) 208 | } 209 | 210 | // Loaders for files that should not be treated as CSS modules. 211 | const excludeCSSModules = isUsingCSSModules ? cssModulesRegex : false 212 | webpackConfig.module.loaders.push({ 213 | test: /\.scss$/, 214 | exclude: excludeCSSModules, 215 | loaders: [ 216 | 'style', 217 | BASE_CSS_LOADER, 218 | 'postcss', 219 | 'sass?sourceMap' 220 | ] 221 | }) 222 | webpackConfig.module.loaders.push({ 223 | test: /\.css$/, 224 | exclude: excludeCSSModules, 225 | loaders: [ 226 | 'style', 227 | BASE_CSS_LOADER, 228 | 'postcss' 229 | ] 230 | }) 231 | 232 | // ------------------------------------ 233 | // Style Configuration 234 | // ------------------------------------ 235 | webpackConfig.sassLoader = { 236 | includePaths: paths.client('styles') 237 | } 238 | 239 | webpackConfig.postcss = [ 240 | cssnano({ 241 | autoprefixer: { 242 | add: true, 243 | remove: true, 244 | browsers: ['last 2 versions'] 245 | }, 246 | discardComments: { 247 | removeAll: true 248 | }, 249 | discardUnused: false, 250 | mergeIdents: false, 251 | reduceIdents: false, 252 | safe: true, 253 | sourcemap: true 254 | }) 255 | ] 256 | 257 | // File loaders 258 | /* eslint-disable */ 259 | webpackConfig.module.loaders.push( 260 | { test: /\.woff(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff' }, 261 | { test: /\.woff2(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff2' }, 262 | { test: /\.otf(\?.*)?$/, loader: 'file?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=font/opentype' }, 263 | { test: /\.ttf(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/octet-stream' }, 264 | { test: /\.eot(\?.*)?$/, loader: 'file?prefix=fonts/&name=[path][name].[ext]' }, 265 | { test: /\.svg(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=image/svg+xml' }, 266 | { test: /\.(png|jpg)$/, loader: 'url?limit=8192' } 267 | ) 268 | /* eslint-enable */ 269 | 270 | // ------------------------------------ 271 | // Finalize Configuration 272 | // ------------------------------------ 273 | // when we don't know the public path (we know it only when HMR is enabled [in development]) we 274 | // need to use the extractTextPlugin to fix this issue: 275 | // http://stackoverflow.com/questions/34133808/webpack-ots-parsing-error-loading-fonts/34133809#34133809 276 | if (!__DEV__) { 277 | debug('Apply ExtractTextPlugin to CSS loaders.') 278 | webpackConfig.module.loaders.filter((loader) => 279 | loader.loaders && loader.loaders.find((name) => /css/.test(name.split('?')[0])) 280 | ).forEach((loader) => { 281 | const [first, ...rest] = loader.loaders 282 | loader.loader = ExtractTextPlugin.extract(first, rest.join('!')) 283 | delete loader.loaders 284 | }) 285 | 286 | webpackConfig.plugins.push( 287 | new ExtractTextPlugin('[name].[contenthash].css', { 288 | allChunks: true 289 | }) 290 | ) 291 | } 292 | 293 | export default webpackConfig 294 | -------------------------------------------------------------------------------- /config/_base.js: -------------------------------------------------------------------------------- 1 | /* eslint key-spacing:0 spaced-comment:0 */ 2 | import _debug from 'debug' 3 | import path from 'path' 4 | import { argv } from 'yargs' 5 | 6 | const debug = _debug('app:config:_base') 7 | const config = { 8 | env : process.env.NODE_ENV || 'development', 9 | 10 | // ---------------------------------- 11 | // Project Structure 12 | // ---------------------------------- 13 | path_base : path.resolve(__dirname, '..'), 14 | dir_client : 'src', 15 | dir_dist : 'dist', 16 | dir_server : 'server', 17 | dir_test : 'tests', 18 | 19 | // ---------------------------------- 20 | // Server Configuration 21 | // ---------------------------------- 22 | server_host : 'localhost', 23 | server_port : process.env.PORT || 3000, 24 | 25 | // ---------------------------------- 26 | // Compiler Configuration 27 | // ---------------------------------- 28 | compiler_css_modules : true, 29 | compiler_devtool : 'source-map', 30 | compiler_hash_type : 'hash', 31 | compiler_fail_on_warning : false, 32 | compiler_quiet : false, 33 | compiler_public_path : '/', 34 | compiler_stats : { 35 | chunks : false, 36 | chunkModules : false, 37 | colors : true 38 | }, 39 | compiler_vendor : [ 40 | 'history', 41 | 'react', 42 | 'react-redux', 43 | 'react-router', 44 | 'react-router-redux', 45 | 'redux' 46 | ], 47 | 48 | // ---------------------------------- 49 | // Test Configuration 50 | // ---------------------------------- 51 | coverage_enabled : !argv.watch, 52 | coverage_reporters : [ 53 | { type : 'text-summary' }, 54 | { type : 'lcov', dir : 'coverage' } 55 | ] 56 | } 57 | 58 | /************************************************ 59 | ------------------------------------------------- 60 | 61 | All Internal Configuration Below 62 | Edit at Your Own Risk 63 | 64 | ------------------------------------------------- 65 | ************************************************/ 66 | 67 | // ------------------------------------ 68 | // Environment 69 | // ------------------------------------ 70 | // N.B.: globals added here must _also_ be added to .eslintrc 71 | config.globals = { 72 | 'process.env' : { 73 | 'NODE_ENV' : JSON.stringify(config.env) 74 | }, 75 | 'NODE_ENV' : config.env, 76 | '__DEV__' : config.env === 'development', 77 | '__PROD__' : config.env === 'production', 78 | '__TEST__' : config.env === 'test', 79 | '__DEBUG__' : config.env === 'development' && !argv.no_debug, 80 | '__DEBUG_NEW_WINDOW__' : !!argv.nw, 81 | '__BASENAME__' : JSON.stringify(process.env.BASENAME || '') 82 | } 83 | 84 | // ------------------------------------ 85 | // Validate Vendor Dependencies 86 | // ------------------------------------ 87 | const pkg = require('../package.json') 88 | 89 | config.compiler_vendor = config.compiler_vendor 90 | .filter((dep) => { 91 | if (pkg.dependencies[dep]) return true 92 | 93 | debug( 94 | `Package "${dep}" was not found as an npm dependency in package.json; ` + 95 | `it won't be included in the webpack vendor bundle. 96 | Consider removing it from vendor_dependencies in ~/config/index.js` 97 | ) 98 | }) 99 | 100 | // ------------------------------------ 101 | // Utilities 102 | // ------------------------------------ 103 | config.utils_paths = (() => { 104 | const resolve = path.resolve 105 | 106 | const base = (...args) => 107 | resolve.apply(resolve, [config.path_base, ...args]) 108 | 109 | return { 110 | base : base, 111 | client : base.bind(null, config.dir_client), 112 | dist : base.bind(null, config.dir_dist) 113 | } 114 | })() 115 | 116 | export default config 117 | -------------------------------------------------------------------------------- /config/_development.js: -------------------------------------------------------------------------------- 1 | // We use an explicit public path when the assets are served by webpack 2 | // to fix this issue: 3 | // http://stackoverflow.com/questions/34133808/webpack-ots-parsing-error-loading-fonts/34133809#34133809 4 | export default (config) => ({ 5 | compiler_public_path: `http://${config.server_host}:${config.server_port}/`, 6 | proxy: { 7 | enabled: false, 8 | options: { 9 | // koa-proxy options 10 | host: 'http://localhost:8000', 11 | match: /^\/api\/.*/ 12 | } 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /config/_production.js: -------------------------------------------------------------------------------- 1 | /* eslint key-spacing:0 */ 2 | export default () => ({ 3 | compiler_fail_on_warning : false, 4 | compiler_hash_type : 'chunkhash', 5 | compiler_devtool : null, 6 | compiler_stats : { 7 | chunks : true, 8 | chunkModules : true, 9 | colors : true 10 | }, 11 | compiler_public_path: '/' 12 | }) 13 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import _debug from 'debug' 3 | import config from './_base' 4 | 5 | const debug = _debug('app:config') 6 | debug('Create configuration.') 7 | debug(`Apply environment overrides for NODE_ENV "${config.env}".`) 8 | 9 | // Check if the file exists before attempting to require it, this 10 | // way we can provide better error reporting that overrides 11 | // weren't applied simply because the file didn't exist. 12 | const overridesFilename = `_${config.env}` 13 | let hasOverridesFile 14 | try { 15 | fs.lstatSync(`${__dirname}/${overridesFilename}.js`) 16 | hasOverridesFile = true 17 | } catch (e) {} 18 | 19 | // Overrides file exists, so we can attempt to require it. 20 | // We intentionally don't wrap this in a try/catch as we want 21 | // the Node process to exit if an error occurs. 22 | let overrides 23 | if (hasOverridesFile) { 24 | overrides = require(`./${overridesFilename}`).default(config) 25 | } else { 26 | debug(`No configuration overrides found for NODE_ENV "${config.env}"`) 27 | } 28 | 29 | export default Object.assign({}, config, overrides) 30 | -------------------------------------------------------------------------------- /interfaces/image.d.js: -------------------------------------------------------------------------------- 1 | declare module Image { 2 | declare var exports: any; 3 | } 4 | -------------------------------------------------------------------------------- /interfaces/webpack-globals.d.js: -------------------------------------------------------------------------------- 1 | declare var __DEBUG__: boolean; 2 | declare var __DEV__: boolean; 3 | declare var __TEST__: boolean; 4 | declare var __PROD__: boolean; 5 | declare var __BASENAME__: boolean; 6 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "ignore": ["dist", "coverage", "tests", "src"] 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-starter-kit", 3 | "version": "2.0.0-alpha.5", 4 | "description": "Get started with React, Redux, and React-Router!", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=4.2.0", 8 | "npm": "^3.0.0" 9 | }, 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "compile": "better-npm-run compile", 13 | "lint": "eslint .", 14 | "lint:fix": "npm run lint -- --fix", 15 | "start": "better-npm-run start", 16 | "dev": "better-npm-run dev", 17 | "dev:nw": "npm run dev -- --nw", 18 | "dev:no-debug": "npm run dev -- --no_debug", 19 | "test": "better-npm-run test", 20 | "test:dev": "npm run test -- --watch", 21 | "deploy": "better-npm-run deploy", 22 | "deploy:dev": "better-npm-run deploy:dev", 23 | "deploy:prod": "better-npm-run deploy:prod", 24 | "flow:check": "babel-node bin/flow-check", 25 | "codecov": "cat coverage/*/lcov.info | codecov" 26 | }, 27 | "betterScripts": { 28 | "compile": { 29 | "command": "babel-node bin/compile", 30 | "env": { 31 | "DEBUG": "app:*" 32 | } 33 | }, 34 | "dev": { 35 | "command": "nodemon --exec babel-node bin/server", 36 | "env": { 37 | "NODE_ENV": "development", 38 | "DEBUG": "app:*" 39 | } 40 | }, 41 | "deploy": { 42 | "command": "npm run clean && npm run compile", 43 | "env": { 44 | "DEBUG": "app:*" 45 | } 46 | }, 47 | "deploy:dev": { 48 | "command": "npm run deploy", 49 | "env": { 50 | "NODE_ENV": "development", 51 | "DEBUG": "app:*" 52 | } 53 | }, 54 | "deploy:prod": { 55 | "command": "npm run deploy", 56 | "env": { 57 | "NODE_ENV": "production", 58 | "DEBUG": "app:*" 59 | } 60 | }, 61 | "start": { 62 | "command": "babel-node bin/server", 63 | "env": { 64 | "DEBUG": "app:*" 65 | } 66 | }, 67 | "test": { 68 | "command": "babel-node ./node_modules/karma/bin/karma start build/karma.conf", 69 | "env": { 70 | "NODE_ENV": "test", 71 | "DEBUG": "app:*" 72 | } 73 | } 74 | }, 75 | "repository": { 76 | "type": "git", 77 | "url": "git+https://github.com/davezuko/react-redux-starter-kit.git" 78 | }, 79 | "author": "David Zukowski (http://zuko.me)", 80 | "license": "MIT", 81 | "dependencies": { 82 | "axios": "^0.9.1", 83 | "babel-cli": "^6.5.1", 84 | "babel-core": "^6.3.17", 85 | "babel-loader": "^6.2.0", 86 | "babel-plugin-react-transform": "^2.0.0", 87 | "babel-plugin-transform-react-constant-elements": "^6.5.0", 88 | "babel-plugin-transform-react-remove-prop-types": "^0.2.2", 89 | "babel-plugin-transform-runtime": "^6.3.13", 90 | "babel-preset-es2015": "^6.3.13", 91 | "babel-preset-power-assert": "^1.0.0", 92 | "babel-preset-react": "^6.3.13", 93 | "babel-preset-stage-0": "^6.3.13", 94 | "babel-register": "^6.3.13", 95 | "babel-runtime": "^6.3.19", 96 | "better-npm-run": "0.0.8", 97 | "css-loader": "^0.23.0", 98 | "cssnano": "^3.3.2", 99 | "debug": "^2.2.0", 100 | "extract-text-webpack-plugin": "^1.0.0", 101 | "file-loader": "^0.8.4", 102 | "fs-extra": "^0.30.0", 103 | "history": "^2.0.0", 104 | "html-webpack-plugin": "^2.7.1", 105 | "imports-loader": "^0.6.5", 106 | "json-loader": "^0.5.4", 107 | "koa": "^2.0.0", 108 | "koa-bodyparser": "^3.0.0", 109 | "koa-connect-history-api-fallback": "^0.3.0", 110 | "koa-convert": "^1.2.0", 111 | "koa-proxy": "^0.5.0", 112 | "koa-router": "^7.0.1", 113 | "koa-static": "^2.0.0", 114 | "material-ui": "^0.15.1", 115 | "node-sass": "^3.3.3", 116 | "postcss-loader": "^0.8.0", 117 | "react": "^15.0.0", 118 | "react-dom": "^15.0.0", 119 | "react-redux": "^4.0.0", 120 | "react-router": "^2.4.0", 121 | "react-router-redux": "^4.0.4", 122 | "react-tap-event-plugin": "^1.0.0", 123 | "redux": "^3.0.0", 124 | "redux-thunk": "^2.0.0", 125 | "rimraf": "^2.5.1", 126 | "sass-loader": "^3.0.0", 127 | "style-loader": "^0.13.0", 128 | "url-loader": "^0.5.6", 129 | "webpack": "^1.12.14", 130 | "yargs": "^4.0.0" 131 | }, 132 | "devDependencies": { 133 | "babel-eslint": "^6.0.0-beta.6", 134 | "chai": "^3.4.1", 135 | "chai-as-promised": "^5.1.0", 136 | "chai-enzyme": "^0.4.0", 137 | "cheerio": "^0.20.0", 138 | "codecov": "^1.0.1", 139 | "enzyme": "^2.0.0", 140 | "eslint": "^2.4.0", 141 | "eslint-config-standard": "^5.1.0", 142 | "eslint-config-standard-react": "^2.2.0", 143 | "eslint-plugin-babel": "^3.0.0", 144 | "eslint-plugin-flow-vars": "^0.3.0", 145 | "eslint-plugin-promise": "^1.0.8", 146 | "eslint-plugin-react": "^4.0.0", 147 | "eslint-plugin-standard": "^1.3.1", 148 | "flow-bin": "0.23.0", 149 | "flow-interfaces": "^0.6.0", 150 | "isparta-loader": "^2.0.0", 151 | "karma": "^0.13.21", 152 | "karma-coverage": "^1.0.0", 153 | "karma-espower-preprocessor": "^1.1.0", 154 | "karma-mocha": "^1.0.1", 155 | "karma-mocha-reporter": "^2.0.0", 156 | "karma-phantomjs-launcher": "^1.0.0", 157 | "karma-webpack-with-fast-source-maps": "^1.9.2", 158 | "mocha": "^2.2.5", 159 | "nodemon": "^1.8.1", 160 | "phantomjs-polyfill": "0.0.2", 161 | "phantomjs-prebuilt": "^2.1.3", 162 | "power-assert": "^1.4.1", 163 | "react-addons-test-utils": "^15.0.0", 164 | "react-transform-catch-errors": "^1.0.2", 165 | "react-transform-hmr": "^1.0.2", 166 | "redbox-react": "^1.2.2", 167 | "redux-devtools": "^3.0.0", 168 | "redux-devtools-dock-monitor": "^1.0.1", 169 | "redux-devtools-log-monitor": "^1.0.1", 170 | "sinon": "^1.17.3", 171 | "sinon-chai": "^2.8.0", 172 | "webpack-dev-middleware": "^1.6.1", 173 | "webpack-hot-middleware": "^2.6.0" 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /server/api/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router' 2 | import todosApi from './todos' 3 | 4 | const api = new Router({ 5 | prefix: '/api' 6 | }) 7 | 8 | api.use('/todos', todosApi.routes()) 9 | 10 | export default api 11 | -------------------------------------------------------------------------------- /server/api/todos.js: -------------------------------------------------------------------------------- 1 | /* 2 | todo api 3 | */ 4 | import Router from 'koa-router' 5 | import bodyParser from 'koa-bodyparser' 6 | import { delay } from './utils' 7 | 8 | const route = new Router() 9 | let todos = [ 10 | 'learn react asynchronously', 11 | 'learn flux asynchronously', 12 | 'learn redux asynchronously' 13 | ] 14 | 15 | // 2秒後Todo一覧を返す 16 | route.get('/', (ctx, next) => { 17 | return delay(2000).then(() => { 18 | ctx.body = todos 19 | }) 20 | }) 21 | 22 | // Todoの追加, 2秒後にレスポンスを返す 23 | route.post('/add', bodyParser(), (ctx, next) => { 24 | return delay(2000).then(() => { 25 | const { text } = ctx.request.body 26 | if (text) { 27 | todos.push(text) 28 | ctx.status = 200 29 | ctx.body = todos 30 | } else { 31 | ctx.status = 404 32 | ctx.body = 'failed to add todo' 33 | } 34 | }) 35 | }) 36 | 37 | // Todoの削除, 2秒後にレスポンスを返す 38 | route.delete('/:index', (ctx, next) => { 39 | return delay(2000).then(() => { 40 | const index = parseInt(ctx.params.index, 10) 41 | todos = todos.filter((todo, i) => i !== index) 42 | ctx.status = 200 43 | ctx.body = todos 44 | }) 45 | }) 46 | 47 | export default route 48 | -------------------------------------------------------------------------------- /server/api/utils.js: -------------------------------------------------------------------------------- 1 | export function delay (msec) { 2 | return new Promise(function (resolve, reject) { 3 | setTimeout(resolve, msec) 4 | }) 5 | } 6 | -------------------------------------------------------------------------------- /server/lib/apply-express-middleware.js: -------------------------------------------------------------------------------- 1 | // Based on: https://github.com/dayAlone/koa-webpack-hot-middleware/blob/master/index.js 2 | export default function applyExpressMiddleware (fn, req, res) { 3 | const originalEnd = res.end 4 | 5 | return new Promise((resolve) => { 6 | res.end = function () { 7 | originalEnd.apply(this, arguments) 8 | resolve(false) 9 | } 10 | fn(req, res, function () { 11 | resolve(true) 12 | }) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import Koa from 'koa' 2 | import convert from 'koa-convert' 3 | import api from './api' 4 | import webpack from 'webpack' 5 | import webpackConfig from '../build/webpack.config' 6 | import historyApiFallback from 'koa-connect-history-api-fallback' 7 | import serve from 'koa-static' 8 | import proxy from 'koa-proxy' 9 | import _debug from 'debug' 10 | import config from '../config' 11 | import webpackDevMiddleware from './middleware/webpack-dev' 12 | import webpackHMRMiddleware from './middleware/webpack-hmr' 13 | 14 | const debug = _debug('app:server') 15 | const paths = config.utils_paths 16 | const app = new Koa() 17 | 18 | // Include API 19 | app.use(api.routes()) 20 | 21 | // Enable koa-proxy if it has been enabled in the config. 22 | if (config.proxy && config.proxy.enabled) { 23 | app.use(convert(proxy(config.proxy.options))) 24 | } 25 | 26 | // This rewrites all routes requests to the root /index.html file 27 | // (ignoring file requests). If you want to implement isomorphic 28 | // rendering, you'll want to remove this middleware. 29 | app.use(convert(historyApiFallback({ 30 | verbose: false 31 | }))) 32 | 33 | // ------------------------------------ 34 | // Apply Webpack HMR Middleware 35 | // ------------------------------------ 36 | if (config.env === 'development') { 37 | const compiler = webpack(webpackConfig) 38 | 39 | // Enable webpack-dev and webpack-hot middleware 40 | const { publicPath } = webpackConfig.output 41 | 42 | app.use(webpackDevMiddleware(compiler, publicPath)) 43 | app.use(webpackHMRMiddleware(compiler)) 44 | 45 | // Serve static assets from ~/src/static since Webpack is unaware of 46 | // these files. This middleware doesn't need to be enabled outside 47 | // of development since this directory will be copied into ~/dist 48 | // when the application is compiled. 49 | app.use(convert(serve(paths.client('static')))) 50 | } else { 51 | debug( 52 | 'Server is being run outside of live development mode. This starter kit ' + 53 | 'does not provide any production-ready server functionality. To learn ' + 54 | 'more about deployment strategies, check out the "deployment" section ' + 55 | 'in the README.' 56 | ) 57 | 58 | // Serving ~/dist by default. Ideally these files should be served by 59 | // the web server and not the app server, but this helps to demo the 60 | // server in production. 61 | app.use(convert(serve(paths.base(config.dir_dist)))) 62 | } 63 | 64 | export default app 65 | -------------------------------------------------------------------------------- /server/middleware/webpack-dev.js: -------------------------------------------------------------------------------- 1 | import WebpackDevMiddleware from 'webpack-dev-middleware' 2 | import applyExpressMiddleware from '../lib/apply-express-middleware' 3 | import _debug from 'debug' 4 | import config from '../../config' 5 | 6 | const paths = config.utils_paths 7 | const debug = _debug('app:server:webpack-dev') 8 | 9 | export default function (compiler, publicPath) { 10 | debug('Enable webpack dev middleware.') 11 | 12 | const middleware = WebpackDevMiddleware(compiler, { 13 | publicPath, 14 | contentBase: paths.base(config.dir_client), 15 | hot: true, 16 | quiet: config.compiler_quiet, 17 | noInfo: config.compiler_quiet, 18 | lazy: false, 19 | stats: config.compiler_stats 20 | }) 21 | 22 | return async function koaWebpackDevMiddleware (ctx, next) { 23 | let hasNext = await applyExpressMiddleware(middleware, ctx.req, { 24 | end: (content) => (ctx.body = content), 25 | setHeader: function () { 26 | ctx.set.apply(ctx, arguments) 27 | } 28 | }) 29 | 30 | if (hasNext) { 31 | await next() 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/middleware/webpack-hmr.js: -------------------------------------------------------------------------------- 1 | import WebpackHotMiddleware from 'webpack-hot-middleware' 2 | import applyExpressMiddleware from '../lib/apply-express-middleware' 3 | import _debug from 'debug' 4 | 5 | const debug = _debug('app:server:webpack-hmr') 6 | 7 | export default function (compiler, opts) { 8 | debug('Enable Webpack Hot Module Replacement (HMR).') 9 | 10 | const middleware = WebpackHotMiddleware(compiler, opts) 11 | return async function koaWebpackHMR (ctx, next) { 12 | let hasNext = await applyExpressMiddleware(middleware, ctx.req, ctx.res) 13 | 14 | if (hasNext && next) { 15 | await next() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adwd/react-redux-introduction/c9575b4946b65aaef3c6ad55b056ff3ee6ed26d8/src/components/.gitkeep -------------------------------------------------------------------------------- /src/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import LogMonitor from 'redux-devtools-log-monitor' 4 | import DockMonitor from 'redux-devtools-dock-monitor' 5 | 6 | export default createDevTools( 7 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /src/containers/DevToolsWindow.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import LogMonitor from 'redux-devtools-log-monitor' 4 | 5 | export default createDevTools( 6 | 7 | ) 8 | -------------------------------------------------------------------------------- /src/containers/Root.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Provider } from 'react-redux' 3 | import { Router } from 'react-router' 4 | 5 | export default class Root extends React.Component { 6 | static propTypes = { 7 | history: PropTypes.object.isRequired, 8 | routes: PropTypes.element.isRequired, 9 | store: PropTypes.object.isRequired 10 | }; 11 | 12 | get content () { 13 | return ( 14 | 15 | {this.props.routes} 16 | 17 | ) 18 | } 19 | 20 | get devTools () { 21 | if (__DEBUG__) { 22 | if (__DEBUG_NEW_WINDOW__) { 23 | if (!window.devToolsExtension) { 24 | require('../redux/utils/createDevToolsWindow').default(this.props.store) 25 | } else { 26 | window.devToolsExtension.open() 27 | } 28 | } else if (!window.devToolsExtension) { 29 | const DevTools = require('containers/DevTools').default 30 | return 31 | } 32 | } 33 | } 34 | 35 | render () { 36 | return ( 37 | 38 |
39 | {this.content} 40 | {this.devTools} 41 |
42 |
43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Redux Introduction 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/CoreLayout.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { withRouter } from 'react-router' 3 | import AppBar from 'material-ui/AppBar' 4 | import Drawer from 'material-ui/Drawer' 5 | import { List, ListItem } from 'material-ui/List' 6 | import Subheader from 'material-ui/Subheader' 7 | import '../../styles/core.scss' 8 | 9 | export class CoreLayout extends Component { 10 | 11 | constructor (props) { 12 | super(props) 13 | 14 | this.handleRequestChange = this.handleRequestChange.bind(this) 15 | this.state = {open: false} 16 | } 17 | 18 | handleToggle = () => this.setState({open: !this.state.open}) 19 | 20 | handleRequestChange (open) { 21 | this.setState({open}) 22 | } 23 | 24 | handleClickItem = (path) => () => { 25 | this.setState({open: !this.state.open}) 26 | this.props.router.push(path) 27 | } 28 | 29 | render () { 30 | return ( 31 |
32 | 35 | 39 | 40 | Samples 41 | 44 | 48 | 52 | 56 | 57 | 58 | {this.props.children} 59 |
60 | ) 61 | } 62 | } 63 | 64 | CoreLayout.propTypes = { 65 | children: PropTypes.element, 66 | router: PropTypes.shape({ 67 | push: PropTypes.func.isRequired 68 | }).isRequired 69 | } 70 | 71 | export default withRouter(CoreLayout) 72 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/index.js: -------------------------------------------------------------------------------- 1 | import CoreLayout from './CoreLayout' 2 | export default CoreLayout 3 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import getMuiTheme from 'material-ui/styles/getMuiTheme' 4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 5 | import createBrowserHistory from 'history/lib/createBrowserHistory' 6 | import { useRouterHistory } from 'react-router' 7 | import { syncHistoryWithStore } from 'react-router-redux' 8 | import makeRoutes from './routes' 9 | import Root from './containers/Root' 10 | import configureStore from './redux/configureStore' 11 | 12 | import injectTapEventPlugin from 'react-tap-event-plugin' 13 | 14 | // Needed for onTouchTap 15 | // http://stackoverflow.com/a/34015469/988941 16 | injectTapEventPlugin() 17 | 18 | // Configure history for react-router 19 | const browserHistory = useRouterHistory(createBrowserHistory)({ 20 | basename: __BASENAME__ 21 | }) 22 | 23 | // Create redux store and sync with react-router-redux. We have installed the 24 | // react-router-redux reducer under the key "router" in src/routes/index.js, 25 | // so we need to provide a custom `selectLocationState` to inform 26 | // react-router-redux of its location. 27 | const initialState = window.__INITIAL_STATE__ 28 | const store = configureStore(initialState, browserHistory) 29 | const history = syncHistoryWithStore(browserHistory, store, { 30 | selectLocationState: (state) => state.router 31 | }) 32 | 33 | // Now that we have the Redux store, we can create our routes. We provide 34 | // the store to the route definitions so that routes have access to it for 35 | // hooks such as `onEnter`. 36 | const routes = makeRoutes(store) 37 | 38 | // Now that redux and react-router have been configured, we can render the 39 | // React application to the DOM! 40 | ReactDOM.render( 41 | 42 | 43 | , 44 | document.getElementById('root') 45 | ) 46 | -------------------------------------------------------------------------------- /src/redux/configureStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import rootReducer from './rootReducer' 4 | import { routerMiddleware } from 'react-router-redux' 5 | 6 | export default function configureStore (initialState = {}, history) { 7 | // Compose final middleware and use devtools in debug environment 8 | let middleware = applyMiddleware(thunk, routerMiddleware(history)) 9 | if (__DEBUG__) { 10 | const devTools = window.devToolsExtension 11 | ? window.devToolsExtension() 12 | : require('containers/DevTools').default.instrument() 13 | middleware = compose(middleware, devTools) 14 | } 15 | 16 | // Create final store and subscribe router in debug env ie. for devtools 17 | const store = createStore(rootReducer, initialState, middleware) 18 | 19 | if (module.hot) { 20 | module.hot.accept('./rootReducer', () => { 21 | const nextRootReducer = require('./rootReducer').default 22 | 23 | store.replaceReducer(nextRootReducer) 24 | }) 25 | } 26 | return store 27 | } 28 | -------------------------------------------------------------------------------- /src/redux/modules/asyncTodo/asyncTodo.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { types } from './others' // todoをAPIで取得する以外の、Todoの追加・更新はothers.jsのほうに書いた 3 | 4 | // Constants 5 | const FETCH_TODO_REQUEST = 'FETCH_TODO_REQUEST' 6 | const FETCH_TODO_SUCCESS = 'FETCH_TODO_SUCCESS' 7 | const FETCH_TODO_FAILURE = 'FETCH_TODO_FAILURE' 8 | 9 | // Actions 10 | function requestTodo () { 11 | return { 12 | type: FETCH_TODO_REQUEST 13 | } 14 | } 15 | 16 | function receiveTodo (todos) { 17 | return { 18 | type: FETCH_TODO_SUCCESS, 19 | todos 20 | } 21 | } 22 | 23 | function failToFetchTodo (error) { 24 | return { 25 | type: FETCH_TODO_FAILURE, 26 | error 27 | } 28 | } 29 | 30 | // Async Actions 31 | export function fetchTodos () { 32 | return dispatch => { 33 | dispatch(requestTodo()) 34 | return axios.get('/api/todos') 35 | .then(response => { 36 | dispatch(receiveTodo(response.data)) 37 | }) 38 | .catch(e => { 39 | dispatch(failToFetchTodo(e)) 40 | }) 41 | } 42 | } 43 | 44 | // Inital state 45 | export const initialState = { 46 | newTodo: '', 47 | todos: [], 48 | loading: false, 49 | 50 | adding: false, 51 | removing: false 52 | } 53 | 54 | // Reducer 55 | export default function todoReducer (state = initialState, action) { 56 | switch (action.type) { 57 | case FETCH_TODO_REQUEST: 58 | return { ...state, loading: true } 59 | 60 | case FETCH_TODO_SUCCESS: 61 | return { ...state, loading: false, todos: action.todos } 62 | 63 | case FETCH_TODO_FAILURE: 64 | return { ...state, loading: false } 65 | 66 | case types.edit: 67 | return { ...state, newTodo: action.text } 68 | 69 | case types.add.REQUEST: 70 | return { ...state, adding: true } 71 | 72 | case types.add.SUCCESS: 73 | return { ...state, adding: false, todos: action.data } 74 | 75 | case types.add.FAILURE: 76 | return { ...state, adding: false } 77 | 78 | case types.remove.REQUEST: 79 | return { ...state, removing: true } 80 | 81 | case types.remove.SUCCESS: 82 | return { ...state, removing: false, todos: action.data } 83 | 84 | case types.remove.FAILURE: 85 | return { ...state, removing: false } 86 | 87 | default: 88 | return state 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/redux/modules/asyncTodo/index.js: -------------------------------------------------------------------------------- 1 | import todo, { fetchTodos } from './asyncTodo' 2 | import { editTodo, addTodo, removeTodo } from './others' 3 | 4 | export { fetchTodos, editTodo, addTodo, removeTodo } 5 | export default todo 6 | -------------------------------------------------------------------------------- /src/redux/modules/asyncTodo/others.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const actionTypeCreator = (type) => ({ 4 | REQUEST: `${type}_REQUEST`, 5 | SUCCESS: `${type}_SUCCESS`, 6 | FAILURE: `${type}_FAILURE` 7 | }) 8 | 9 | // Constants 10 | export const types = { 11 | add: actionTypeCreator('ADD_TODO'), 12 | remove: actionTypeCreator('REMOVE_TODO'), 13 | edit: 'EDIT_NEW_TODO' 14 | } 15 | 16 | // Actions 17 | const actionCreator = (type) => ({ 18 | request: () => ({ type: type.REQUEST }), 19 | success: (data) => ({ type: type.SUCCESS, data }), 20 | failure: (error) => ({ type: type.FAILURE, error }) 21 | }) 22 | 23 | const actions = { 24 | addTodo: actionCreator(types.add), 25 | removeTodo: actionCreator(types.remove) 26 | } 27 | 28 | export function editTodo (text) { 29 | return { 30 | type: types.edit, 31 | text 32 | } 33 | } 34 | 35 | // Async Actions 36 | export function addTodo (text) { 37 | return dispatch => { 38 | dispatch(actions.addTodo.request()) 39 | return axios.post('/api/todos/add', { text }) 40 | .then(response => { 41 | dispatch(actions.addTodo.success(response.data)) 42 | }) 43 | .catch(e => { 44 | dispatch(actions.addTodo.failure(e)) 45 | }) 46 | } 47 | } 48 | 49 | export function removeTodo (index) { 50 | return dispatch => { 51 | dispatch(actions.removeTodo.request()) 52 | return axios.delete(`/api/todos/${index}`) 53 | .then(response => { 54 | dispatch(actions.removeTodo.success(response.data)) 55 | }) 56 | .catch(e => { 57 | dispatch(actions.removeTodo.failure(e)) 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/redux/modules/counter.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // ------------------------------------ 3 | // Constants 4 | // ------------------------------------ 5 | export const COUNTER_INCREMENT = 'COUNTER_INCREMENT' 6 | 7 | // ------------------------------------ 8 | // Actions 9 | // ------------------------------------ 10 | // NOTE: "Action" is a Flow interface defined in https://github.com/TechnologyAdvice/flow-interfaces 11 | // If you're unfamiliar with Flow, you are completely welcome to avoid annotating your code, but 12 | // if you'd like to learn more you can check out: flowtype.org. 13 | // DOUBLE NOTE: there is currently a bug with babel-eslint where a `space-infix-ops` error is 14 | // incorrectly thrown when using arrow functions, hence the oddity. 15 | export function increment (value: number = 1): Action { 16 | return { 17 | type: COUNTER_INCREMENT, 18 | payload: value 19 | } 20 | } 21 | 22 | // This is a thunk, meaning it is a function that immediately 23 | // returns a function for lazy evaluation. It is incredibly useful for 24 | // creating async actions, especially when combined with redux-thunk! 25 | // NOTE: This is solely for demonstration purposes. In a real application, 26 | // you'd probably want to dispatch an action of COUNTER_DOUBLE and let the 27 | // reducer take care of this logic. 28 | export const doubleAsync = (): Function => { 29 | return (dispatch: Function, getState: Function): Promise => { 30 | return new Promise((resolve: Function): void => { 31 | setTimeout(() => { 32 | dispatch(increment(getState().counter)) 33 | resolve() 34 | }, 200) 35 | }) 36 | } 37 | } 38 | 39 | export const actions = { 40 | increment, 41 | doubleAsync 42 | } 43 | 44 | // ------------------------------------ 45 | // Action Handlers 46 | // ------------------------------------ 47 | const ACTION_HANDLERS = { 48 | [COUNTER_INCREMENT]: (state: number, action: {payload: number}): number => state + action.payload 49 | } 50 | 51 | // ------------------------------------ 52 | // Reducer 53 | // ------------------------------------ 54 | const initialState = 0 55 | export default function counterReducer (state: number = initialState, action: Action): number { 56 | const handler = ACTION_HANDLERS[action.type] 57 | 58 | return handler ? handler(state, action) : state 59 | } 60 | -------------------------------------------------------------------------------- /src/redux/modules/todo.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | export const ADD_TODO = 'ADD_TODO' 3 | export const REMOVE_TODO = 'REMOVE_TODO' 4 | export const EDIT_NEW_TODO = 'EDIT_NEW_TODO' 5 | 6 | // Actions 7 | export function addTodo () { 8 | return { 9 | type: ADD_TODO 10 | } 11 | } 12 | 13 | export function removeTodo (index) { 14 | return { 15 | type: REMOVE_TODO, 16 | index 17 | } 18 | } 19 | 20 | export function editNewTodo (text) { 21 | return { 22 | type: EDIT_NEW_TODO, 23 | text 24 | } 25 | } 26 | 27 | // Inital state 28 | const initialState = { 29 | newTodo: '', 30 | todos: [ 31 | 'learn react', 32 | 'learn flux', 33 | 'learn redux' 34 | ] 35 | } 36 | 37 | // Reducer 38 | export default function todoReducer (state = initialState, action) { 39 | switch (action.type) { 40 | case ADD_TODO: 41 | return { newTodo: '', todos: [...state.todos, state.newTodo] } 42 | 43 | case REMOVE_TODO: 44 | return { ...state, todos: state.todos.filter((todo, index) => index !== action.index) } 45 | 46 | case EDIT_NEW_TODO: 47 | return { ...state, newTodo: action.text } 48 | 49 | default: 50 | return state 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/redux/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer as router } from 'react-router-redux' 3 | import counter from './modules/counter' 4 | import todo from './modules/todo' 5 | import asyncTodo from './modules/asyncTodo' 6 | 7 | export default combineReducers({ 8 | counter, 9 | todo, 10 | asyncTodo, 11 | router 12 | }) 13 | -------------------------------------------------------------------------------- /src/redux/utils/createDevToolsWindow.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import DevTools from '../../containers/DevToolsWindow' 5 | 6 | export default function createDevToolsWindow (store) { 7 | const win = window.open( 8 | null, 9 | 'redux-devtools', // give it a name so it reuses the same window 10 | `width=400,height=${window.outerHeight},menubar=no,location=no,resizable=yes,scrollbars=no,status=no` 11 | ) 12 | 13 | // reload in case it's reusing the same window with the old content 14 | win.location.reload() 15 | 16 | // wait a little bit for it to reload, then render 17 | setTimeout(() => { 18 | // Wait for the reload to prevent: 19 | // "Uncaught Error: Invariant Violation: _registerComponent(...): Target container is not a DOM element." 20 | win.document.write('
') 21 | win.document.body.style.margin = '0' 22 | 23 | ReactDOM.render( 24 | 25 | 26 | 27 | , win.document.getElementById('react-devtools-root') 28 | ) 29 | }, 10) 30 | } 31 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, IndexRoute } from 'react-router' 3 | 4 | // NOTE: here we're making use of the `resolve.root` configuration 5 | // option in webpack, which allows us to specify import paths as if 6 | // they were from the root of the ~/src directory. This makes it 7 | // very easy to navigate to files regardless of how deeply nested 8 | // your current file is. 9 | import CoreLayout from 'layouts/CoreLayout/CoreLayout' 10 | import HomeView from 'views/HomeView/HomeView' 11 | import ReactSample from 'views/ReactSample' 12 | import ReduxSample from 'views/ReduxSample' 13 | import ReduxAsyncSample from 'views/ReduxAsyncSample' 14 | 15 | export default (store) => ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adwd/react-redux-introduction/c9575b4946b65aaef3c6ad55b056ff3ee6ed26d8/src/static/favicon.ico -------------------------------------------------------------------------------- /src/static/humans.txt: -------------------------------------------------------------------------------- 1 | # Check it out: http://humanstxt.org/ 2 | 3 | # TEAM 4 | 5 | -- -- 6 | 7 | # THANKS 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Application Settings Go Here 3 | ------------------------------------ 4 | This file acts as a bundler for all variables/mixins/themes, so they 5 | can easily be swapped out without `core.scss` ever having to know. 6 | 7 | For example: 8 | 9 | @import './variables/colors'; 10 | @import './variables/components'; 11 | @import './themes/default'; 12 | */ 13 | -------------------------------------------------------------------------------- /src/styles/core.scss: -------------------------------------------------------------------------------- 1 | :global { 2 | @import 'base'; 3 | @import 'vendor/normalize'; 4 | 5 | // Some best-practice CSS that's useful for most apps 6 | // Just remove them if they're not what you want 7 | html { 8 | box-sizing: border-box; 9 | } 10 | 11 | html, 12 | body { 13 | margin: 0; 14 | padding: 0; 15 | height: 100%; 16 | } 17 | 18 | *, 19 | *:before, 20 | *:after { 21 | box-sizing: inherit; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/styles/vendor/_normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } 428 | -------------------------------------------------------------------------------- /src/views/HomeView/Duck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adwd/react-redux-introduction/c9575b4946b65aaef3c6ad55b056ff3ee6ed26d8/src/views/HomeView/Duck.jpg -------------------------------------------------------------------------------- /src/views/HomeView/HomeView.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React, { PropTypes } from 'react' 3 | import { connect } from 'react-redux' 4 | import { Link, withRouter } from 'react-router' 5 | import Paper from 'material-ui/Paper' 6 | import { Card, CardTitle, CardText } from 'material-ui/Card' 7 | 8 | // We can use Flow (http://flowtype.org/) to type our component's props 9 | // and state. For convenience we've included both regular propTypes and 10 | // Flow types, but if you want to try just using Flow you'll want to 11 | // disable the eslint rule `react/prop-types`. 12 | // NOTE: You can run `npm run flow:check` to check for any errors in your 13 | // code, or `npm i -g flow-bin` to have access to the binary globally. 14 | // Sorry Windows users :(. 15 | type Props = { 16 | counter: number, 17 | doubleAsync: Function, 18 | increment: Function 19 | }; 20 | 21 | const styles = { 22 | container: { 23 | textAlign: 'center' 24 | }, 25 | content: { 26 | width: '80%', 27 | margin: 20, 28 | display: 'inline-block' 29 | } 30 | } 31 | 32 | // We avoid using the `@connect` decorator on the class definition so 33 | // that we can export the undecorated component for testing. 34 | // See: http://rackt.github.io/redux/docs/recipes/WritingTests.html 35 | export class HomeView extends React.Component { 36 | render () { 37 | const toReduxAsync = () => this.props.router.push('redux-async') 38 | return ( 39 |
40 | 41 | 42 | 43 | 44 |

react sample

45 |

redux sample

46 | 47 |
48 |
49 |
50 |
51 | ) 52 | } 53 | } 54 | 55 | HomeView.propTypes = { 56 | router: PropTypes.shape({ 57 | push: PropTypes.func 58 | }) 59 | } 60 | 61 | export default connect(state => state)(withRouter(HomeView)) 62 | -------------------------------------------------------------------------------- /src/views/HomeView/HomeView.scss: -------------------------------------------------------------------------------- 1 | .counter { 2 | font-weight: bold; 3 | } 4 | 5 | .counter--green { 6 | composes: counter; 7 | color: rgb(25,200,25); 8 | } 9 | 10 | .duck { 11 | display: block; 12 | width: 100%; 13 | margin-top: 1.5rem; 14 | } 15 | -------------------------------------------------------------------------------- /src/views/HomeView/index.js: -------------------------------------------------------------------------------- 1 | import HomeView from './HomeView' 2 | export default HomeView 3 | -------------------------------------------------------------------------------- /src/views/ReactSample/TodoItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | export default class TodoItem extends Component { 4 | static propTypes = { 5 | index: PropTypes.number, 6 | onRemove: PropTypes.func, 7 | children: PropTypes.oneOfType([ 8 | React.PropTypes.arrayOf(React.PropTypes.node), 9 | React.PropTypes.node 10 | ]) 11 | } 12 | 13 | render () { 14 | const { index, onRemove, children } = this.props 15 | const onClick = () => onRemove(index) 16 | return ( 17 |
18 | {`${index + 1}`}: {children} 19 |
20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/views/ReactSample/TodoList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import TodoItem from './TodoItem' 3 | 4 | export default class TodoList extends Component { 5 | constructor (props) { 6 | super(props) 7 | 8 | // コンストラクタでstateの初期値を設定する 9 | // コンストラクタ内ではthis.stateに直接代入するが、 10 | // ここ以外の状態の変更はthis.setStateを使う 11 | this.state = { 12 | newTodo: '', 13 | todos: [ 14 | 'learn react', 15 | 'learn flux', 16 | 'learn redux' 17 | ] 18 | } 19 | } 20 | 21 | changeText = (e) => { 22 | this.setState({ newTodo: e.target.value }) 23 | } 24 | 25 | addTodo = () => { 26 | const newTodo = this.state.newTodo 27 | this.setState({ 28 | newTodo: '', 29 | todos: [...this.state.todos, newTodo] 30 | }) 31 | } 32 | 33 | deleteTodoItem = (index) => { 34 | this.setState({ 35 | todos: this.state.todos.filter((todo, i) => i !== index) 36 | }) 37 | } 38 | 39 | render () { 40 | // JSON形式でスタイルシートの直接指定ができる 41 | const style = { 42 | content: { 43 | padding: '10px 40px' 44 | }, 45 | pre: { 46 | // ハイフンはキャメルケースにして書く 47 | fontFamily: 'Consolas, Monaco, monospace', 48 | fontSize: 12, 49 | backgroundColor: '#eeeeee', 50 | margin: '20px', 51 | padding: '10px' 52 | } 53 | } 54 | 55 | return ( 56 |
57 |

React Sample

58 |

Todos

59 | 60 | 61 | 62 | { 63 | this.state.todos.map((text, index) => ( 64 | 65 | {text} 66 | 67 | )) 68 | } 69 | 70 |

state:

71 |
72 |           {JSON.stringify(this.state, null, 2)}
73 |         
74 |
75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/views/ReactSample/index.js: -------------------------------------------------------------------------------- 1 | import TodoList from './TodoList' 2 | 3 | export default TodoList 4 | -------------------------------------------------------------------------------- /src/views/ReduxAsyncSample/ReduxAsyncSample.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { fetchTodos, editTodo, addTodo, removeTodo } from '../../redux/modules/asyncTodo' 4 | 5 | class ReduxAsyncSample extends Component { 6 | static propTypes = { 7 | dispatch: PropTypes.func, 8 | todo: PropTypes.any 9 | } 10 | 11 | componentDidMount () { 12 | this.props.dispatch(fetchTodos()) 13 | } 14 | 15 | render () { 16 | const { dispatch } = this.props 17 | const { newTodo, todos, loading, adding, removing } = this.props.todo 18 | const style = { 19 | p: { 20 | margin: '10px' 21 | }, 22 | content: { 23 | height: 500, 24 | width: '90%', 25 | marginRight: 'auto', 26 | marginLeft: 'auto' 27 | }, 28 | pre: { 29 | fontFamily: 'Consolas, Monaco, monospace', 30 | fontSize: 12, 31 | backgroundColor: '#eeeeee', 32 | margin: '20px', 33 | padding: '10px' 34 | } 35 | } 36 | console.log(this.props) 37 | 38 | const edit = (e) => dispatch(editTodo(e.target.value)) 39 | const add = () => dispatch(addTodo(this.refs.newTodo.value)) 40 | const remove = (index) => () => dispatch(removeTodo(index)) 41 | 42 | return ( 43 |
44 |

Redux Async Sample

45 |

Todos

46 | 47 | 48 | 49 | {loading 50 | ?

loading todo

51 | : todos.map((text, index) => ( 52 |
53 | {`${index + 1}`}: {text} 54 | 55 |
56 | )) 57 | } 58 |
{JSON.stringify(this.props.todo, null, 2)}
59 |
60 | ) 61 | } 62 | } 63 | 64 | const ConnectedReduxAsyncSample = connect( 65 | state => ({ 66 | todo: state.asyncTodo 67 | }) 68 | )(ReduxAsyncSample) 69 | 70 | export default ConnectedReduxAsyncSample 71 | -------------------------------------------------------------------------------- /src/views/ReduxAsyncSample/index.js: -------------------------------------------------------------------------------- 1 | import ReduxAsyncSample from './ReduxAsyncSample' 2 | 3 | export default ReduxAsyncSample 4 | -------------------------------------------------------------------------------- /src/views/ReduxSample/ReduxSample.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { addTodo, removeTodo, editNewTodo } from '../../redux/modules/todo' 4 | 5 | class ReduxSample extends Component { 6 | static propTypes = { 7 | todo: PropTypes.object, 8 | dispatch: PropTypes.func.isRequired 9 | } 10 | 11 | render () { 12 | const { todo: { newTodo, todos }, dispatch } = this.props 13 | 14 | // jsxのプロパティのアロー関数を入れるとESLintに怒られるのでここでつくる 15 | const edit = (e) => dispatch(editNewTodo(e.target.value)) 16 | const add = () => dispatch(addTodo()) 17 | const remove = (index) => () => dispatch(removeTodo(index)) 18 | 19 | return ( 20 |
21 |

Redux Sample

22 |

Todos

23 | 24 | 25 | 26 | { 27 | todos.map((text, index) => ( 28 |
29 | {`${index + 1}`}: {text} 30 | 31 |
32 | )) 33 | } 34 |
35 | ) 36 | } 37 | } 38 | 39 | const ConnectedReduxSample = connect( 40 | state => ({todo: state.todo}) 41 | )(ReduxSample) 42 | 43 | export default ConnectedReduxSample 44 | -------------------------------------------------------------------------------- /src/views/ReduxSample/index.js: -------------------------------------------------------------------------------- 1 | import ReduxSample from './ReduxSample' 2 | 3 | export default ReduxSample 4 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../.eslintrc", 3 | "env" : { 4 | "mocha" : true 5 | }, 6 | "globals" : { 7 | "expect" : false, 8 | "should" : false, 9 | "sinon" : false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/framework.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import React from 'react' 3 | import {mount, render, shallow} from 'enzyme' 4 | 5 | class Fixture extends React.Component { 6 | render () { 7 | return ( 8 |
9 | 10 | 11 |
12 | ) 13 | } 14 | } 15 | 16 | describe('(Framework) Karma Plugins', function () { 17 | it('Should expose "expect" globally.', function () { 18 | assert.ok(expect) 19 | }) 20 | 21 | it('Should expose "should" globally.', function () { 22 | assert.ok(should) 23 | }) 24 | 25 | it('Should have chai-as-promised helpers.', function () { 26 | const pass = new Promise(res => res('test')) 27 | const fail = new Promise((res, rej) => rej()) 28 | 29 | return Promise.all([ 30 | expect(pass).to.be.fulfilled, 31 | expect(fail).to.not.be.fulfilled 32 | ]) 33 | }) 34 | 35 | it('should have chai-enzyme working', function() { 36 | let wrapper = shallow() 37 | expect(wrapper.find('#checked')).to.be.checked() 38 | 39 | wrapper = mount() 40 | expect(wrapper.find('#checked')).to.be.checked() 41 | 42 | wrapper = render() 43 | expect(wrapper.find('#checked')).to.be.checked() 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /tests/layouts/CoreLayout.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import assert from 'power-assert' 4 | import CoreLayout from 'layouts/CoreLayout/CoreLayout' 5 | import { getMuiTheme } from 'material-ui/styles' 6 | 7 | describe('(Layout) Core', function () { 8 | const muiTheme = getMuiTheme() 9 | 10 | it('Should render.', function () { 11 | const wrapper = shallow(, { context: { muiTheme } }) 12 | assert(wrapper.first().is('CoreLayout')) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /tests/redux/modules/counter.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | COUNTER_INCREMENT, 3 | increment, 4 | doubleAsync, 5 | default as counterReducer 6 | } from 'redux/modules/counter' 7 | 8 | describe('(Redux Module) Counter', function () { 9 | it('Should export a constant COUNTER_INCREMENT.', function () { 10 | expect(COUNTER_INCREMENT).to.equal('COUNTER_INCREMENT') 11 | }) 12 | 13 | describe('(Reducer)', function () { 14 | it('Should be a function.', function () { 15 | expect(counterReducer).to.be.a('function') 16 | }) 17 | 18 | it('Should initialize with a state of 0 (Number).', function () { 19 | expect(counterReducer(undefined, {})).to.equal(0) 20 | }) 21 | 22 | it('Should return the previous state if an action was not matched.', function () { 23 | let state = counterReducer(undefined, {}) 24 | expect(state).to.equal(0) 25 | state = counterReducer(state, {type: '@@@@@@@'}) 26 | expect(state).to.equal(0) 27 | state = counterReducer(state, increment(5)) 28 | expect(state).to.equal(5) 29 | state = counterReducer(state, {type: '@@@@@@@'}) 30 | expect(state).to.equal(5) 31 | }) 32 | }) 33 | 34 | describe('(Action Creator) increment', function () { 35 | it('Should be exported as a function.', function () { 36 | expect(increment).to.be.a('function') 37 | }) 38 | 39 | it('Should return an action with type "COUNTER_INCREMENT".', function () { 40 | expect(increment()).to.have.property('type', COUNTER_INCREMENT) 41 | }) 42 | 43 | it('Should assign the first argument to the "payload" property.', function () { 44 | expect(increment(5)).to.have.property('payload', 5) 45 | }) 46 | 47 | it('Should default the "payload" property to 1 if not provided.', function () { 48 | expect(increment()).to.have.property('payload', 1) 49 | }) 50 | }) 51 | 52 | describe('(Action Creator) doubleAsync', function () { 53 | let _globalState 54 | let _dispatchSpy 55 | let _getStateSpy 56 | 57 | beforeEach(function () { 58 | _globalState = { 59 | counter: counterReducer(undefined, {}) 60 | } 61 | _dispatchSpy = sinon.spy((action) => { 62 | _globalState = { 63 | ..._globalState, 64 | counter: counterReducer(_globalState.counter, action) 65 | } 66 | }) 67 | _getStateSpy = sinon.spy(() => { 68 | return _globalState 69 | }) 70 | }) 71 | 72 | it('Should be exported as a function.', function () { 73 | expect(doubleAsync).to.be.a('function') 74 | }) 75 | 76 | it('Should return a function (is a thunk).', function () { 77 | expect(doubleAsync()).to.be.a('function') 78 | }) 79 | 80 | it('Should return a promise from that thunk that gets fulfilled.', function () { 81 | return doubleAsync()(_dispatchSpy, _getStateSpy).should.eventually.be.fulfilled 82 | }) 83 | 84 | it('Should call dispatch and getState exactly once.', function () { 85 | return doubleAsync()(_dispatchSpy, _getStateSpy) 86 | .then(() => { 87 | _dispatchSpy.should.have.been.calledOnce 88 | _getStateSpy.should.have.been.calledOnce 89 | }) 90 | }) 91 | 92 | it('Should produce a state that is double the previous state.', function () { 93 | _globalState = { counter: 2 } 94 | 95 | return doubleAsync()(_dispatchSpy, _getStateSpy) 96 | .then(() => { 97 | _dispatchSpy.should.have.been.calledOnce 98 | _getStateSpy.should.have.been.calledOnce 99 | expect(_globalState.counter).to.equal(4) 100 | return doubleAsync()(_dispatchSpy, _getStateSpy) 101 | }) 102 | .then(() => { 103 | _dispatchSpy.should.have.been.calledTwice 104 | _getStateSpy.should.have.been.calledTwice 105 | expect(_globalState.counter).to.equal(8) 106 | }) 107 | }) 108 | }) 109 | 110 | // NOTE: if you have a more complex state, you will probably want to verify 111 | // that you did not mutate the state. In this case our state is just a number 112 | // (which cannot be mutated). 113 | describe('(Action Handler) COUNTER_INCREMENT', function () { 114 | it('Should increment the state by the action payload\'s "value" property.', function () { 115 | let state = counterReducer(undefined, {}) 116 | expect(state).to.equal(0) 117 | state = counterReducer(state, increment(1)) 118 | expect(state).to.equal(1) 119 | state = counterReducer(state, increment(2)) 120 | expect(state).to.equal(3) 121 | state = counterReducer(state, increment(-3)) 122 | expect(state).to.equal(0) 123 | }) 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /tests/test-bundler.js: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Test Environment Setup 3 | // --------------------------------------- 4 | import sinon from 'sinon' 5 | import chai from 'chai' 6 | import sinonChai from 'sinon-chai' 7 | import chaiAsPromised from 'chai-as-promised' 8 | import chaiEnzyme from 'chai-enzyme' 9 | 10 | chai.use(sinonChai) 11 | chai.use(chaiAsPromised) 12 | chai.use(chaiEnzyme()) 13 | 14 | global.chai = chai 15 | global.sinon = sinon 16 | global.expect = chai.expect 17 | global.should = chai.should() 18 | 19 | // --------------------------------------- 20 | // Require Tests 21 | // --------------------------------------- 22 | // for use with karma-webpack-with-fast-source-maps 23 | // NOTE: `new Array()` is used rather than an array literal since 24 | // for some reason an array literal without a trailing `;` causes 25 | // some build environments to fail. 26 | const __karmaWebpackManifest__ = new Array() // eslint-disable-line 27 | const inManifest = (path) => ~__karmaWebpackManifest__.indexOf(path) 28 | 29 | // require all `tests/**/*.spec.js` 30 | const testsContext = require.context('./', true, /\.spec\.js$/) 31 | 32 | // only run tests that have changed after the first pass. 33 | const testsToRun = testsContext.keys().filter(inManifest) 34 | ;(testsToRun.length ? testsToRun : testsContext.keys()).forEach(testsContext) 35 | 36 | // require all `src/**/*.js` except for `main.js` (for isparta coverage reporting) 37 | const componentsContext = require.context('../src/', true, /^((?!main).)*\.js$/) 38 | 39 | componentsContext.keys().forEach(componentsContext) 40 | -------------------------------------------------------------------------------- /tests/views/HomeView.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow, mount } from 'enzyme' 3 | import assert from 'power-assert' 4 | import { HomeView } from 'views/HomeView/HomeView' 5 | import { getMuiTheme } from 'material-ui/styles' 6 | 7 | describe('(View) Home', function () { 8 | const muiTheme = getMuiTheme() 9 | 10 | it('Should render as a
.', function () { 11 | const wrapper = shallow(, { context: { muiTheme } }) 12 | assert(wrapper.is('div')) 13 | }) 14 | 15 | }) 16 | -------------------------------------------------------------------------------- /tests/views/ReactSample.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { shallow, mount } from 'enzyme' 4 | import { spy } from 'sinon' 5 | import assert from 'power-assert' 6 | import ReactSample from 'views/ReactSample' 7 | import TodoItem from 'views/ReactSample/TodoItem' 8 | 9 | describe('ReactSample', function () { 10 | 11 | it('Should render as a
.', function () { 12 | const wrapper = shallow() 13 | assert(wrapper.is('div')) 14 | }) 15 | 16 | it('Should include an

with \'React Sample\' text.', function () { 17 | const wrapper = shallow() 18 | assert(wrapper.childAt(0).type() === 'h1') 19 | assert(wrapper.childAt(0).text() === 'React Sample') 20 | }) 21 | 22 | it('Should render with an

with \'Todos\' text.', function () { 23 | const wrapper = shallow() 24 | const h2 = wrapper.find('h2') 25 | assert(h2.length === 1) 26 | assert(h2.text() === 'Todos') 27 | }) 28 | 29 | it('should start with three todos list', () => { 30 | const wrapper = shallow() 31 | assert.deepEqual(wrapper.state('todos'), ['learn react', 'learn flux', 'learn redux']) 32 | }) 33 | 34 | it('edit input text box', () => { 35 | const wrapper = shallow() 36 | const input = wrapper.find('input') 37 | input.simulate('change', { target: { value: 'learn mocha' } }) 38 | assert.equal(wrapper.state('newTodo'), 'learn mocha') 39 | }) 40 | 41 | it('adds items to the list', () => { 42 | const wrapper = shallow() 43 | wrapper.setState({ newTodo: 'learn enzyme' }) 44 | wrapper.instance().addTodo() 45 | assert.deepEqual(wrapper.state('todos'), ['learn react', 'learn flux', 'learn redux', 'learn enzyme']) 46 | }) 47 | 48 | it('passes addTodo to button', () => { 49 | const wrapper = shallow() 50 | const button = wrapper.find('button') 51 | const addTodo = wrapper.instance().addTodo 52 | assert(button.prop('onClick'), addTodo) 53 | }) 54 | 55 | it('passes a bound addItem function to button', () => { 56 | const wrapper = shallow() 57 | wrapper.setState({ newTodo: 'learn enzyme'}) 58 | const button = wrapper.find('button') 59 | button.prop('onClick')('learn enzyme') 60 | assert.deepEqual(wrapper.state('todos'), ['learn react', 'learn flux', 'learn redux', 'learn enzyme']) 61 | }) 62 | 63 | it('should render three items', () => { 64 | const wrapper = shallow() 65 | assert(wrapper.find(TodoItem).length === 3) 66 | }) 67 | 68 | it('passes text to TodoItem', () => { 69 | const wrapper = shallow() 70 | 71 | assert(wrapper.find(TodoItem).length === 3) 72 | assert(wrapper.find(TodoItem).at(0).props().children === 'learn react') 73 | assert(wrapper.find(TodoItem).at(1).props().children === 'learn flux') 74 | assert(wrapper.find(TodoItem).at(2).props().children === 'learn redux') 75 | }) 76 | 77 | it('passes deleteTodoItem to TodoItem', () => { 78 | const wrapper = shallow() 79 | const todoItem = wrapper.find(TodoItem) 80 | const deleteTodoItem = wrapper.instance().deleteTodoItem 81 | assert(todoItem.everyWhere(n => n.prop('onRemove') === deleteTodoItem)) 82 | }) 83 | 84 | it('passes a bound deleteTodoItem function to TodoItem', () => { 85 | const wrapper = shallow() 86 | const firstTodoItem = wrapper.find(TodoItem).first() 87 | firstTodoItem.prop('onRemove')(0) 88 | assert.deepEqual(wrapper.state('todos'), ['learn flux', 'learn redux']) 89 | }) 90 | 91 | it('should render todo texts', () => { 92 | const wrapper = mount() 93 | 94 | assert(wrapper.find('div').at(1).text() === '1: learn react ') 95 | assert(wrapper.find('div').at(2).text() === '2: learn flux ') 96 | assert(wrapper.find('div').at(3).text() === '3: learn redux ') 97 | }) 98 | 99 | it('onRemove callback is called if x button is clicked', () => { 100 | const onButtonClick = spy() 101 | const wrapper = mount() 102 | 103 | wrapper.find('input').simulate('click') 104 | assert(onButtonClick.calledOnce) 105 | }) 106 | }) 107 | --------------------------------------------------------------------------------