├── .editorconfig ├── .gitignore ├── .madgerc ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── configs └── webpack.config.js ├── graph.svg ├── index.html ├── jest.json ├── jest.stubs.js ├── madge.config.js ├── package.json ├── setupTests.js ├── src ├── app.tsx ├── components │ ├── __snapshots__ │ │ ├── counter.spec.tsx.snap │ │ └── list-view.spec.tsx.snap │ ├── counter.spec.tsx │ ├── counter.tsx │ ├── list-view.spec.tsx │ └── list-view.tsx ├── features │ ├── counters │ │ ├── actions.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ └── selectors.ts │ ├── root-action.ts │ ├── root-epic.ts │ └── root-reducer.ts ├── index.tsx ├── rxjs-imports.ts ├── store.ts └── typings │ ├── globals.d.ts │ ├── modules.d.ts │ └── redux.d.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | insert_final_newline = true 11 | 12 | # Indentation for md files 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | # Indentation for all source files 18 | [*.{json,js,jsx,ts,tsx,css,html}] 19 | max_line_length = 80 20 | trim_trailing_whitespace = true 21 | indent_style = space 22 | indent_size = 2 23 | insert_final_newline = true 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project 2 | dist/ 3 | out/ 4 | 5 | ### https://raw.github.com/github/gitignore/77e29837cf03b59fc4d885ea011bbd683caaaf85/node.gitignore 6 | 7 | # Logs 8 | logs 9 | *.log 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directory 32 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 33 | node_modules 34 | 35 | 36 | -------------------------------------------------------------------------------- /.madgerc: -------------------------------------------------------------------------------- 1 | { 2 | "baseDir": "./", 3 | "webpackConfig": "madge.config.js" 4 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Jest Tests", 9 | "type": "node", 10 | "request": "launch", 11 | "runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand"], 12 | "console": "integratedTerminal", 13 | "internalConsoleOptions": "neverOpen" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Piotr Witek (http://piotrwitek.github.io) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React / Redux / Typescript / Webpack - Starter 2 | ## _Powered by Webpack optimized for performance_ 3 | 4 | ### Recently updated all dependencies! 5 | 6 | ## Features: 7 | - No babel! 8 | - Ultra performance 9 | - Webpack 2 (simple one-file config, separate vendor bundle, dashboard) 10 | - Setup for `ts-loader` and `awesome-typescript-loader` for comparison (check `npm run dev` or `npm run dev:awesome`) 11 | 12 | - React Hot Loader 13 | ![](https://raw.githubusercontent.com/piotrwitek/react-redux-typescript-webpack-starter/docs/images/dev.gif) 14 | 15 | - Dependency graph of the entire application! 🌟 __NEW__ 16 | ![](./graph.svg) 17 | 18 | --- 19 | 20 | ## Installation 21 | - project optimized to use yarn 22 | ``` 23 | // Clone repo 24 | git clone https://github.com/piotrwitek/react-redux-typescript-webpack-starter 25 | 26 | // Install dependencies 27 | npm install 28 | 29 | // Run development server with react hot-reload 30 | npm run dev (ts-loader) 31 | ``` 32 | 33 | --- 34 | 35 | ## CLI Commands 36 | 37 | #### - Development 38 | 39 | `npm run dev` - start dev-server with hot-reload (ts-loader) 40 | 41 | `npm run dev:dashboard` - start dev-server with `webpack-dashboard` 42 | 43 | `npm run dev:awesome` - start dev-server with `awesome-typescript-loader` 44 | 45 | #### - Type checking 46 | 47 | `npm run tsc` - entire project type-check 48 | 49 | `npm run tsc:watch` - fast incremental type-checking in watch mode 50 | 51 | #### - Production Bundling (`dist/` folder) 52 | 53 | `npm run clean` - clean dist 54 | 55 | `npm run build` - build dist bundle 56 | 57 | #### - Utility & Git Hooks 58 | 59 | `npm run reinstall` - reinstall all dependencies (useful when switching branch) (note: use `reinstall:win` on Windows) 60 | 61 | `npm run lint` - run linter (tslint) 62 | 63 | `npm run test` - run tests with jest runner 64 | 65 | `npm run test:update` - update jest snapshots 66 | 67 | `npm run precommit` - pre commit git hook - linter 68 | 69 | `npm run prepush` - pre push git hook - linter, tests and check types 70 | 71 | #### - Deployment 72 | 73 | ~~`npm run deploy` - commit and push all changes found in `/dist` folder to "gh-pages" branch~~ 74 | 75 | --- 76 | 77 | ## The MIT License (MIT) 78 | 79 | Copyright (c) 2016 Piotr Witek (http://piotrwitek.github.io/) 80 | -------------------------------------------------------------------------------- /configs/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const DashboardPlugin = require('webpack-dashboard/plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | const PATHS = { 7 | root: path.resolve(__dirname, '..'), 8 | nodeModules: path.resolve(__dirname, '../node_modules'), 9 | src: path.resolve(__dirname, '../src'), 10 | dist: path.resolve(__dirname, '../dist'), 11 | }; 12 | 13 | const DEV_SERVER = { 14 | hot: true, 15 | hotOnly: true, 16 | historyApiFallback: true, 17 | overlay: true, 18 | // stats: 'verbose', 19 | // proxy: { 20 | // '/api': 'http://localhost:3000' 21 | // }, 22 | }; 23 | 24 | module.exports = (env = {}) => { 25 | console.log({ env }); 26 | const isBuild = !!env.build; 27 | const isDev = !env.build; 28 | const isSourceMap = !!env.sourceMap || isDev; 29 | 30 | return { 31 | cache: true, 32 | devtool: isDev ? 'eval-source-map' : 'source-map', 33 | devServer: DEV_SERVER, 34 | 35 | context: PATHS.root, 36 | 37 | entry: { 38 | app: [ 39 | 'react-hot-loader/patch', 40 | './src/index.tsx', 41 | ], 42 | }, 43 | output: { 44 | path: PATHS.dist, 45 | filename: isDev ? '[name].js' : '[name].[hash].js', 46 | publicPath: '/', 47 | // chunkFilename: '[id].chunk.js', 48 | }, 49 | 50 | resolve: { 51 | alias: { 52 | Components: '../src/components', 53 | Features: '../src/features', 54 | }, 55 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], 56 | modules: ['src', 'node_modules'], 57 | }, 58 | 59 | module: { 60 | rules: [ 61 | // typescript 62 | { 63 | test: /\.tsx?$/, 64 | include: PATHS.src, 65 | use: (env.awesome ? 66 | [ 67 | { loader: 'react-hot-loader/webpack' }, 68 | { 69 | loader: 'awesome-typescript-loader', 70 | options: { 71 | transpileOnly: true, 72 | useTranspileModule: false, 73 | sourceMap: isSourceMap, 74 | }, 75 | }, 76 | ] : [ 77 | { loader: 'react-hot-loader/webpack' }, 78 | { 79 | loader: 'ts-loader', 80 | options: { 81 | transpileOnly: true, 82 | compilerOptions: { 83 | 'sourceMap': isSourceMap, 84 | 'target': isDev ? 'es2015' : 'es5', 85 | 'isolatedModules': true, 86 | 'noEmitOnError': false, 87 | }, 88 | }, 89 | }, 90 | ] 91 | ), 92 | }, 93 | // json 94 | { 95 | test: /\.json$/, 96 | include: [PATHS.src], 97 | use: { loader: 'json-loader' }, 98 | }, 99 | // // css 100 | // { 101 | // test: /\.css$/, 102 | // include: [PATHS.STYLES], 103 | // loader: ExtractTextPlugin.extract([ 104 | // 'css-loader?{modules: false}', 105 | // 'postcss-loader', 106 | // ]), 107 | // }, 108 | // // less 109 | // { 110 | // test: /\.less$/, 111 | // include: [PATHS.STYLES], 112 | // loader: ExtractTextPlugin.extract([ 113 | // 'css-loader?{modules: false}', 114 | // 'less-loader', 115 | // ]), 116 | // }, 117 | // // images 118 | // { 119 | // test: /\.(jpg|jpeg|png|gif|svg)$/, 120 | // include: [PATHS.IMAGES], 121 | // use: { 122 | // loader: 'url-loader', 123 | // options: { 124 | // name: 'images/[hash].[ext]', 125 | // limit: 1000, // inline file data until size 126 | // }, 127 | // }, 128 | // }, 129 | // // fonts 130 | // { 131 | // test: /\.(woff|woff2|ttf|eot)$/, 132 | // include: [ 133 | // PATHS.ASSETS, 134 | // ], 135 | // use: { 136 | // loader: 'file-loader', 137 | // options: { 138 | // name: 'fonts/[name].[hash].[ext]', 139 | // }, 140 | // }, 141 | // }, 142 | ], 143 | }, 144 | 145 | plugins: [ 146 | new DashboardPlugin(), 147 | new webpack.DefinePlugin({ 148 | 'process.env': { 149 | NODE_ENV: JSON.stringify(isDev ? 'development' : 'production'), 150 | }, 151 | }), 152 | new webpack.optimize.CommonsChunkPlugin({ 153 | name: 'vendor', 154 | minChunks: (module) => module.context && module.context.indexOf('node_modules') !== -1, 155 | }), 156 | new webpack.optimize.CommonsChunkPlugin({ 157 | name: 'manifest', 158 | }), 159 | new HtmlWebpackPlugin({ 160 | template: './index.html' 161 | }), 162 | ...(isDev ? [ 163 | new webpack.HotModuleReplacementPlugin({ 164 | // multiStep: true, // better performance with many files 165 | }), 166 | new webpack.NamedModulesPlugin(), 167 | ] : []), 168 | ...(isBuild ? [ 169 | new webpack.LoaderOptionsPlugin({ 170 | minimize: true, 171 | debug: false 172 | }), 173 | new webpack.optimize.UglifyJsPlugin({ 174 | beautify: false, 175 | compress: { 176 | screw_ie8: true 177 | }, 178 | comments: false, 179 | sourceMap: isSourceMap, 180 | }), 181 | ] : []), 182 | ] 183 | }; 184 | 185 | }; 186 | -------------------------------------------------------------------------------- /graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | 13 | 14 | out/app.js 15 | 16 | out/app.js 17 | 18 | 19 | 20 | out/components/counter.js 21 | 22 | out/components/counter.js 23 | 24 | 25 | 26 | out/app.js->out/components/counter.js 27 | 28 | 29 | 30 | 31 | 32 | out/components/list-view.js 33 | 34 | out/components/list-view.js 35 | 36 | 37 | 38 | out/app.js->out/components/list-view.js 39 | 40 | 41 | 42 | 43 | 44 | out/features/counters/actions.js 45 | 46 | out/features/counters/actions.js 47 | 48 | 49 | 50 | out/features/counters/reducer.js 51 | 52 | out/features/counters/reducer.js 53 | 54 | 55 | 56 | out/features/counters/reducer.js->out/features/counters/actions.js 57 | 58 | 59 | 60 | 61 | 62 | out/features/root-epic.js 63 | 64 | out/features/root-epic.js 65 | 66 | 67 | 68 | out/features/root-reducer.js 69 | 70 | out/features/root-reducer.js 71 | 72 | 73 | 74 | out/features/root-reducer.js->out/features/counters/reducer.js 75 | 76 | 77 | 78 | 79 | 80 | out/index.js 81 | 82 | out/index.js 83 | 84 | 85 | 86 | out/index.js->out/app.js 87 | 88 | 89 | 90 | 91 | 92 | out/rxjs-imports.js 93 | 94 | out/rxjs-imports.js 95 | 96 | 97 | 98 | out/index.js->out/rxjs-imports.js 99 | 100 | 101 | 102 | 103 | 104 | out/store.js 105 | 106 | out/store.js 107 | 108 | 109 | 110 | out/index.js->out/store.js 111 | 112 | 113 | 114 | 115 | 116 | out/store.js->out/features/root-epic.js 117 | 118 | 119 | 120 | 121 | 122 | out/store.js->out/features/root-reducer.js 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React / Redux / Typescript / Webpack - Starter 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "transform": { 4 | ".(ts|tsx)": "./node_modules/ts-jest/preprocessor.js" 5 | }, 6 | "testRegex": "(/spec/.*|\\.(test|spec))\\.(ts|tsx|js)$", 7 | "testPathIgnorePatterns": ["/dist/", "/node_modules/"], 8 | "moduleFileExtensions": ["ts", "tsx", "js"], 9 | "globals": { 10 | "window": {}, 11 | "ts-jest": { 12 | "tsConfigFile": "./tsconfig.json" 13 | } 14 | }, 15 | "setupFiles": [ 16 | "./jest.stubs.js", 17 | "./src/rxjs-imports.ts" 18 | ], 19 | "setupTestFrameworkScriptFile": "./setupTests.js" 20 | } 21 | -------------------------------------------------------------------------------- /jest.stubs.js: -------------------------------------------------------------------------------- 1 | // Global/Window object Stubs for Jest 2 | window.matchMedia = window.matchMedia || function () { 3 | return { 4 | matches: false, 5 | addListener: function () { }, 6 | removeListener: function () { }, 7 | }; 8 | }; 9 | 10 | window.requestAnimationFrame = function (callback) { 11 | setTimeout(callback); 12 | }; 13 | 14 | window.localStorage = { 15 | getItem: function () { }, 16 | setItem: function () { }, 17 | }; 18 | -------------------------------------------------------------------------------- /madge.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | resolve: { 3 | alias: { 4 | Components: './out/components', 5 | Features: './out/features', 6 | }, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-typescript-webpack-starter", 3 | "version": "0.1.0", 4 | "description": "React / Redux / Typescript / Webpack - Starter", 5 | "author": "Piotr Witek (https://piotrwitek.github.io/)", 6 | "homepage": "https://piotrwitek.github.io/react-redux-typescript-webpack-starter/", 7 | "repository": "https://github.com/piotrwitek/react-redux-typescript-webpack-starter.git", 8 | "bugs": "https://github.com/piotrwitek/react-redux-typescript-webpack-starter/issues", 9 | "license": "MIT", 10 | "main": "dist/app.js", 11 | "scripts": { 12 | "clean": "rimraf dist", 13 | "build": "yarn run clean && webpack-dashboard -- webpack --config configs/webpack.config.js --env.build --env.sourceMap", 14 | "dev": "webpack-dev-server --config configs/webpack.config.js --open", 15 | "dev:dashboard": "webpack-dashboard -m -- webpack-dev-server --config configs/webpack.config.js --open", 16 | "dev:awesome": "webpack-dashboard -m -- webpack-dev-server --config configs/webpack.config.js --open --env.awesome", 17 | "lint": "tslint --project tsconfig.json", 18 | "tsc": "tsc -p . --noEmit", 19 | "tsc:watch": "tsc -p . --noEmit -w", 20 | "test": "jest --config jest.json", 21 | "test:watch": "jest --config jest.json --watch", 22 | "test:update": "jest --config jest.json -u", 23 | "reinstall": "rm -rf node_modules && yarn", 24 | "reinstall:win": "rd /s /q node_modules && yarn", 25 | "precommit": "yarn run lint", 26 | "prepush": "yarn run lint & yarn run tsc & yarn run test", 27 | "deploy": "echo 'deploy not set!'", 28 | "graph": "tsc && madge --image graph.svg out/index.js --warning" 29 | }, 30 | "dependencies": { 31 | "react": "16.2.0", 32 | "react-dom": "16.2.0", 33 | "react-redux": "5.0.6", 34 | "react-router-dom": "4.2.2", 35 | "react-router-redux": "5.0.0-alpha.9", 36 | "redux": "3.7.2", 37 | "redux-observable": "0.17.0", 38 | "reselect": "3.0.1", 39 | "rxjs": "5.5.6", 40 | "tslib": "1.8.1", 41 | "typesafe-actions": "1.1.2", 42 | "utility-types": "1.0.0-rc.2" 43 | }, 44 | "devDependencies": { 45 | "@types/enzyme": "3.1.6", 46 | "@types/jest": "22.0.1", 47 | "@types/react": "16.0.34", 48 | "@types/react-dom": "16.0.3", 49 | "@types/react-hot-loader": "3.0.5", 50 | "@types/react-redux": "5.0.14", 51 | "@types/react-router-dom": "4.2.3", 52 | "@types/react-router-redux": "5.0.11", 53 | "@types/webpack": "3.8.2", 54 | "@types/webpack-dev-server": "2.9.2", 55 | "@types/webpack-env": "1.13.3", 56 | "enzyme": "3.3.0", 57 | "enzyme-adapter-react-16": "1.1.1", 58 | "html-webpack-plugin": "2.30.1", 59 | "husky": "0.14.3", 60 | "jest": "22.1.1", 61 | "react-hot-loader": "3", 62 | "rimraf": "2.6.2", 63 | "ts-jest": "22.0.1", 64 | "ts-loader": "3.2.0", 65 | "tslint": "5.9.1", 66 | "tslint-react": "3.4.0", 67 | "typescript": "2.6.2", 68 | "webpack": "2", 69 | "webpack-dashboard": "^0.4.0", 70 | "webpack-dev-server": "2.11.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { Store } from 'redux'; 4 | import { ConnectedRouter } from 'react-router-redux'; 5 | import { Route } from 'react-router-dom'; 6 | import { History } from 'history'; 7 | 8 | import { ListView } from 'Components/list-view'; 9 | import { Counter } from 'Components/counter'; 10 | 11 | interface Props { 12 | store: Store; 13 | history: History; 14 | } 15 | 16 | export class App extends React.Component { 17 | render() { 18 | const { store, history } = this.props; 19 | return ( 20 | 21 | 22 | ( 26 | 27 | 28 | 29 | )} 30 | /> 31 | 32 | 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/__snapshots__/counter.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Counter should match a snapshot 1`] = ` 4 | ShallowWrapper { 5 | "length": 1, 6 | Symbol(enzyme.__root__): [Circular], 7 | Symbol(enzyme.__unrendered__): , 8 | Symbol(enzyme.__renderer__): Object { 9 | "batchedUpdates": [Function], 10 | "getNode": [Function], 11 | "render": [Function], 12 | "simulateEvent": [Function], 13 | "unmount": [Function], 14 | }, 15 | Symbol(enzyme.__node__): Object { 16 | "instance": null, 17 | "key": undefined, 18 | "nodeType": "host", 19 | "props": Object { 20 | "children":
21 | Counter: 22 | 0 23 |
, 24 | }, 25 | "ref": null, 26 | "rendered": Object { 27 | "instance": null, 28 | "key": undefined, 29 | "nodeType": "host", 30 | "props": Object { 31 | "children": Array [ 32 | "Counter: ", 33 | 0, 34 | ], 35 | }, 36 | "ref": null, 37 | "rendered": Array [ 38 | "Counter: ", 39 | 0, 40 | ], 41 | "type": "div", 42 | }, 43 | "type": "div", 44 | }, 45 | Symbol(enzyme.__nodes__): Array [ 46 | Object { 47 | "instance": null, 48 | "key": undefined, 49 | "nodeType": "host", 50 | "props": Object { 51 | "children":
52 | Counter: 53 | 0 54 |
, 55 | }, 56 | "ref": null, 57 | "rendered": Object { 58 | "instance": null, 59 | "key": undefined, 60 | "nodeType": "host", 61 | "props": Object { 62 | "children": Array [ 63 | "Counter: ", 64 | 0, 65 | ], 66 | }, 67 | "ref": null, 68 | "rendered": Array [ 69 | "Counter: ", 70 | 0, 71 | ], 72 | "type": "div", 73 | }, 74 | "type": "div", 75 | }, 76 | ], 77 | Symbol(enzyme.__options__): Object { 78 | "adapter": ReactSixteenAdapter { 79 | "options": Object { 80 | "enableComponentDidUpdateOnSetState": true, 81 | }, 82 | }, 83 | }, 84 | } 85 | `; 86 | -------------------------------------------------------------------------------- /src/components/__snapshots__/list-view.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ListView with children should match a snapshot 1`] = ` 4 | ShallowWrapper { 5 | "length": 1, 6 | Symbol(enzyme.__root__): [Circular], 7 | Symbol(enzyme.__unrendered__): 8 | 9 | child 10 | 11 | , 12 | Symbol(enzyme.__renderer__): Object { 13 | "batchedUpdates": [Function], 14 | "getNode": [Function], 15 | "render": [Function], 16 | "simulateEvent": [Function], 17 | "unmount": [Function], 18 | }, 19 | Symbol(enzyme.__node__): Object { 20 | "instance": null, 21 | "key": undefined, 22 | "nodeType": "function", 23 | "props": Object { 24 | "children": Array [ 25 | undefined, 26 |
27 | 28 | child 29 | 30 |
, 31 | ], 32 | }, 33 | "ref": null, 34 | "rendered": Array [ 35 | undefined, 36 | Object { 37 | "instance": null, 38 | "key": undefined, 39 | "nodeType": "host", 40 | "props": Object { 41 | "children": 42 | child 43 | , 44 | }, 45 | "ref": null, 46 | "rendered": Object { 47 | "instance": null, 48 | "key": undefined, 49 | "nodeType": "host", 50 | "props": Object { 51 | "children": "child", 52 | }, 53 | "ref": null, 54 | "rendered": "child", 55 | "type": "span", 56 | }, 57 | "type": "div", 58 | }, 59 | ], 60 | "type": Symbol(react.fragment), 61 | }, 62 | Symbol(enzyme.__nodes__): Array [ 63 | Object { 64 | "instance": null, 65 | "key": undefined, 66 | "nodeType": "function", 67 | "props": Object { 68 | "children": Array [ 69 | undefined, 70 |
71 | 72 | child 73 | 74 |
, 75 | ], 76 | }, 77 | "ref": null, 78 | "rendered": Array [ 79 | undefined, 80 | Object { 81 | "instance": null, 82 | "key": undefined, 83 | "nodeType": "host", 84 | "props": Object { 85 | "children": 86 | child 87 | , 88 | }, 89 | "ref": null, 90 | "rendered": Object { 91 | "instance": null, 92 | "key": undefined, 93 | "nodeType": "host", 94 | "props": Object { 95 | "children": "child", 96 | }, 97 | "ref": null, 98 | "rendered": "child", 99 | "type": "span", 100 | }, 101 | "type": "div", 102 | }, 103 | ], 104 | "type": Symbol(react.fragment), 105 | }, 106 | ], 107 | Symbol(enzyme.__options__): Object { 108 | "adapter": ReactSixteenAdapter { 109 | "options": Object { 110 | "enableComponentDidUpdateOnSetState": true, 111 | }, 112 | }, 113 | }, 114 | } 115 | `; 116 | 117 | exports[`ListView with title should match a snapshot 1`] = ` 118 | ShallowWrapper { 119 | "length": 1, 120 | Symbol(enzyme.__root__): [Circular], 121 | Symbol(enzyme.__unrendered__): , 124 | Symbol(enzyme.__renderer__): Object { 125 | "batchedUpdates": [Function], 126 | "getNode": [Function], 127 | "render": [Function], 128 | "simulateEvent": [Function], 129 | "unmount": [Function], 130 | }, 131 | Symbol(enzyme.__node__): Object { 132 | "instance": null, 133 | "key": undefined, 134 | "nodeType": "function", 135 | "props": Object { 136 | "children": Array [ 137 |

138 | A title 139 |

, 140 |
, 141 | ], 142 | }, 143 | "ref": null, 144 | "rendered": Array [ 145 | Object { 146 | "instance": null, 147 | "key": undefined, 148 | "nodeType": "host", 149 | "props": Object { 150 | "children": "A title", 151 | }, 152 | "ref": null, 153 | "rendered": "A title", 154 | "type": "h2", 155 | }, 156 | Object { 157 | "instance": null, 158 | "key": undefined, 159 | "nodeType": "host", 160 | "props": Object { 161 | "children": undefined, 162 | }, 163 | "ref": null, 164 | "rendered": null, 165 | "type": "div", 166 | }, 167 | ], 168 | "type": Symbol(react.fragment), 169 | }, 170 | Symbol(enzyme.__nodes__): Array [ 171 | Object { 172 | "instance": null, 173 | "key": undefined, 174 | "nodeType": "function", 175 | "props": Object { 176 | "children": Array [ 177 |

178 | A title 179 |

, 180 |
, 181 | ], 182 | }, 183 | "ref": null, 184 | "rendered": Array [ 185 | Object { 186 | "instance": null, 187 | "key": undefined, 188 | "nodeType": "host", 189 | "props": Object { 190 | "children": "A title", 191 | }, 192 | "ref": null, 193 | "rendered": "A title", 194 | "type": "h2", 195 | }, 196 | Object { 197 | "instance": null, 198 | "key": undefined, 199 | "nodeType": "host", 200 | "props": Object { 201 | "children": undefined, 202 | }, 203 | "ref": null, 204 | "rendered": null, 205 | "type": "div", 206 | }, 207 | ], 208 | "type": Symbol(react.fragment), 209 | }, 210 | ], 211 | Symbol(enzyme.__options__): Object { 212 | "adapter": ReactSixteenAdapter { 213 | "options": Object { 214 | "enableComponentDidUpdateOnSetState": true, 215 | }, 216 | }, 217 | }, 218 | } 219 | `; 220 | 221 | exports[`ListView without props should match a snapshot 1`] = ` 222 | ShallowWrapper { 223 | "length": 1, 224 | Symbol(enzyme.__root__): [Circular], 225 | Symbol(enzyme.__unrendered__): , 226 | Symbol(enzyme.__renderer__): Object { 227 | "batchedUpdates": [Function], 228 | "getNode": [Function], 229 | "render": [Function], 230 | "simulateEvent": [Function], 231 | "unmount": [Function], 232 | }, 233 | Symbol(enzyme.__node__): Object { 234 | "instance": null, 235 | "key": undefined, 236 | "nodeType": "function", 237 | "props": Object { 238 | "children": Array [ 239 | undefined, 240 |
, 241 | ], 242 | }, 243 | "ref": null, 244 | "rendered": Array [ 245 | undefined, 246 | Object { 247 | "instance": null, 248 | "key": undefined, 249 | "nodeType": "host", 250 | "props": Object { 251 | "children": undefined, 252 | }, 253 | "ref": null, 254 | "rendered": null, 255 | "type": "div", 256 | }, 257 | ], 258 | "type": Symbol(react.fragment), 259 | }, 260 | Symbol(enzyme.__nodes__): Array [ 261 | Object { 262 | "instance": null, 263 | "key": undefined, 264 | "nodeType": "function", 265 | "props": Object { 266 | "children": Array [ 267 | undefined, 268 |
, 269 | ], 270 | }, 271 | "ref": null, 272 | "rendered": Array [ 273 | undefined, 274 | Object { 275 | "instance": null, 276 | "key": undefined, 277 | "nodeType": "host", 278 | "props": Object { 279 | "children": undefined, 280 | }, 281 | "ref": null, 282 | "rendered": null, 283 | "type": "div", 284 | }, 285 | ], 286 | "type": Symbol(react.fragment), 287 | }, 288 | ], 289 | Symbol(enzyme.__options__): Object { 290 | "adapter": ReactSixteenAdapter { 291 | "options": Object { 292 | "enableComponentDidUpdateOnSetState": true, 293 | }, 294 | }, 295 | }, 296 | } 297 | `; 298 | -------------------------------------------------------------------------------- /src/components/counter.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { Counter } from './counter'; 4 | 5 | describe('Counter', () => { 6 | const component = shallow(); 7 | 8 | it('should match a snapshot', () => { 9 | expect(component).toMatchSnapshot(); 10 | }); 11 | 12 | it('should initialize state', () => { 13 | expect(component.state()).toEqual({ count: 0 }); 14 | }); 15 | 16 | it('should change state after a second', (done) => { 17 | setTimeout(() => { 18 | expect(component.state()).not.toEqual({ count: 0 }); 19 | done(); 20 | }, 1500); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/components/counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface Props { 4 | } 5 | interface State { 6 | count: number; 7 | } 8 | 9 | export class Counter extends React.Component { 10 | interval: number; 11 | state = { count: 0 }; 12 | 13 | componentWillMount() { 14 | const incrementCounter = () => { 15 | this.setState({ count: this.state.count + 1 }); 16 | }; 17 | this.interval = setInterval(incrementCounter, 1000); 18 | } 19 | 20 | componentWillUnmount() { 21 | clearInterval(this.interval); 22 | } 23 | 24 | render() { 25 | return ( 26 |
27 |
Counter: {this.state.count}
28 |
29 | ); 30 | } 31 | } 32 | 33 | export default Counter; 34 | -------------------------------------------------------------------------------- /src/components/list-view.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { ListView } from './list-view'; 4 | 5 | describe('ListView', () => { 6 | 7 | describe('without props', () => { 8 | const component = shallow(); 9 | 10 | it('should match a snapshot', () => { 11 | expect(component).toMatchSnapshot(); 12 | }); 13 | }); 14 | 15 | describe('with title', () => { 16 | const component = shallow(); 17 | 18 | it('should match a snapshot', () => { 19 | expect(component).toMatchSnapshot(); 20 | }); 21 | 22 | it('should have a title header', () => { 23 | const header = component.find('h2'); 24 | 25 | expect(header.exists()).toBeTruthy(); 26 | expect(header.contains('A title')).toBeTruthy(); 27 | }); 28 | }); 29 | 30 | describe('with children', () => { 31 | const component = shallow(child); 32 | 33 | it('should match a snapshot', () => { 34 | expect(component).toMatchSnapshot(); 35 | }); 36 | 37 | it('should have a child', () => { 38 | const child = component.find('span'); 39 | 40 | expect(child.exists()).toBeTruthy(); 41 | expect(child.contains('child')).toBeTruthy(); 42 | expect(component.find('div').children().length).toEqual(1); 43 | }); 44 | }); 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /src/components/list-view.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface ListViewProps { 4 | title?: string; 5 | } 6 | 7 | export class ListView extends React.Component { 8 | render() { 9 | const { title, children } = this.props; 10 | 11 | return ( 12 | <> 13 | {title &&

{title}

} 14 |
{children}
15 | 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/features/counters/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from 'typesafe-actions'; 2 | 3 | const INCREMENT = 'INCREMENT'; 4 | const ADD = 'ADD'; 5 | 6 | export const increment = createAction(INCREMENT); 7 | export const add = createAction(ADD, (amount: number) => ({ 8 | type: ADD, payload: amount, 9 | })); 10 | -------------------------------------------------------------------------------- /src/features/counters/index.ts: -------------------------------------------------------------------------------- 1 | // public API 2 | import * as countersActions from './actions'; 3 | import * as countersSelectors from './selectors'; 4 | import { reducer, State } from './reducer'; 5 | // export * from './epics'; 6 | 7 | export { reducer, State, countersActions, countersSelectors }; 8 | -------------------------------------------------------------------------------- /src/features/counters/reducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { getType, getReturnOfExpression } from 'typesafe-actions'; 3 | 4 | import * as countersActions from './actions'; 5 | const returnsOfActions = Object.values(countersActions).map(getReturnOfExpression); 6 | export type Action = typeof returnsOfActions[number]; 7 | 8 | export type State = { 9 | readonly reduxCounter: number; 10 | }; 11 | 12 | export const reducer = combineReducers({ 13 | reduxCounter: (state = 0, action) => { 14 | switch (action.type) { 15 | case getType(countersActions.increment): 16 | return state + 1; // action is type: { type: "INCREMENT"; } 17 | 18 | case getType(countersActions.add): 19 | return state + action.payload; // action is type: { type: "ADD"; payload: number; } 20 | 21 | default: 22 | return state; 23 | } 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /src/features/counters/selectors.ts: -------------------------------------------------------------------------------- 1 | import { State } from './'; 2 | 3 | export const getReduxCounter = 4 | (state: State) => state.reduxCounter; 5 | -------------------------------------------------------------------------------- /src/features/root-action.ts: -------------------------------------------------------------------------------- 1 | // RootActions 2 | import { RouterAction, LocationChangeAction } from 'react-router-redux'; 3 | import { getReturnOfExpression } from 'utility-types'; 4 | 5 | import * as countersActions from './counters/actions'; 6 | 7 | export const actions = { 8 | counters: countersActions, 9 | }; 10 | 11 | const returnsOfActions = [ 12 | ...Object.values(countersActions), 13 | ].map(getReturnOfExpression); 14 | 15 | type AppAction = typeof returnsOfActions[number]; 16 | type ReactRouterAction = RouterAction | LocationChangeAction; 17 | 18 | export type RootAction = 19 | | AppAction 20 | | ReactRouterAction; 21 | -------------------------------------------------------------------------------- /src/features/root-epic.ts: -------------------------------------------------------------------------------- 1 | import { combineEpics } from 'redux-observable'; 2 | 3 | // import { epics as toasts } from './toasts/epics'; 4 | 5 | export const rootEpic = combineEpics( 6 | // toasts 7 | ); 8 | -------------------------------------------------------------------------------- /src/features/root-reducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer as router, RouterState } from 'react-router-redux'; 3 | 4 | import { reducer as counters, State as CountersState } from './counters/reducer'; 5 | 6 | interface StoreEnhancerState { } 7 | 8 | export interface RootState extends StoreEnhancerState { 9 | router: RouterState; 10 | counters: CountersState; 11 | } 12 | 13 | import { RootAction } from './root-action'; 14 | export const rootReducer = combineReducers({ 15 | router, 16 | counters, 17 | }); 18 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | // tslint:disable:no-import-side-effect 5 | // side-effect imports here 6 | import './rxjs-imports'; 7 | // tslint:enable:no-import-side-effect 8 | 9 | import { App } from './app'; 10 | import { store, browserHistory, epicMiddleware } from './store'; 11 | 12 | const renderRoot = (app: JSX.Element) => { 13 | ReactDOM.render(app, document.getElementById('root')); 14 | }; 15 | 16 | if (process.env.NODE_ENV === 'production') { 17 | renderRoot(( 18 | 19 | )); 20 | } else { // removed in production, hot-reload config 21 | // tslint:disable-next-line:no-var-requires 22 | const AppContainer = require('react-hot-loader').AppContainer; 23 | renderRoot(( 24 | 25 | 26 | 27 | )); 28 | 29 | if (module.hot) { 30 | // app 31 | module.hot.accept('./app', async () => { 32 | // const NextApp = require('./app').App; 33 | const NextApp = (await System.import('./app')).App; 34 | renderRoot(( 35 | 36 | 37 | 38 | )); 39 | }); 40 | 41 | // reducers 42 | module.hot.accept('./features/root-reducer', () => { 43 | const newRootReducer = require('./features/root-reducer').default; 44 | store.replaceReducer(newRootReducer); 45 | }); 46 | 47 | // epics 48 | module.hot.accept('./features/root-epic', () => { 49 | const newRootEpic = require('./features/root-epic').default; 50 | epicMiddleware.replaceEpic(newRootEpic); 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/rxjs-imports.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-import-side-effect 2 | 3 | // observable 4 | import 'rxjs/add/observable/of'; 5 | 6 | // operators 7 | import 'rxjs/add/operator/concat'; 8 | import 'rxjs/add/operator/concatMap'; 9 | import 'rxjs/add/operator/delay'; 10 | import 'rxjs/add/operator/filter'; 11 | import 'rxjs/add/operator/map'; 12 | import 'rxjs/add/operator/merge'; 13 | import 'rxjs/add/operator/debounceTime'; 14 | import 'rxjs/add/operator/do'; 15 | import 'rxjs/add/operator/ignoreElements'; 16 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import { createEpicMiddleware } from 'redux-observable'; 3 | import { routerMiddleware as createRouterMiddleware } from 'react-router-redux'; 4 | import { createBrowserHistory } from 'history'; 5 | 6 | import { rootReducer, RootState } from 'Features/root-reducer'; 7 | import { rootEpic } from 'Features/root-epic'; 8 | 9 | const composeEnhancers = ( 10 | process.env.NODE_ENV === 'development' && 11 | window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 12 | ) || compose; 13 | 14 | export const epicMiddleware = createEpicMiddleware(rootEpic); 15 | export const browserHistory = createBrowserHistory(); 16 | export const routerMiddleware = createRouterMiddleware(browserHistory); 17 | 18 | function configureStore(initialState?: RootState) { 19 | // configure middlewares 20 | const middlewares = [ 21 | epicMiddleware, 22 | routerMiddleware, 23 | ]; 24 | // compose enhancers 25 | const enhancer = composeEnhancers( 26 | applyMiddleware(...middlewares) 27 | ); 28 | // create store 29 | return createStore( 30 | rootReducer, 31 | initialState!, 32 | enhancer 33 | ); 34 | } 35 | 36 | // pass an optional param to rehydrate state on app start 37 | export const store = configureStore(); 38 | 39 | // export store singleton instance 40 | export default store; 41 | -------------------------------------------------------------------------------- /src/typings/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | __REDUX_DEVTOOLS_EXTENSION__: any; 3 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any; 4 | } 5 | 6 | declare interface System { 7 | import(module: string): Promise 8 | } 9 | declare var System: System; 10 | -------------------------------------------------------------------------------- /src/typings/modules.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piotrwitek/react-redux-typescript-webpack-starter/1dd80c5c866cf3f56b469fdbeaf768c4a087a842/src/typings/modules.d.ts -------------------------------------------------------------------------------- /src/typings/redux.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * An *action* is a plain object that represents an intention to change the 4 | * state. Actions are the only way to get data into the store. Any data, 5 | * whether from UI events, network callbacks, or other sources such as 6 | * WebSockets needs to eventually be dispatched as actions. 7 | * 8 | * Actions must have a `type` field that indicates the type of action being 9 | * performed. Types can be defined as constants and imported from another 10 | * module. It's better to use strings for `type` than Symbols because strings 11 | * are serializable. 12 | * 13 | * Other than `type`, the structure of an action object is really up to you. 14 | * If you're interested, check out Flux Standard Action for recommendations on 15 | * how actions should be constructed. 16 | * 17 | * @template T the type of the action's `type` tag. 18 | */ 19 | export interface Action { 20 | type: T; 21 | } 22 | 23 | /* reducers */ 24 | 25 | /** 26 | * A *reducer* (also called a *reducing function*) is a function that accepts 27 | * an accumulation and a value and returns a new accumulation. They are used 28 | * to reduce a collection of values down to a single value 29 | * 30 | * Reducers are not unique to Redux—they are a fundamental concept in 31 | * functional programming. Even most non-functional languages, like 32 | * JavaScript, have a built-in API for reducing. In JavaScript, it's 33 | * `Array.prototype.reduce()`. 34 | * 35 | * In Redux, the accumulated value is the state object, and the values being 36 | * accumulated are actions. Reducers calculate a new state given the previous 37 | * state and an action. They must be *pure functions*—functions that return 38 | * the exact same output for given inputs. They should also be free of 39 | * side-effects. This is what enables exciting features like hot reloading and 40 | * time travel. 41 | * 42 | * Reducers are the most important concept in Redux. 43 | * 44 | * *Do not put API calls into reducers.* 45 | * 46 | * @template S The type of state consumed and produced by this reducer. 47 | * @template A The type of actions the reducer can potentially respond to. 48 | */ 49 | export type Reducer = (state: S | undefined, action: A) => S; 50 | 51 | /** 52 | * Object whose values correspond to different reducer functions. 53 | * 54 | * @template A The type of actions the reducers can potentially respond to. 55 | */ 56 | export type ReducersMapObject = { 57 | [K in keyof S]: Reducer; 58 | } 59 | 60 | /** 61 | * Turns an object whose values are different reducer functions, into a single 62 | * reducer function. It will call every child reducer, and gather their results 63 | * into a single state object, whose keys correspond to the keys of the passed 64 | * reducer functions. 65 | * 66 | * @template S Combined state object type. 67 | * 68 | * @param reducers An object whose values correspond to different reducer 69 | * functions that need to be combined into one. One handy way to obtain it 70 | * is to use ES6 `import * as reducers` syntax. The reducers may never 71 | * return undefined for any action. Instead, they should return their 72 | * initial state if the state passed to them was undefined, and the current 73 | * state for any unrecognized action. 74 | * 75 | * @returns A reducer function that invokes every reducer inside the passed 76 | * object, and builds a state object with the same shape. 77 | */ 78 | export function combineReducers(reducers: ReducersMapObject): Reducer; 79 | 80 | 81 | /* store */ 82 | 83 | /** 84 | * A *dispatching function* (or simply *dispatch function*) is a function that 85 | * accepts an action or an async action; it then may or may not dispatch one 86 | * or more actions to the store. 87 | * 88 | * We must distinguish between dispatching functions in general and the base 89 | * `dispatch` function provided by the store instance without any middleware. 90 | * 91 | * The base dispatch function *always* synchronously sends an action to the 92 | * store's reducer, along with the previous state returned by the store, to 93 | * calculate a new state. It expects actions to be plain objects ready to be 94 | * consumed by the reducer. 95 | * 96 | * Middleware wraps the base dispatch function. It allows the dispatch 97 | * function to handle async actions in addition to actions. Middleware may 98 | * transform, delay, ignore, or otherwise interpret actions or async actions 99 | * before passing them to the next middleware. 100 | * 101 | * @template D the type of things (actions or otherwise) which may be dispatched. 102 | */ 103 | export interface Dispatch { 104 | (action: A): A; 105 | } 106 | 107 | /** 108 | * Function to remove listener added by `Store.subscribe()`. 109 | */ 110 | export interface Unsubscribe { 111 | (): void; 112 | } 113 | 114 | /** 115 | * A store is an object that holds the application's state tree. 116 | * There should only be a single store in a Redux app, as the composition 117 | * happens on the reducer level. 118 | * 119 | * @template S The type of state held by this store. 120 | * @template A the type of actions which may be dispatched by this store. 121 | * @template N The type of non-actions which may be dispatched by this store. 122 | */ 123 | export interface Store { 124 | /** 125 | * Dispatches an action. It is the only way to trigger a state change. 126 | * 127 | * The `reducer` function, used to create the store, will be called with the 128 | * current state tree and the given `action`. Its return value will be 129 | * considered the **next** state of the tree, and the change listeners will 130 | * be notified. 131 | * 132 | * The base implementation only supports plain object actions. If you want 133 | * to dispatch a Promise, an Observable, a thunk, or something else, you 134 | * need to wrap your store creating function into the corresponding 135 | * middleware. For example, see the documentation for the `redux-thunk` 136 | * package. Even the middleware will eventually dispatch plain object 137 | * actions using this method. 138 | * 139 | * @param action A plain object representing “what changed”. It is a good 140 | * idea to keep actions serializable so you can record and replay user 141 | * sessions, or use the time travelling `redux-devtools`. An action must 142 | * have a `type` property which may not be `undefined`. It is a good idea 143 | * to use string constants for action types. 144 | * 145 | * @returns For convenience, the same action object you dispatched. 146 | * 147 | * Note that, if you use a custom middleware, it may wrap `dispatch()` to 148 | * return something else (for example, a Promise you can await). 149 | */ 150 | dispatch: Dispatch; 151 | 152 | /** 153 | * Reads the state tree managed by the store. 154 | * 155 | * @returns The current state tree of your application. 156 | */ 157 | getState(): S; 158 | 159 | /** 160 | * Adds a change listener. It will be called any time an action is 161 | * dispatched, and some part of the state tree may potentially have changed. 162 | * You may then call `getState()` to read the current state tree inside the 163 | * callback. 164 | * 165 | * You may call `dispatch()` from a change listener, with the following 166 | * caveats: 167 | * 168 | * 1. The subscriptions are snapshotted just before every `dispatch()` call. 169 | * If you subscribe or unsubscribe while the listeners are being invoked, 170 | * this will not have any effect on the `dispatch()` that is currently in 171 | * progress. However, the next `dispatch()` call, whether nested or not, 172 | * will use a more recent snapshot of the subscription list. 173 | * 174 | * 2. The listener should not expect to see all states changes, as the state 175 | * might have been updated multiple times during a nested `dispatch()` before 176 | * the listener is called. It is, however, guaranteed that all subscribers 177 | * registered before the `dispatch()` started will be called with the latest 178 | * state by the time it exits. 179 | * 180 | * @param listener A callback to be invoked on every dispatch. 181 | * @returns A function to remove this change listener. 182 | */ 183 | subscribe(listener: () => void): Unsubscribe; 184 | 185 | /** 186 | * Replaces the reducer currently used by the store to calculate the state. 187 | * 188 | * You might need this if your app implements code splitting and you want to 189 | * load some of the reducers dynamically. You might also need this if you 190 | * implement a hot reloading mechanism for Redux. 191 | * 192 | * @param nextReducer The reducer for the store to use instead. 193 | */ 194 | replaceReducer(nextReducer: Reducer): void; 195 | } 196 | 197 | export type DeepPartial = {[K in keyof T]?: DeepPartial }; 198 | 199 | /** 200 | * A store creator is a function that creates a Redux store. Like with 201 | * dispatching function, we must distinguish the base store creator, 202 | * `createStore(reducer, preloadedState)` exported from the Redux package, from 203 | * store creators that are returned from the store enhancers. 204 | * 205 | * @template S The type of state to be held by the store. 206 | * @template A The type of actions which may be dispatched. 207 | * @template D The type of all things which may be dispatched. 208 | */ 209 | export interface StoreCreator { 210 | (reducer: Reducer, enhancer?: StoreEnhancer): Store; 211 | (reducer: Reducer, preloadedState: DeepPartial, enhancer?: StoreEnhancer): Store; 212 | } 213 | 214 | /** 215 | * A store enhancer is a higher-order function that composes a store creator 216 | * to return a new, enhanced store creator. This is similar to middleware in 217 | * that it allows you to alter the store interface in a composable way. 218 | * 219 | * Store enhancers are much the same concept as higher-order components in 220 | * React, which are also occasionally called “component enhancers”. 221 | * 222 | * Because a store is not an instance, but rather a plain-object collection of 223 | * functions, copies can be easily created and modified without mutating the 224 | * original store. There is an example in `compose` documentation 225 | * demonstrating that. 226 | * 227 | * Most likely you'll never write a store enhancer, but you may use the one 228 | * provided by the developer tools. It is what makes time travel possible 229 | * without the app being aware it is happening. Amusingly, the Redux 230 | * middleware implementation is itself a store enhancer. 231 | * 232 | */ 233 | export type StoreEnhancer = (next: StoreEnhancerStoreCreator) => StoreEnhancerStoreCreator; 234 | export type GenericStoreEnhancer = StoreEnhancer; 235 | export type StoreEnhancerStoreCreator = (reducer: Reducer, preloadedState?: DeepPartial) => Store; 236 | 237 | /** 238 | * Creates a Redux store that holds the state tree. 239 | * The only way to change the data in the store is to call `dispatch()` on it. 240 | * 241 | * There should only be a single store in your app. To specify how different 242 | * parts of the state tree respond to actions, you may combine several 243 | * reducers 244 | * into a single reducer function by using `combineReducers`. 245 | * 246 | * @template S State object type. 247 | * 248 | * @param reducer A function that returns the next state tree, given the 249 | * current state tree and the action to handle. 250 | * 251 | * @param [preloadedState] The initial state. You may optionally specify it to 252 | * hydrate the state from the server in universal apps, or to restore a 253 | * previously serialized user session. If you use `combineReducers` to 254 | * produce the root reducer function, this must be an object with the same 255 | * shape as `combineReducers` keys. 256 | * 257 | * @param [enhancer] The store enhancer. You may optionally specify it to 258 | * enhance the store with third-party capabilities such as middleware, time 259 | * travel, persistence, etc. The only store enhancer that ships with Redux 260 | * is `applyMiddleware()`. 261 | * 262 | * @returns A Redux store that lets you read the state, dispatch actions and 263 | * subscribe to changes. 264 | */ 265 | export const createStore: StoreCreator; 266 | 267 | 268 | /* middleware */ 269 | 270 | export interface MiddlewareAPI { 271 | dispatch: Dispatch; 272 | getState(): S; 273 | } 274 | 275 | /** 276 | * A middleware is a higher-order function that composes a dispatch function 277 | * to return a new dispatch function. It often turns async actions into 278 | * actions. 279 | * 280 | * Middleware is composable using function composition. It is useful for 281 | * logging actions, performing side effects like routing, or turning an 282 | * asynchronous API call into a series of synchronous actions. 283 | */ 284 | export interface Middleware { 285 | (api: MiddlewareAPI): (next: Dispatch) => Dispatch; 286 | } 287 | 288 | /** 289 | * Creates a store enhancer that applies middleware to the dispatch method 290 | * of the Redux store. This is handy for a variety of tasks, such as 291 | * expressing asynchronous actions in a concise manner, or logging every 292 | * action payload. 293 | * 294 | * See `redux-thunk` package as an example of the Redux middleware. 295 | * 296 | * Because middleware is potentially asynchronous, this should be the first 297 | * store enhancer in the composition chain. 298 | * 299 | * Note that each middleware will be given the `dispatch` and `getState` 300 | * functions as named arguments. 301 | * 302 | * @param middlewares The middleware chain to be applied. 303 | * @returns A store enhancer applying the middleware. 304 | */ 305 | export function applyMiddleware(...middlewares: Middleware[]): GenericStoreEnhancer; 306 | 307 | 308 | /* action creators */ 309 | 310 | /** 311 | * An *action creator* is, quite simply, a function that creates an action. Do 312 | * not confuse the two terms—again, an action is a payload of information, and 313 | * an action creator is a factory that creates an action. 314 | * 315 | * Calling an action creator only produces an action, but does not dispatch 316 | * it. You need to call the store's `dispatch` function to actually cause the 317 | * mutation. Sometimes we say *bound action creators* to mean functions that 318 | * call an action creator and immediately dispatch its result to a specific 319 | * store instance. 320 | * 321 | * If an action creator needs to read the current state, perform an API call, 322 | * or cause a side effect, like a routing transition, it should return an 323 | * async action instead of an action. 324 | * 325 | * @template A Returned action type. 326 | */ 327 | export interface ActionCreator { 328 | (...args: any[]): A; 329 | } 330 | 331 | /** 332 | * Object whose values are action creator functions. 333 | */ 334 | export interface ActionCreatorsMapObject { 335 | [key: string]: ActionCreator; 336 | } 337 | 338 | /** 339 | * Turns an object whose values are action creators, into an object with the 340 | * same keys, but with every function wrapped into a `dispatch` call so they 341 | * may be invoked directly. This is just a convenience method, as you can call 342 | * `store.dispatch(MyActionCreators.doSomething())` yourself just fine. 343 | * 344 | * For convenience, you can also pass a single function as the first argument, 345 | * and get a function in return. 346 | * 347 | * @param actionCreator An object whose values are action creator functions. 348 | * One handy way to obtain it is to use ES6 `import * as` syntax. You may 349 | * also pass a single function. 350 | * 351 | * @param dispatch The `dispatch` function available on your Redux store. 352 | * 353 | * @returns The object mimicking the original object, but with every action 354 | * creator wrapped into the `dispatch` call. If you passed a function as 355 | * `actionCreator`, the return value will also be a single function. 356 | */ 357 | export function bindActionCreators>(actionCreator: C, dispatch: Dispatch): C; 358 | 359 | export function bindActionCreators< 360 | A extends ActionCreator, 361 | B extends ActionCreator 362 | >(actionCreator: A, dispatch: Dispatch): B; 363 | 364 | export function bindActionCreators>(actionCreators: M, dispatch: Dispatch): M; 365 | 366 | export function bindActionCreators< 367 | M extends ActionCreatorsMapObject, 368 | N extends ActionCreatorsMapObject 369 | >(actionCreators: M, dispatch: Dispatch): N; 370 | 371 | 372 | /* compose */ 373 | 374 | type Func0 = () => R; 375 | type Func1 = (a1: T1) => R; 376 | type Func2 = (a1: T1, a2: T2) => R; 377 | type Func3 = (a1: T1, a2: T2, a3: T3, ...args: any[]) => R; 378 | 379 | /** 380 | * Composes single-argument functions from right to left. The rightmost 381 | * function can take multiple arguments as it provides the signature for the 382 | * resulting composite function. 383 | * 384 | * @param funcs The functions to compose. 385 | * @returns R function obtained by composing the argument functions from right 386 | * to left. For example, `compose(f, g, h)` is identical to doing 387 | * `(...args) => f(g(h(...args)))`. 388 | */ 389 | export function compose(): (a: R) => R; 390 | 391 | export function compose(f: F): F; 392 | 393 | /* two functions */ 394 | export function compose( 395 | f1: (b: A) => R, f2: Func0 396 | ): Func0; 397 | export function compose( 398 | f1: (b: A) => R, f2: Func1 399 | ): Func1; 400 | export function compose( 401 | f1: (b: A) => R, f2: Func2 402 | ): Func2; 403 | export function compose( 404 | f1: (b: A) => R, f2: Func3 405 | ): Func3; 406 | 407 | /* three functions */ 408 | export function compose( 409 | f1: (b: B) => R, f2: (a: A) => B, f3: Func0 410 | ): Func0; 411 | export function compose( 412 | f1: (b: B) => R, f2: (a: A) => B, f3: Func1 413 | ): Func1; 414 | export function compose( 415 | f1: (b: B) => R, f2: (a: A) => B, f3: Func2 416 | ): Func2; 417 | export function compose( 418 | f1: (b: B) => R, f2: (a: A) => B, f3: Func3 419 | ): Func3; 420 | 421 | /* four functions */ 422 | export function compose( 423 | f1: (b: C) => R, f2: (a: B) => C, f3: (a: A) => B, f4: Func0 424 | ): Func0; 425 | export function compose( 426 | f1: (b: C) => R, f2: (a: B) => C, f3: (a: A) => B, f4: Func1 427 | ): Func1; 428 | export function compose( 429 | f1: (b: C) => R, f2: (a: B) => C, f3: (a: A) => B, f4: Func2 430 | ): Func2; 431 | export function compose( 432 | f1: (b: C) => R, f2: (a: B) => C, f3: (a: A) => B, f4: Func3 433 | ): Func3; 434 | 435 | /* rest */ 436 | export function compose( 437 | f1: (b: any) => R, ...funcs: Function[] 438 | ): (...args: any[]) => R; 439 | 440 | export function compose(...funcs: Function[]): (...args: any[]) => R; 441 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", // enables relative imports to root 4 | "paths": { 5 | "redux": ["src/typings/redux.d.ts"], 6 | "Components/*": ["src/components/*"], 7 | "Features/*": ["src/features/*"], 8 | }, 9 | "outDir": "out/", // target for compiled files 10 | "allowSyntheticDefaultImports": true, // no errors on commonjs default import 11 | "allowJs": true, // include js files 12 | "checkJs": true, // typecheck js files 13 | "declaration": false, // don't emit declarations 14 | "emitDecoratorMetadata": true, 15 | "experimentalDecorators": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "importHelpers": true, // importing helper functions from tslib 18 | "noEmitHelpers": true, // disable emitting inline helper functions 19 | "jsx": "react", // process JSX 20 | "lib": [ 21 | "dom", 22 | "es2016", 23 | "es2017.object" 24 | ], 25 | "target": "es2015", 26 | "module": "es2015", 27 | "moduleResolution": "node", 28 | "noEmitOnError": true, 29 | "noFallthroughCasesInSwitch": true, 30 | "noImplicitAny": true, 31 | "noImplicitReturns": true, 32 | "noImplicitThis": true, 33 | "strictNullChecks": true, 34 | "pretty": true, 35 | "removeComments": true, 36 | "sourceMap": true, 37 | "types":[ 38 | "webpack-env" 39 | ] 40 | }, 41 | "include": [ 42 | "src/**/*" 43 | ], 44 | "exclude": [ 45 | "node_modules", 46 | "dist", 47 | "src/**/*.spec.*" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react"], 3 | "rules": { 4 | "arrow-parens": false, 5 | "arrow-return-shorthand": [false], 6 | "comment-format": [true, "check-space"], 7 | "import-blacklist": [true, "rxjs"], 8 | "interface-over-type-literal": false, 9 | "interface-name": false, 10 | "max-line-length": [true, 120], 11 | "member-access": false, 12 | "member-ordering": [true, { "order": "fields-first" }], 13 | "newline-before-return": false, 14 | "no-any": false, 15 | "no-empty-interface": false, 16 | "no-import-side-effect": [true], 17 | "no-inferrable-types": [true, "ignore-params", "ignore-properties"], 18 | "no-invalid-this": [true, "check-function-in-method"], 19 | "no-null-keyword": false, 20 | "no-require-imports": false, 21 | "no-submodule-imports": [false], 22 | "no-this-assignment": [true, { "allow-destructuring": true }], 23 | "no-trailing-whitespace": true, 24 | "no-unused-variable": [true, "react"], 25 | "object-literal-sort-keys": false, 26 | "object-literal-shorthand": false, 27 | "one-variable-per-declaration": [false], 28 | "only-arrow-functions": [true, "allow-declarations"], 29 | "ordered-imports": [false], 30 | "prefer-method-signature": false, 31 | "prefer-template": [true, "allow-single-concat"], 32 | "quotemark": [true, "single", "jsx-double"], 33 | "semicolon": [true, "always"], 34 | "trailing-comma": [true, { 35 | "singleline": "never", 36 | "multiline": { 37 | "objects": "always", 38 | "arrays": "always", 39 | "functions": "never", 40 | "typeLiterals": "ignore" 41 | }, 42 | "esSpecCompliant": true 43 | }], 44 | "triple-equals": [true, "allow-null-check"], 45 | "type-literal-delimiter": true, 46 | "typedef": [true,"parameter", "property-declaration"], 47 | "variable-name": [true, "ban-keywords", "check-format", "allow-pascal-case", "allow-leading-underscore"], 48 | // tslint-react 49 | "jsx-no-lambda": false, 50 | "jsx-no-multiline-js": false 51 | } 52 | } --------------------------------------------------------------------------------