├── .gitignore ├── README.md ├── config ├── babel.dev.js ├── babel.prod.js ├── eslint.js ├── flow │ ├── css.js.flow │ └── file.js.flow ├── paths.js ├── polyfills.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── favicon.ico ├── images └── demo.gif ├── index.html ├── package.json ├── scripts ├── build.js ├── start.js └── utils │ ├── chrome.applescript │ ├── detectPort.js │ └── prompt.js └── src ├── App.css ├── App.js ├── actions ├── action-types.js └── people-actions.js ├── components ├── PeopleContainer.js ├── PeopleList.js ├── Person.js └── PersonInput.js ├── index.css ├── index.js ├── logo.svg ├── reducers ├── index.js └── people-reducer.js └── store └── configure-store.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | 13 | # typings definitions (used with VS Code) 14 | typings 15 | 16 | # jsconfig for intellisense in VS Code 17 | jsconfig.json 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `create-react-app` ... with Redux 2 | 3 | This is a barebones implementation of Redux with a React app that was generated with `create-react-app`, and then ejected with `npm run eject`. 4 | 5 | :bulb: This repository consists of two commits and only two commits. 6 | 7 | 1. **first commit** is the base code of a `create-react-app` app source code after eject (`npm run eject`) 8 | 2. **second commit** are the sample additions to implement basic redux 9 | 10 | :bulb: :bulb: It is worth noting that it is **not** required to run `npm run eject` in order to get Redux implemented in this app. Implementing Redux in the app without ejecting is completely possible 11 | 12 | ![demo](images/demo.gif) 13 | -------------------------------------------------------------------------------- /config/babel.dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | babelrc: false, 3 | cacheDirectory: true, 4 | presets: [ 5 | 'babel-preset-es2015', 6 | 'babel-preset-es2016', 7 | 'babel-preset-react' 8 | ].map(require.resolve), 9 | plugins: [ 10 | 'babel-plugin-syntax-trailing-function-commas', 11 | 'babel-plugin-transform-class-properties', 12 | 'babel-plugin-transform-object-rest-spread' 13 | ].map(require.resolve).concat([ 14 | [require.resolve('babel-plugin-transform-runtime'), { 15 | helpers: false, 16 | polyfill: false, 17 | regenerator: true 18 | }] 19 | ]) 20 | }; 21 | -------------------------------------------------------------------------------- /config/babel.prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | babelrc: false, 3 | presets: [ 4 | 'babel-preset-es2015', 5 | 'babel-preset-es2016', 6 | 'babel-preset-react' 7 | ].map(require.resolve), 8 | plugins: [ 9 | 'babel-plugin-syntax-trailing-function-commas', 10 | 'babel-plugin-transform-class-properties', 11 | 'babel-plugin-transform-object-rest-spread', 12 | 'babel-plugin-transform-react-constant-elements', 13 | ].map(require.resolve).concat([ 14 | [require.resolve('babel-plugin-transform-runtime'), { 15 | helpers: false, 16 | polyfill: false, 17 | regenerator: true 18 | }] 19 | ]) 20 | }; 21 | -------------------------------------------------------------------------------- /config/eslint.js: -------------------------------------------------------------------------------- 1 | // Inspired by https://github.com/airbnb/javascript but less opinionated. 2 | 3 | // We use eslint-loader so even warnings are very visibile. 4 | // This is why we only use "WARNING" level for potential errors, 5 | // and we don't use "ERROR" level at all. 6 | 7 | // In the future, we might create a separate list of rules for production. 8 | // It would probably be more strict. 9 | 10 | module.exports = { 11 | root: true, 12 | 13 | parser: 'babel-eslint', 14 | 15 | // import plugin is termporarily disabled, scroll below to see why 16 | plugins: [/*'import', */'flowtype', 'jsx-a11y', 'react'], 17 | 18 | env: { 19 | browser: true, 20 | commonjs: true, 21 | es6: true, 22 | node: true 23 | }, 24 | 25 | parserOptions: { 26 | ecmaVersion: 6, 27 | sourceType: 'module', 28 | ecmaFeatures: { 29 | jsx: true, 30 | generators: true, 31 | experimentalObjectRestSpread: true 32 | } 33 | }, 34 | 35 | settings: { 36 | 'import/ignore': [ 37 | 'node_modules', 38 | '\\.(json|css|jpg|png|gif|eot|svg|ttf|woff|woff2|mp4|webm)$', 39 | ], 40 | 'import/extensions': ['.js'], 41 | 'import/resolver': { 42 | node: { 43 | extensions: ['.js', '.json'] 44 | } 45 | } 46 | }, 47 | 48 | rules: { 49 | // http://eslint.org/docs/rules/ 50 | 'array-callback-return': 'warn', 51 | 'default-case': ['warn', { commentPattern: '^no default$' }], 52 | 'dot-location': ['warn', 'property'], 53 | eqeqeq: ['warn', 'allow-null'], 54 | 'guard-for-in': 'warn', 55 | 'new-cap': ['warn', { newIsCap: true }], 56 | 'new-parens': 'warn', 57 | 'no-array-constructor': 'warn', 58 | 'no-caller': 'warn', 59 | 'no-cond-assign': ['warn', 'always'], 60 | 'no-const-assign': 'warn', 61 | 'no-control-regex': 'warn', 62 | 'no-delete-var': 'warn', 63 | 'no-dupe-args': 'warn', 64 | 'no-dupe-class-members': 'warn', 65 | 'no-dupe-keys': 'warn', 66 | 'no-duplicate-case': 'warn', 67 | 'no-empty-character-class': 'warn', 68 | 'no-empty-pattern': 'warn', 69 | 'no-eval': 'warn', 70 | 'no-ex-assign': 'warn', 71 | 'no-extend-native': 'warn', 72 | 'no-extra-bind': 'warn', 73 | 'no-extra-label': 'warn', 74 | 'no-fallthrough': 'warn', 75 | 'no-func-assign': 'warn', 76 | 'no-implied-eval': 'warn', 77 | 'no-invalid-regexp': 'warn', 78 | 'no-iterator': 'warn', 79 | 'no-label-var': 'warn', 80 | 'no-labels': ['warn', { allowLoop: false, allowSwitch: false }], 81 | 'no-lone-blocks': 'warn', 82 | 'no-loop-func': 'warn', 83 | 'no-mixed-operators': ['warn', { 84 | groups: [ 85 | ['&', '|', '^', '~', '<<', '>>', '>>>'], 86 | ['==', '!=', '===', '!==', '>', '>=', '<', '<='], 87 | ['&&', '||'], 88 | ['in', 'instanceof'] 89 | ], 90 | allowSamePrecedence: false 91 | }], 92 | 'no-multi-str': 'warn', 93 | 'no-native-reassign': 'warn', 94 | 'no-negated-in-lhs': 'warn', 95 | 'no-new-func': 'warn', 96 | 'no-new-object': 'warn', 97 | 'no-new-symbol': 'warn', 98 | 'no-new-wrappers': 'warn', 99 | 'no-obj-calls': 'warn', 100 | 'no-octal': 'warn', 101 | 'no-octal-escape': 'warn', 102 | 'no-redeclare': 'warn', 103 | 'no-regex-spaces': 'warn', 104 | 'no-restricted-syntax': [ 105 | 'warn', 106 | 'LabeledStatement', 107 | 'WithStatement', 108 | ], 109 | 'no-return-assign': 'warn', 110 | 'no-script-url': 'warn', 111 | 'no-self-assign': 'warn', 112 | 'no-self-compare': 'warn', 113 | 'no-sequences': 'warn', 114 | 'no-shadow-restricted-names': 'warn', 115 | 'no-sparse-arrays': 'warn', 116 | 'no-this-before-super': 'warn', 117 | 'no-throw-literal': 'warn', 118 | 'no-undef': 'warn', 119 | 'no-unexpected-multiline': 'warn', 120 | 'no-unreachable': 'warn', 121 | 'no-unused-expressions': 'warn', 122 | 'no-unused-labels': 'warn', 123 | 'no-unused-vars': ['warn', { vars: 'local', args: 'none' }], 124 | 'no-use-before-define': ['warn', 'nofunc'], 125 | 'no-useless-computed-key': 'warn', 126 | 'no-useless-concat': 'warn', 127 | 'no-useless-constructor': 'warn', 128 | 'no-useless-escape': 'warn', 129 | 'no-useless-rename': ['warn', { 130 | ignoreDestructuring: false, 131 | ignoreImport: false, 132 | ignoreExport: false, 133 | }], 134 | 'no-with': 'warn', 135 | 'no-whitespace-before-property': 'warn', 136 | 'operator-assignment': ['warn', 'always'], 137 | radix: 'warn', 138 | 'require-yield': 'warn', 139 | 'rest-spread-spacing': ['warn', 'never'], 140 | strict: ['warn', 'never'], 141 | 'unicode-bom': ['warn', 'never'], 142 | 'use-isnan': 'warn', 143 | 'valid-typeof': 'warn', 144 | 145 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/ 146 | 147 | // TODO: import rules are temporarily disabled because they don't play well 148 | // with how eslint-loader only checks the file you change. So if module A 149 | // imports module B, and B is missing a default export, the linter will 150 | // record this as an issue in module A. Now if you fix module B, the linter 151 | // will not be aware that it needs to re-lint A as well, so the error 152 | // will stay until the next restart, which is really confusing. 153 | 154 | // This is probably fixable with a patch to eslint-loader. 155 | // When file A is saved, we want to invalidate all files that import it 156 | // *and* that currently have lint errors. This should fix the problem. 157 | 158 | // 'import/default': 'warn', 159 | // 'import/export': 'warn', 160 | // 'import/named': 'warn', 161 | // 'import/namespace': 'warn', 162 | // 'import/no-amd': 'warn', 163 | // 'import/no-duplicates': 'warn', 164 | // 'import/no-extraneous-dependencies': 'warn', 165 | // 'import/no-named-as-default': 'warn', 166 | // 'import/no-named-as-default-member': 'warn', 167 | // 'import/no-unresolved': ['warn', { commonjs: true }], 168 | 169 | // https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules 170 | 'react/jsx-equals-spacing': ['warn', 'never'], 171 | 'react/jsx-no-duplicate-props': ['warn', { ignoreCase: true }], 172 | 'react/jsx-no-undef': 'warn', 173 | 'react/jsx-pascal-case': ['warn', { 174 | allowAllCaps: true, 175 | ignore: [], 176 | }], 177 | 'react/jsx-uses-react': 'warn', 178 | 'react/jsx-uses-vars': 'warn', 179 | 'react/no-deprecated': 'warn', 180 | 'react/no-direct-mutation-state': 'warn', 181 | 'react/no-is-mounted': 'warn', 182 | 'react/react-in-jsx-scope': 'warn', 183 | 'react/require-render-return': 'warn', 184 | 185 | // https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules 186 | 'jsx-a11y/aria-role': 'warn', 187 | 'jsx-a11y/img-has-alt': 'warn', 188 | 'jsx-a11y/img-redundant-alt': 'warn', 189 | 'jsx-a11y/no-access-key': 'warn', 190 | 191 | // https://github.com/gajus/eslint-plugin-flowtype 192 | 'flowtype/define-flow-type': 'warn', 193 | 'flowtype/require-valid-file-annotation': 'warn', 194 | 'flowtype/use-flow-type': 'warn' 195 | } 196 | }; 197 | -------------------------------------------------------------------------------- /config/flow/css.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | -------------------------------------------------------------------------------- /config/flow/file.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | declare export default string; 3 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | // TODO: we can split this file into several files (pre-eject, post-eject, test) 2 | // and use those instead. This way we don't need to branch here. 3 | 4 | var path = require('path'); 5 | 6 | // True after ejecting, false when used as a dependency 7 | var isEjected = ( 8 | path.resolve(path.join(__dirname, '..')) === 9 | path.resolve(process.cwd()) 10 | ); 11 | 12 | // Are we developing create-react-app locally? 13 | var isInCreateReactAppSource = ( 14 | process.argv.some(arg => arg.indexOf('--debug-template') > -1) 15 | ); 16 | 17 | function resolveOwn(relativePath) { 18 | return path.resolve(__dirname, relativePath); 19 | } 20 | 21 | function resolveApp(relativePath) { 22 | return path.resolve(relativePath); 23 | } 24 | 25 | if (isInCreateReactAppSource) { 26 | // create-react-app development: we're in ./config/ 27 | module.exports = { 28 | appBuild: resolveOwn('../build'), 29 | appHtml: resolveOwn('../template/index.html'), 30 | appFavicon: resolveOwn('../template/favicon.ico'), 31 | appPackageJson: resolveOwn('../package.json'), 32 | appSrc: resolveOwn('../template/src'), 33 | appNodeModules: resolveOwn('../node_modules'), 34 | ownNodeModules: resolveOwn('../node_modules') 35 | }; 36 | } else if (!isEjected) { 37 | // before eject: we're in ./node_modules/react-scripts/config/ 38 | module.exports = { 39 | appBuild: resolveApp('build'), 40 | appHtml: resolveApp('index.html'), 41 | appFavicon: resolveApp('favicon.ico'), 42 | appPackageJson: resolveApp('package.json'), 43 | appSrc: resolveApp('src'), 44 | appNodeModules: resolveApp('node_modules'), 45 | // this is empty with npm3 but node resolution searches higher anyway: 46 | ownNodeModules: resolveOwn('../node_modules') 47 | }; 48 | } else { 49 | // after eject: we're in ./config/ 50 | module.exports = { 51 | appBuild: resolveApp('build'), 52 | appHtml: resolveApp('index.html'), 53 | appFavicon: resolveApp('favicon.ico'), 54 | appPackageJson: resolveApp('package.json'), 55 | appSrc: resolveApp('src'), 56 | appNodeModules: resolveApp('node_modules'), 57 | ownNodeModules: resolveApp('node_modules') 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | if (typeof Promise === 'undefined') { 2 | // Rejection tracking prevents a common issue where React gets into an 3 | // inconsistent state due to an error, but it gets swallowed by a Promise, 4 | // and the user has no idea what causes React's erratic future behavior. 5 | require('promise/lib/rejection-tracking').enable(); 6 | window.Promise = require('promise/lib/es6-extensions.js'); 7 | } 8 | 9 | require('whatwg-fetch'); 10 | -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var autoprefixer = require('autoprefixer'); 3 | var webpack = require('webpack'); 4 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); 6 | var paths = require('./paths'); 7 | 8 | module.exports = { 9 | devtool: 'eval', 10 | entry: [ 11 | require.resolve('webpack-dev-server/client') + '?/', 12 | require.resolve('webpack/hot/dev-server'), 13 | require.resolve('./polyfills'), 14 | path.join(paths.appSrc, 'index') 15 | ], 16 | output: { 17 | // Next line is not used in dev but WebpackDevServer crashes without it: 18 | path: paths.appBuild, 19 | pathinfo: true, 20 | filename: 'static/js/bundle.js', 21 | publicPath: '/' 22 | }, 23 | resolve: { 24 | extensions: ['', '.js', '.json'], 25 | alias: { 26 | // This `alias` section can be safely removed after ejection. 27 | // We do this because `babel-runtime` may be inside `react-scripts`, 28 | // so when `babel-plugin-transform-runtime` imports it, it will not be 29 | // available to the app directly. This is a temporary solution that lets 30 | // us ship support for generators. However it is far from ideal, and 31 | // if we don't have a good solution, we should just make `babel-runtime` 32 | // a dependency in generated projects. 33 | // See https://github.com/facebookincubator/create-react-app/issues/255 34 | 'babel-runtime/regenerator': require.resolve('babel-runtime/regenerator') 35 | } 36 | }, 37 | resolveLoader: { 38 | root: paths.ownNodeModules, 39 | moduleTemplates: ['*-loader'] 40 | }, 41 | module: { 42 | preLoaders: [ 43 | { 44 | test: /\.js$/, 45 | loader: 'eslint', 46 | include: paths.appSrc, 47 | } 48 | ], 49 | loaders: [ 50 | { 51 | test: /\.js$/, 52 | include: paths.appSrc, 53 | loader: 'babel', 54 | query: require('./babel.dev') 55 | }, 56 | { 57 | test: /\.css$/, 58 | include: [paths.appSrc, paths.appNodeModules], 59 | loader: 'style!css!postcss' 60 | }, 61 | { 62 | test: /\.json$/, 63 | include: [paths.appSrc, paths.appNodeModules], 64 | loader: 'json' 65 | }, 66 | { 67 | test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/, 68 | include: [paths.appSrc, paths.appNodeModules], 69 | loader: 'file', 70 | query: { 71 | name: 'static/media/[name].[ext]' 72 | } 73 | }, 74 | { 75 | test: /\.(mp4|webm)(\?.*)?$/, 76 | include: [paths.appSrc, paths.appNodeModules], 77 | loader: 'url', 78 | query: { 79 | limit: 10000, 80 | name: 'static/media/[name].[ext]' 81 | } 82 | } 83 | ] 84 | }, 85 | eslint: { 86 | configFile: path.join(__dirname, 'eslint.js'), 87 | useEslintrc: false 88 | }, 89 | postcss: function() { 90 | return [autoprefixer]; 91 | }, 92 | plugins: [ 93 | new HtmlWebpackPlugin({ 94 | inject: true, 95 | template: paths.appHtml, 96 | favicon: paths.appFavicon, 97 | }), 98 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"development"' }), 99 | // Note: only CSS is currently hot reloaded 100 | new webpack.HotModuleReplacementPlugin(), 101 | new CaseSensitivePathsPlugin() 102 | ] 103 | }; 104 | -------------------------------------------------------------------------------- /config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var autoprefixer = require('autoprefixer'); 3 | var webpack = require('webpack'); 4 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | var url = require('url'); 7 | var paths = require('./paths'); 8 | 9 | var homepagePath = require(paths.appPackageJson).homepage; 10 | var publicPath = homepagePath ? url.parse(homepagePath).pathname : '/'; 11 | if (!publicPath.endsWith('/')) { 12 | // Prevents incorrect paths in file-loader 13 | publicPath += '/'; 14 | } 15 | 16 | module.exports = { 17 | bail: true, 18 | devtool: 'source-map', 19 | entry: [ 20 | require.resolve('./polyfills'), 21 | path.join(paths.appSrc, 'index') 22 | ], 23 | output: { 24 | path: paths.appBuild, 25 | filename: 'static/js/[name].[chunkhash:8].js', 26 | chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js', 27 | publicPath: publicPath 28 | }, 29 | resolve: { 30 | extensions: ['', '.js', '.json'], 31 | alias: { 32 | // This `alias` section can be safely removed after ejection. 33 | // We do this because `babel-runtime` may be inside `react-scripts`, 34 | // so when `babel-plugin-transform-runtime` imports it, it will not be 35 | // available to the app directly. This is a temporary solution that lets 36 | // us ship support for generators. However it is far from ideal, and 37 | // if we don't have a good solution, we should just make `babel-runtime` 38 | // a dependency in generated projects. 39 | // See https://github.com/facebookincubator/create-react-app/issues/255 40 | 'babel-runtime/regenerator': require.resolve('babel-runtime/regenerator') 41 | } 42 | }, 43 | resolveLoader: { 44 | root: paths.ownNodeModules, 45 | moduleTemplates: ['*-loader'] 46 | }, 47 | module: { 48 | preLoaders: [ 49 | { 50 | test: /\.js$/, 51 | loader: 'eslint', 52 | include: paths.appSrc 53 | } 54 | ], 55 | loaders: [ 56 | { 57 | test: /\.js$/, 58 | include: paths.appSrc, 59 | loader: 'babel', 60 | query: require('./babel.prod') 61 | }, 62 | { 63 | test: /\.css$/, 64 | include: [paths.appSrc, paths.appNodeModules], 65 | // Disable autoprefixer in css-loader itself: 66 | // https://github.com/webpack/css-loader/issues/281 67 | // We already have it thanks to postcss. 68 | loader: ExtractTextPlugin.extract('style', 'css?-autoprefixer!postcss') 69 | }, 70 | { 71 | test: /\.json$/, 72 | include: [paths.appSrc, paths.appNodeModules], 73 | loader: 'json' 74 | }, 75 | { 76 | test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/, 77 | include: [paths.appSrc, paths.appNodeModules], 78 | loader: 'file', 79 | query: { 80 | name: 'static/media/[name].[hash:8].[ext]' 81 | } 82 | }, 83 | { 84 | test: /\.(mp4|webm)(\?.*)?$/, 85 | include: [paths.appSrc, paths.appNodeModules], 86 | loader: 'url', 87 | query: { 88 | limit: 10000, 89 | name: 'static/media/[name].[hash:8].[ext]' 90 | } 91 | } 92 | ] 93 | }, 94 | eslint: { 95 | // TODO: consider separate config for production, 96 | // e.g. to enable no-console and no-debugger only in prod. 97 | configFile: path.join(__dirname, 'eslint.js'), 98 | useEslintrc: false 99 | }, 100 | postcss: function() { 101 | return [autoprefixer]; 102 | }, 103 | plugins: [ 104 | new HtmlWebpackPlugin({ 105 | inject: true, 106 | template: paths.appHtml, 107 | favicon: paths.appFavicon, 108 | minify: { 109 | removeComments: true, 110 | collapseWhitespace: true, 111 | removeRedundantAttributes: true, 112 | useShortDoctype: true, 113 | removeEmptyAttributes: true, 114 | removeStyleLinkTypeAttributes: true, 115 | keepClosingSlash: true, 116 | minifyJS: true, 117 | minifyCSS: true, 118 | minifyURLs: true 119 | } 120 | }), 121 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }), 122 | new webpack.optimize.OccurrenceOrderPlugin(), 123 | new webpack.optimize.DedupePlugin(), 124 | new webpack.optimize.UglifyJsPlugin({ 125 | compress: { 126 | screw_ie8: true, 127 | warnings: false 128 | }, 129 | mangle: { 130 | screw_ie8: true 131 | }, 132 | output: { 133 | comments: false, 134 | screw_ie8: true 135 | } 136 | }), 137 | new ExtractTextPlugin('static/css/[name].[contenthash:8].css') 138 | ] 139 | }; 140 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trstringer/create-react-app-with-redux/1966b89cbe609ce3794760ea41111252d1e70a8a/favicon.ico -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trstringer/create-react-app-with-redux/1966b89cbe609ce3794760ea41111252d1e70a8a/images/demo.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React App 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-app-with-redux", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "autoprefixer": "6.3.7", 7 | "babel-core": "6.11.4", 8 | "babel-eslint": "6.1.2", 9 | "babel-loader": "6.2.4", 10 | "babel-plugin-syntax-trailing-function-commas": "6.8.0", 11 | "babel-plugin-transform-class-properties": "6.11.5", 12 | "babel-plugin-transform-object-rest-spread": "6.8.0", 13 | "babel-plugin-transform-react-constant-elements": "6.9.1", 14 | "babel-plugin-transform-runtime": "6.12.0", 15 | "babel-preset-es2015": "6.9.0", 16 | "babel-preset-es2016": "6.11.3", 17 | "babel-preset-react": "6.11.1", 18 | "babel-runtime": "6.11.6", 19 | "case-sensitive-paths-webpack-plugin": "1.1.2", 20 | "chalk": "1.1.3", 21 | "cross-spawn": "4.0.0", 22 | "css-loader": "0.23.1", 23 | "eslint": "3.1.1", 24 | "eslint-loader": "1.4.1", 25 | "eslint-plugin-flowtype": "2.4.0", 26 | "eslint-plugin-import": "1.12.0", 27 | "eslint-plugin-jsx-a11y": "2.0.1", 28 | "eslint-plugin-react": "5.2.2", 29 | "extract-text-webpack-plugin": "1.0.1", 30 | "file-loader": "0.9.0", 31 | "filesize": "3.3.0", 32 | "fs-extra": "0.30.0", 33 | "gzip-size": "3.0.0", 34 | "html-webpack-plugin": "2.22.0", 35 | "json-loader": "0.5.4", 36 | "opn": "4.0.2", 37 | "postcss-loader": "0.9.1", 38 | "promise": "7.1.1", 39 | "rimraf": "2.5.4", 40 | "style-loader": "0.13.1", 41 | "url-loader": "0.5.7", 42 | "webpack": "1.13.1", 43 | "webpack-dev-server": "1.14.1", 44 | "whatwg-fetch": "1.0.0" 45 | }, 46 | "dependencies": { 47 | "react": "^15.2.1", 48 | "react-dom": "^15.2.1", 49 | "react-redux": "^4.4.5", 50 | "redux": "^3.5.2" 51 | }, 52 | "scripts": { 53 | "start": "node ./scripts/start.js", 54 | "build": "node ./scripts/build.js" 55 | }, 56 | "eslintConfig": { 57 | "extends": "./config/eslint.js" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production'; 2 | 3 | var chalk = require('chalk'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var filesize = require('filesize'); 7 | var gzipSize = require('gzip-size').sync; 8 | var rimrafSync = require('rimraf').sync; 9 | var webpack = require('webpack'); 10 | var config = require('../config/webpack.config.prod'); 11 | var paths = require('../config/paths'); 12 | 13 | // Remove all content but keep the directory so that 14 | // if you're in it, you don't end up in Trash 15 | rimrafSync(paths.appBuild + '/*'); 16 | 17 | console.log('Creating an optimized production build...'); 18 | webpack(config).run(function(err, stats) { 19 | if (err) { 20 | console.error('Failed to create a production build. Reason:'); 21 | console.error(err.message || err); 22 | process.exit(1); 23 | } 24 | 25 | console.log(chalk.green('Compiled successfully.')); 26 | console.log(); 27 | 28 | console.log('File sizes after gzip:'); 29 | console.log(); 30 | var assets = stats.toJson().assets 31 | .filter(asset => /\.(js|css)$/.test(asset.name)) 32 | .map(asset => { 33 | var fileContents = fs.readFileSync(paths.appBuild + '/' + asset.name); 34 | var size = gzipSize(fileContents); 35 | return { 36 | folder: path.join('build', path.dirname(asset.name)), 37 | name: path.basename(asset.name), 38 | size: size, 39 | sizeLabel: filesize(size) 40 | }; 41 | }); 42 | assets.sort((a, b) => b.size - a.size); 43 | 44 | var longestSizeLabelLength = Math.max.apply(null, 45 | assets.map(a => a.sizeLabel.length) 46 | ); 47 | assets.forEach(asset => { 48 | var sizeLabel = asset.sizeLabel; 49 | if (sizeLabel.length < longestSizeLabelLength) { 50 | var rightPadding = ' '.repeat(longestSizeLabelLength - sizeLabel.length); 51 | sizeLabel += rightPadding; 52 | } 53 | console.log( 54 | ' ' + chalk.green(sizeLabel) + 55 | ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name) 56 | ); 57 | }); 58 | console.log(); 59 | 60 | var openCommand = process.platform === 'win32' ? 'start' : 'open'; 61 | var homepagePath = require(paths.appPackageJson).homepage; 62 | if (homepagePath) { 63 | console.log('You can now publish them at ' + homepagePath + '.'); 64 | console.log('For example, if you use GitHub Pages:'); 65 | console.log(); 66 | console.log(' git commit -am "Save local changes"'); 67 | console.log(' git checkout -B gh-pages'); 68 | console.log(' git add -f build'); 69 | console.log(' git commit -am "Rebuild website"'); 70 | console.log(' git filter-branch -f --prune-empty --subdirectory-filter build'); 71 | console.log(' git push -f origin gh-pages'); 72 | console.log(' git checkout -'); 73 | console.log(); 74 | } else { 75 | console.log('You can now serve them with any static server.'); 76 | console.log('For example:'); 77 | console.log(); 78 | console.log(' npm install -g pushstate-server'); 79 | console.log(' pushstate-server build'); 80 | console.log(' ' + openCommand + ' http://localhost:9000'); 81 | console.log(); 82 | console.log(chalk.dim('The project was built assuming it is hosted at the root.')); 83 | console.log(chalk.dim('Set the "homepage" field in package.json to override this.')); 84 | console.log(chalk.dim('For example, "homepage": "http://user.github.io/project".')); 85 | } 86 | console.log(); 87 | }); 88 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'development'; 2 | 3 | var path = require('path'); 4 | var chalk = require('chalk'); 5 | var webpack = require('webpack'); 6 | var WebpackDevServer = require('webpack-dev-server'); 7 | var execSync = require('child_process').execSync; 8 | var opn = require('opn'); 9 | var detect = require('./utils/detectPort'); 10 | var prompt = require('./utils/prompt'); 11 | var config = require('../config/webpack.config.dev'); 12 | 13 | // Tools like Cloud9 rely on this 14 | var DEFAULT_PORT = process.env.PORT || 3000; 15 | var compiler; 16 | 17 | // TODO: hide this behind a flag and eliminate dead code on eject. 18 | // This shouldn't be exposed to the user. 19 | var handleCompile; 20 | var isSmokeTest = process.argv.some(arg => arg.indexOf('--smoke-test') > -1); 21 | if (isSmokeTest) { 22 | handleCompile = function (err, stats) { 23 | if (err || stats.hasErrors() || stats.hasWarnings()) { 24 | process.exit(1); 25 | } else { 26 | process.exit(0); 27 | } 28 | }; 29 | } 30 | 31 | var friendlySyntaxErrorLabel = 'Syntax error:'; 32 | 33 | function isLikelyASyntaxError(message) { 34 | return message.indexOf(friendlySyntaxErrorLabel) !== -1; 35 | } 36 | 37 | // This is a little hacky. 38 | // It would be easier if webpack provided a rich error object. 39 | 40 | function formatMessage(message) { 41 | return message 42 | // Make some common errors shorter: 43 | .replace( 44 | // Babel syntax error 45 | 'Module build failed: SyntaxError:', 46 | friendlySyntaxErrorLabel 47 | ) 48 | .replace( 49 | // Webpack file not found error 50 | /Module not found: Error: Cannot resolve 'file' or 'directory'/, 51 | 'Module not found:' 52 | ) 53 | // Internal stacks are generally useless so we strip them 54 | .replace(/^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, '') // at ... ...:x:y 55 | // Webpack loader names obscure CSS filenames 56 | .replace('./~/css-loader!./~/postcss-loader!', ''); 57 | } 58 | 59 | function clearConsole() { 60 | process.stdout.write('\x1bc'); 61 | } 62 | 63 | function setupCompiler(port) { 64 | compiler = webpack(config, handleCompile); 65 | 66 | compiler.plugin('invalid', function() { 67 | clearConsole(); 68 | console.log('Compiling...'); 69 | }); 70 | 71 | compiler.plugin('done', function(stats) { 72 | clearConsole(); 73 | var hasErrors = stats.hasErrors(); 74 | var hasWarnings = stats.hasWarnings(); 75 | if (!hasErrors && !hasWarnings) { 76 | console.log(chalk.green('Compiled successfully!')); 77 | console.log(); 78 | console.log('The app is running at http://localhost:' + port + '/'); 79 | console.log(); 80 | return; 81 | } 82 | 83 | var json = stats.toJson(); 84 | var formattedErrors = json.errors.map(message => 85 | 'Error in ' + formatMessage(message) 86 | ); 87 | var formattedWarnings = json.warnings.map(message => 88 | 'Warning in ' + formatMessage(message) 89 | ); 90 | 91 | if (hasErrors) { 92 | console.log(chalk.red('Failed to compile.')); 93 | console.log(); 94 | if (formattedErrors.some(isLikelyASyntaxError)) { 95 | // If there are any syntax errors, show just them. 96 | // This prevents a confusing ESLint parsing error 97 | // preceding a much more useful Babel syntax error. 98 | formattedErrors = formattedErrors.filter(isLikelyASyntaxError); 99 | } 100 | formattedErrors.forEach(message => { 101 | console.log(message); 102 | console.log(); 103 | }); 104 | // If errors exist, ignore warnings. 105 | return; 106 | } 107 | 108 | if (hasWarnings) { 109 | console.log(chalk.yellow('Compiled with warnings.')); 110 | console.log(); 111 | formattedWarnings.forEach(message => { 112 | console.log(message); 113 | console.log(); 114 | }); 115 | 116 | console.log('You may use special comments to disable some warnings.'); 117 | console.log('Use ' + chalk.yellow('// eslint-disable-next-line') + ' to ignore the next line.'); 118 | console.log('Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.'); 119 | } 120 | }); 121 | } 122 | 123 | function openBrowser(port) { 124 | if (process.platform === 'darwin') { 125 | try { 126 | // Try our best to reuse existing tab 127 | // on OS X Google Chrome with AppleScript 128 | execSync('ps cax | grep "Google Chrome"'); 129 | execSync( 130 | 'osascript ' + 131 | path.resolve(__dirname, './utils/chrome.applescript') + 132 | ' http://localhost:' + port + '/' 133 | ); 134 | return; 135 | } catch (err) { 136 | // Ignore errors. 137 | } 138 | } 139 | // Fallback to opn 140 | // (It will always open new tab) 141 | opn('http://localhost:' + port + '/'); 142 | } 143 | 144 | function runDevServer(port) { 145 | new WebpackDevServer(compiler, { 146 | historyApiFallback: true, 147 | hot: true, // Note: only CSS is currently hot reloaded 148 | publicPath: config.output.publicPath, 149 | quiet: true, 150 | watchOptions: { 151 | ignored: /node_modules/ 152 | } 153 | }).listen(port, (err, result) => { 154 | if (err) { 155 | return console.log(err); 156 | } 157 | 158 | clearConsole(); 159 | console.log(chalk.cyan('Starting the development server...')); 160 | console.log(); 161 | openBrowser(port); 162 | }); 163 | } 164 | 165 | function run(port) { 166 | setupCompiler(port); 167 | runDevServer(port); 168 | } 169 | 170 | detect(DEFAULT_PORT).then(port => { 171 | if (port === DEFAULT_PORT) { 172 | run(port); 173 | return; 174 | } 175 | 176 | clearConsole(); 177 | var question = 178 | chalk.yellow('Something is already running at port ' + DEFAULT_PORT + '.') + 179 | '\n\nWould you like to run the app at another port instead?'; 180 | 181 | prompt(question, true).then(shouldChangePort => { 182 | if (shouldChangePort) { 183 | run(port); 184 | } 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /scripts/utils/chrome.applescript: -------------------------------------------------------------------------------- 1 | on run argv 2 | set theURL to item 1 of argv 3 | 4 | tell application "Chrome" 5 | 6 | if (count every window) = 0 then 7 | make new window 8 | end if 9 | 10 | -- Find a tab currently running the debugger 11 | set found to false 12 | set theTabIndex to -1 13 | repeat with theWindow in every window 14 | set theTabIndex to 0 15 | repeat with theTab in every tab of theWindow 16 | set theTabIndex to theTabIndex + 1 17 | if theTab's URL is theURL then 18 | set found to true 19 | exit repeat 20 | end if 21 | end repeat 22 | 23 | if found then 24 | exit repeat 25 | end if 26 | end repeat 27 | 28 | if found then 29 | tell theTab to reload 30 | set index of theWindow to 1 31 | set theWindow's active tab index to theTabIndex 32 | else 33 | tell window 1 34 | activate 35 | make new tab with properties {URL:theURL} 36 | end tell 37 | end if 38 | end tell 39 | end run 40 | -------------------------------------------------------------------------------- /scripts/utils/detectPort.js: -------------------------------------------------------------------------------- 1 | /* ================================================================ 2 | * detect-port by xdf(xudafeng[at]126.com) 3 | * 4 | * first created at : Tue Mar 17 2015 00:16:10 GMT+0800 (CST) 5 | * 6 | * ================================================================ 7 | * Copyright xdf 8 | * 9 | * Licensed under the MIT License 10 | * You may not use this file except in compliance with the License. 11 | * 12 | * ================================================================ */ 13 | 14 | // We are forking this temporarily to resolve 15 | // https://github.com/facebookincubator/create-react-app/issues/302. 16 | 17 | // We can replace this fork with `detect-port` package when this is merged: 18 | // https://github.com/xudafeng/detect-port/pull/4. 19 | 20 | 'use strict'; 21 | 22 | var net = require('net'); 23 | 24 | var inject = function(port) { 25 | var options = global.__detect ? global.__detect.options : {}; 26 | 27 | if (options.verbose) { 28 | console.log('port %d was occupied', port); 29 | } 30 | }; 31 | 32 | function detect(port, fn) { 33 | 34 | var _detect = function(port) { 35 | return new Promise(function(resolve, reject) { 36 | var socket = new net.Socket(); 37 | socket.once('error', function() { 38 | socket.removeAllListeners('connect'); 39 | socket.removeAllListeners('error'); 40 | socket.end(); 41 | socket.destroy(); 42 | socket.unref(); 43 | var server = new net.Server(); 44 | server.on('error', function() { 45 | inject(port); 46 | port++; 47 | resolve(_detect(port)); 48 | }); 49 | 50 | server.listen(port, function() { 51 | server.once('close', function() { 52 | resolve(port); 53 | }); 54 | server.close(); 55 | }); 56 | }); 57 | socket.once('connect', function() { 58 | inject(port); 59 | port++; 60 | resolve(_detect(port)); 61 | socket.removeAllListeners('connect'); 62 | socket.removeAllListeners('error'); 63 | socket.end(); 64 | socket.destroy(); 65 | socket.unref(); 66 | }); 67 | socket.connect({ 68 | port: port 69 | }); 70 | }); 71 | } 72 | 73 | var _detect_with_cb = function(_fn) { 74 | _detect(port) 75 | .then(function(result) { 76 | _fn(null, result); 77 | }) 78 | .catch(function(e) { 79 | _fn(e); 80 | }); 81 | }; 82 | 83 | return fn ? _detect_with_cb(fn) : _detect(port); 84 | } 85 | 86 | module.exports = detect; 87 | -------------------------------------------------------------------------------- /scripts/utils/prompt.js: -------------------------------------------------------------------------------- 1 | var rl = require('readline'); 2 | 3 | // Convention: "no" should be the conservative choice. 4 | // If you mistype the answer, we'll always take it as a "no". 5 | // You can control the behavior on with `isYesDefault`. 6 | module.exports = function (question, isYesDefault) { 7 | if (typeof isYesDefault !== 'boolean') { 8 | throw new Error('Provide explicit boolean isYesDefault as second argument.'); 9 | } 10 | return new Promise(resolve => { 11 | var rlInterface = rl.createInterface({ 12 | input: process.stdin, 13 | output: process.stdout, 14 | }); 15 | 16 | var hint = isYesDefault === true ? '[Y/n]' : '[y/N]'; 17 | var message = question + ' ' + hint + '\n'; 18 | 19 | rlInterface.question(message, function(answer) { 20 | rlInterface.close(); 21 | 22 | var useDefault = answer.trim().length === 0; 23 | if (useDefault) { 24 | return resolve(isYesDefault); 25 | } 26 | 27 | var isYes = answer.match(/^(yes|y)$/i); 28 | return resolve(isYes); 29 | }); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | 21 | @keyframes App-logo-spin { 22 | from { transform: rotate(0deg); } 23 | to { transform: rotate(360deg); } 24 | } 25 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | import PeopleContainer from './components/PeopleContainer'; 5 | 6 | class App extends Component { 7 | render() { 8 | return ( 9 |
10 |
11 | logo 12 |

Welcome to React

13 |
14 |

15 | To get started, edit src/App.js and save to reload. 16 |

17 | 18 |
19 | ); 20 | } 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /src/actions/action-types.js: -------------------------------------------------------------------------------- 1 | export const ADD_PERSON = 'ADD_PERSON'; 2 | -------------------------------------------------------------------------------- /src/actions/people-actions.js: -------------------------------------------------------------------------------- 1 | import * as types from './action-types'; 2 | 3 | export const addPerson = (person) => { 4 | return { 5 | type: types.ADD_PERSON, 6 | person 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/PeopleContainer.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes, Component} from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux'; 4 | import * as peopleActions from '../actions/people-actions'; 5 | import PeopleList from './PeopleList'; 6 | import PersonInput from './PersonInput'; 7 | 8 | class PeopleContainer extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | people: [] 14 | }; 15 | } 16 | 17 | render() { 18 | const {people} = this.props; 19 | 20 | return ( 21 |
22 | 23 | 24 |
25 | ); 26 | } 27 | } 28 | 29 | PeopleContainer.propTypes = { 30 | people: PropTypes.array.isRequired, 31 | actions: PropTypes.object.isRequired 32 | }; 33 | 34 | function mapStateToProps(state, props) { 35 | return { 36 | people: state.people 37 | }; 38 | } 39 | 40 | function mapDispatchToProps(dispatch) { 41 | return { 42 | actions: bindActionCreators(peopleActions, dispatch) 43 | } 44 | } 45 | 46 | export default connect(mapStateToProps, mapDispatchToProps)(PeopleContainer); 47 | -------------------------------------------------------------------------------- /src/components/PeopleList.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import Person from './Person'; 3 | 4 | const PeopleList = ({people}) => { 5 | return ( 6 |
7 | {people.map((person) => 8 | 9 | )} 10 |
11 | ); 12 | }; 13 | 14 | PeopleList.propTypes = { 15 | people: PropTypes.array.isRequired 16 | }; 17 | 18 | export default PeopleList; 19 | -------------------------------------------------------------------------------- /src/components/Person.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | 3 | const Person = ({person}) => { 4 | return ( 5 |
6 | {person.lastname}, {person.firstname} 7 |
8 | ); 9 | }; 10 | 11 | Person.propTypes = { 12 | person: PropTypes.object.isRequired 13 | }; 14 | 15 | export default Person; 16 | -------------------------------------------------------------------------------- /src/components/PersonInput.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes, Component} from 'react'; 2 | 3 | class PersonInput extends Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | this.onAddPersonClick = this.onAddPersonClick.bind(this); 8 | } 9 | 10 | onAddPersonClick() { 11 | const firstNameElement = document.getElementById('firstname'); 12 | const lastNameElement = document.getElementById('lastname'); 13 | 14 | this.props.addPerson({ 15 | firstname: firstNameElement.value, 16 | lastname: lastNameElement.value 17 | }); 18 | 19 | firstNameElement.value = ""; 20 | lastNameElement.value = ""; 21 | 22 | firstNameElement.focus(); 23 | } 24 | 25 | componentDidMount() { 26 | document.getElementById('firstname').focus(); 27 | } 28 | 29 | render() { 30 | return ( 31 |
32 | 33 | 34 | 35 |
36 | ); 37 | } 38 | } 39 | 40 | PersonInput.propTypes = { 41 | addPerson: PropTypes.func.isRequired 42 | }; 43 | 44 | export default PersonInput; 45 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './index.css'; 5 | import {Provider} from 'react-redux'; 6 | import configureStore from './store/configure-store'; 7 | 8 | const store = configureStore(); 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import people from './people-reducer.js'; 2 | import {combineReducers} from 'redux'; 3 | 4 | const rootReducer = combineReducers({ 5 | people 6 | }); 7 | 8 | export default rootReducer; 9 | -------------------------------------------------------------------------------- /src/reducers/people-reducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/action-types'; 2 | 3 | export default (state = [], action) => { 4 | switch (action.type) { 5 | case types.ADD_PERSON: 6 | return [...state, Object.assign({}, action.person)]; 7 | default: 8 | return state; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/store/configure-store.js: -------------------------------------------------------------------------------- 1 | import rootReducer from '../reducers'; 2 | import {createStore, compose} from 'redux'; 3 | 4 | // enable redux devtools... can this be done with Webpack instead? 5 | const enhancers = compose( 6 | window.devToolsExtension ? window.devToolsExtension() : f => f 7 | ) 8 | 9 | export default (initialState) => { 10 | return createStore(rootReducer, initialState, enhancers); 11 | }; 12 | --------------------------------------------------------------------------------