├── public ├── favicon.ico ├── manifest.json └── index.html ├── tsconfig.test.json ├── src ├── container │ ├── About │ │ ├── Loadable.ts │ │ ├── action.ts │ │ ├── selector.ts │ │ ├── reducer.ts │ │ ├── epics.ts │ │ └── index.tsx │ ├── Main │ │ ├── Loadable.ts │ │ ├── reducer.ts │ │ ├── epics.ts │ │ └── index.tsx │ └── App │ │ ├── reducer.ts │ │ ├── epics.ts │ │ └── index.tsx ├── routes.ts ├── epics.ts ├── component │ └── CustomPropsToRoutes │ │ └── index.tsx ├── types │ └── index.d.ts ├── reducers.ts ├── index.css ├── utils │ ├── reducerInjectors.ts │ └── injectReducer.tsx ├── index.tsx ├── configureStore.ts ├── logo.svg └── registerServiceWorker.ts ├── config ├── jest │ ├── typescriptTransform.js │ ├── fileTransform.js │ └── cssTransform.js ├── polyfills.js ├── paths.js ├── env.js ├── webpackDevServer.config.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── .gitignore ├── Dockerfile ├── tsconfig.json ├── scripts ├── test.js ├── start.js └── build.js ├── LICENSE ├── conf └── nginx.conf ├── tslint.json ├── README.md └── package.json /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aaaaash/typescript-react-redux-starter-kit/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /src/container/About/Loadable.ts: -------------------------------------------------------------------------------- 1 | import Loadable from 'react-loadable'; 2 | 3 | export default Loadable({ 4 | loader: () => import('./index'), 5 | loading: () => null, 6 | }); 7 | -------------------------------------------------------------------------------- /src/container/Main/Loadable.ts: -------------------------------------------------------------------------------- 1 | import Loadable from 'react-loadable'; 2 | 3 | export default Loadable({ 4 | loader: () => import('./index'), 5 | loading: () => null, 6 | }); 7 | -------------------------------------------------------------------------------- /config/jest/typescriptTransform.js: -------------------------------------------------------------------------------- 1 | // Copyright 2004-present Facebook. All Rights Reserved. 2 | 3 | 'use strict'; 4 | 5 | const tsJestPreprocessor = require('ts-jest/preprocessor'); 6 | 7 | module.exports = tsJestPreprocessor; 8 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | import About from './container/About/Loadable'; 2 | import Main from './container/Main/Loadable'; 3 | 4 | export default [ 5 | { path: '/', exact: true, component: Main }, 6 | { path: '/about', component: About }, 7 | ]; 8 | -------------------------------------------------------------------------------- /src/container/About/action.ts: -------------------------------------------------------------------------------- 1 | export function someAction(name: string) { 2 | return { 3 | type: 'GET_SOME_DATA', 4 | name, 5 | }; 6 | } 7 | 8 | export function getSuccess(data: object) { 9 | return { 10 | type: 'FETCH_USER_FULFILLED', 11 | data, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/epics.ts: -------------------------------------------------------------------------------- 1 | import { combineEpics, Epic } from 'redux-observable'; 2 | import appEpics from './container/App/epics'; 3 | import { Action, LifeStore } from './types'; 4 | 5 | export default function createRootEpics(): Epic { 6 | return combineEpics(appEpics); 7 | } 8 | -------------------------------------------------------------------------------- /src/container/App/reducer.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import { Reducer } from 'redux'; 3 | 4 | import { Action, State } from '../../types'; 5 | 6 | const initialState = fromJS({}); 7 | 8 | const reducer: Reducer = 9 | (state: State = initialState, action: Action) => { 10 | return state; 11 | } 12 | 13 | export default reducer; 14 | -------------------------------------------------------------------------------- /src/container/Main/reducer.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import { Reducer } from 'redux'; 3 | 4 | import { Action, State } from '../../types'; 5 | 6 | const initialState = fromJS({}); 7 | 8 | const reducer: Reducer = 9 | (state: State = initialState, action: Action) => { 10 | return state; 11 | } 12 | 13 | export default reducer; 14 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/container/About/selector.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | import { ReduxState } from '../../types'; 4 | 5 | const selectAbout = (state: ReduxState) => state.get('about'); 6 | 7 | const selectMyGithubInfo = () => createSelector( 8 | selectAbout, 9 | (state: ReduxState) => state.get('myInfo').toJS() 10 | ); 11 | 12 | export { 13 | selectMyGithubInfo, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/component/CustomPropsToRoutes/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | 4 | const CustomPropsToRoute = ({ component: Component, initialData, ...rest }: any) => ( 5 | ( 8 | 9 | )} 10 | /> 11 | ) 12 | 13 | export default CustomPropsToRoute; 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:9.2.0 2 | RUN apt-get update \ 3 | && apt-get install -y nginx \ 4 | && apt-get install -y vim \ 5 | && rm -rf /etc/nginx/nginx.conf 6 | COPY conf/nginx.conf /etc/nginx 7 | WORKDIR /app 8 | COPY . /app/ 9 | EXPOSE 80 10 | RUN npm install \ 11 | && npm run build \ 12 | && cp -r build/* /var/www/html \ 13 | && rm -rf /app 14 | CMD ["nginx","-g","daemon off;"] 15 | -------------------------------------------------------------------------------- /src/container/App/epics.ts: -------------------------------------------------------------------------------- 1 | import { ActionsObservable, Epic, combineEpics } from 'redux-observable'; 2 | import { Action, LifeStore } from '../../types'; 3 | import 'rxjs'; 4 | 5 | const pingEpic: Epic = (action$: ActionsObservable) => 6 | action$.filter((action: Action) => action.type === 'PING') 7 | .delay(1000) 8 | .mapTo({ type: 'PONG' }); 9 | 10 | export default combineEpics( 11 | pingEpic 12 | ); 13 | -------------------------------------------------------------------------------- /src/container/Main/epics.ts: -------------------------------------------------------------------------------- 1 | import { ActionsObservable, Epic, combineEpics } from 'redux-observable'; 2 | import { Action, LifeStore } from '../../types'; 3 | import 'rxjs'; 4 | 5 | const pingEpic: Epic = (action$: ActionsObservable) => 6 | action$.filter((action: Action) => action.type === 'PING') 7 | .delay(1000) 8 | .mapTo({ type: 'PONG' }); 9 | 10 | export default combineEpics( 11 | pingEpic 12 | ); 13 | -------------------------------------------------------------------------------- /src/container/About/reducer.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import { Reducer } from 'redux'; 3 | 4 | import { Action, State } from '../../types'; 5 | 6 | const initialState = fromJS({ 7 | myInfo: {} 8 | }); 9 | 10 | const reducer: Reducer = 11 | (state: State = initialState, action: Action) => { 12 | switch (action.type) { 13 | case 'FETCH_USER_FULFILLED': 14 | return state.set('myInfo', fromJS(action.data)); 15 | default: 16 | return state; 17 | } 18 | } 19 | 20 | export default reducer; 21 | -------------------------------------------------------------------------------- /src/container/App/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, ReactNode } from 'react'; 2 | import { withRouter, Route, Switch } from 'react-router-dom'; 3 | 4 | import Main from '../Main/Loadable'; 5 | import About from '../About/Loadable'; 6 | 7 | class App extends PureComponent { 8 | render(): ReactNode { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 |
16 | ); 17 | } 18 | } 19 | 20 | export default withRouter(App); 21 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, Reducer, Unsubscribe, ReducersMapObject, Store } from 'redux'; 2 | import { RouterState } from 'react-router-redux'; 3 | 4 | export interface LifeStore extends Store<{}> { 5 | injectedReducers?: any; 6 | } 7 | 8 | export interface Action { 9 | type: string; 10 | [propName: string]: any; 11 | } 12 | 13 | export interface InjectReducerParams { 14 | key: string; 15 | reducer: (state: any, action: Action) => any; 16 | } 17 | 18 | export interface ReduxState { 19 | route: RouterState; 20 | global: object; 21 | [propName: string]: any; 22 | } 23 | 24 | export type State = Map<{}, {}>; 25 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "es5", 6 | "lib": ["es6", "dom"], 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "jsx": "react", 10 | "moduleResolution": "node", 11 | "rootDir": "src", 12 | "forceConsistentCasingInFileNames": true, 13 | "allowSyntheticDefaultImports": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "build", 24 | "scripts", 25 | "acceptance-tests", 26 | "webpack", 27 | "jest", 28 | "src/setupTests.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/reducers.ts: -------------------------------------------------------------------------------- 1 | import { fromJS, Map } from 'immutable'; 2 | import { combineReducers } from 'redux-immutable'; 3 | import { LOCATION_CHANGE } from 'react-router-redux'; 4 | 5 | import globalReducer from './container/App/reducer'; 6 | import { Action } from './types'; 7 | 8 | const routeInitialState = fromJS({ 9 | location: null, 10 | }); 11 | 12 | function routeReducer(state: Map<{}, {}> = routeInitialState, action: Action) { 13 | switch (action.type) { 14 | case LOCATION_CHANGE: 15 | return { ...state, ...action.payload }; 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | export default function createReducer(injectedReducers?: any) { 22 | return combineReducers({ 23 | route: routeReducer, 24 | global: globalReducer, 25 | ...injectedReducers, 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | appearance: none; 18 | -webkit-appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | 23 | body { 24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 25 | line-height: 1.4em; 26 | color: #4d4d4d; 27 | min-width: 230px; 28 | margin: 0 auto; 29 | -webkit-font-smoothing: antialiased; 30 | -moz-osx-font-smoothing: grayscale; 31 | font-weight: 300; 32 | } 33 | 34 | :focus { 35 | outline: 0; 36 | } 37 | 38 | .hidden { 39 | display: none; 40 | } 41 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | const argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /src/container/About/epics.ts: -------------------------------------------------------------------------------- 1 | import { ActionsObservable, Epic, combineEpics } from 'redux-observable'; 2 | import { ajax } from 'rxjs/observable/dom/ajax'; 3 | import { Action, LifeStore } from '../../types'; 4 | import 'rxjs'; 5 | 6 | import { 7 | getSuccess, 8 | } from './action'; 9 | 10 | const pingEpic: Epic = (action$: ActionsObservable) => 11 | action$.filter((action: Action) => action.type === 'PING') 12 | .delay(1000) 13 | .mapTo({ type: 'PONG' }); 14 | 15 | const fetchUserEpic: Epic = (action$: ActionsObservable) => 16 | action$.ofType('GET_SOME_DATA') 17 | .mergeMap((action: Action) => 18 | ajax.getJSON(`https://api.github.com/users/${action.name}`) 19 | .map(response => getSuccess(response)) 20 | ); 21 | 22 | export default combineEpics( 23 | pingEpic, 24 | fetchUserEpic 25 | ); 26 | -------------------------------------------------------------------------------- /src/utils/reducerInjectors.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import { isEmpty, isFunction, isString } from 'lodash'; 3 | import { Reducer } from 'redux'; 4 | import createReducer from '../reducers'; 5 | import { LifeStore } from '../types'; 6 | 7 | export function injectReducerFactory(store: LifeStore) { 8 | return function injectReducer(key: string, reducer: Reducer) { 9 | invariant( 10 | isString(key) && !isEmpty(key) && isFunction(reducer), 11 | '(app/utils...) injectReducer: Expected `reducer` to be a reducer function' 12 | ); 13 | if (Reflect.has(store.injectedReducers, key) && store.injectedReducers[key] === reducer) return; 14 | 15 | store.injectedReducers[key] = reducer; 16 | store.replaceReducer(createReducer(store.injectedReducers)); 17 | }; 18 | } 19 | 20 | export default function getInjectors(store: LifeStore) { 21 | return { 22 | injectReducer: injectReducerFactory(store), 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/injectReducer.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentType } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import hoistNonReactStatics from 'hoist-non-react-statics'; 4 | 5 | import getInjectors from './reducerInjectors'; 6 | import { InjectReducerParams } from '../types'; 7 | 8 | export default ({ key, reducer }: InjectReducerParams) => (WrappedComponent: ComponentType) => { 9 | class ReducerInjector extends React.PureComponent { 10 | static WrappedComponent = WrappedComponent; 11 | static contextTypes = { 12 | store: PropTypes.object.isRequired, 13 | }; 14 | static displayName = `withReducer(${(WrappedComponent.displayName || WrappedComponent.name || 'Component')})`; 15 | 16 | injectors = getInjectors(this.context.store); 17 | 18 | componentWillMount() { 19 | const { injectReducer } = this.injectors; 20 | 21 | injectReducer(key, reducer); 22 | } 23 | 24 | render() { 25 | return ; 26 | } 27 | } 28 | return hoistNonReactStatics(ReducerInjector, WrappedComponent); 29 | }; 30 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { Router } from 'react-router-dom'; 5 | import createHistory from 'history/createBrowserHistory'; 6 | import { History } from 'history'; 7 | 8 | import App from './container/App'; 9 | import './index.css'; 10 | import configureStore from './configureStore'; 11 | import createReducer from './reducers'; 12 | import registerServiceWorker from './registerServiceWorker'; 13 | 14 | import { LifeStore } from './types/'; 15 | 16 | const history: History = createHistory(); 17 | const initialState: object = {}; 18 | 19 | const store: LifeStore = configureStore(initialState, history); 20 | 21 | if (module.hot) { 22 | module.hot.accept('./reducers', () => { 23 | store.replaceReducer(createReducer(store.injectedReducers)); 24 | }); 25 | } 26 | 27 | ReactDOM.render( 28 | 29 | 30 | 31 | 32 | , 33 | document.getElementById('root') as HTMLElement 34 | ); 35 | registerServiceWorker(); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 大表哥 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 | -------------------------------------------------------------------------------- /src/container/Main/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, ReactNode } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import { compose, Dispatch } from 'redux'; 5 | 6 | import injectReducer from '../../utils/injectReducer'; 7 | import { ReduxState } from '../../types'; 8 | import reducer from './reducer'; 9 | import mainEpics from './epics'; 10 | import { injectEpics } from '../../configureStore'; 11 | 12 | class Main extends PureComponent { 13 | render(): ReactNode { 14 | return ( 15 |
16 |

this is main page!

17 | about 18 |
19 | ); 20 | } 21 | } 22 | 23 | injectEpics('main', mainEpics); 24 | 25 | const mapStateToProps = (state: ReduxState) => ({ 26 | }); 27 | 28 | const mapDispatchToProps = (dispatch: Dispatch) => ({ 29 | }); 30 | 31 | function mergePropss(stateProps: Object, dispatchProps: Object, ownProps: Object) { 32 | return Object.assign({}, ownProps, stateProps, dispatchProps); 33 | } 34 | 35 | const withReducer = injectReducer({ key: 'main', reducer }); 36 | const withConnect = connect(mapStateToProps, mapDispatchToProps, mergePropss); 37 | 38 | export default compose(withReducer, withConnect)(Main); 39 | -------------------------------------------------------------------------------- /src/container/About/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, ReactNode } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStructuredSelector } from 'reselect'; 4 | import { compose, Dispatch } from 'redux'; 5 | 6 | import injectReducer from '../../utils/injectReducer'; 7 | import { ReduxState } from '../../types'; 8 | import reducer from './reducer'; 9 | import aboutEpics from './epics'; 10 | import { injectEpics } from '../../configureStore'; 11 | import { 12 | someAction, 13 | } from './action'; 14 | import { 15 | selectMyGithubInfo, 16 | } from './selector'; 17 | 18 | interface Props { 19 | asyncRequest: (name: string) => void; 20 | myInfo: Object; 21 | } 22 | 23 | class About extends PureComponent { 24 | componentDidMount() { 25 | this.props.asyncRequest('sakuraash'); 26 | } 27 | 28 | render(): ReactNode { 29 | const { myInfo } = this.props; 30 | console.log(myInfo); 31 | return
this is about page
; 32 | } 33 | } 34 | 35 | injectEpics('about', aboutEpics); 36 | 37 | const mapStateToProps = (state: ReduxState) => createStructuredSelector({ 38 | myInfo: selectMyGithubInfo(), 39 | }); 40 | 41 | const mapDispatchToProps = (dispatch: Dispatch) => ({ 42 | asyncRequest: (name: string) => dispatch(someAction(name)) 43 | }); 44 | 45 | function mergePropss(stateProps: Object, dispatchProps: Object, ownProps: Object) { 46 | return Object.assign({}, ownProps, stateProps, dispatchProps); 47 | } 48 | 49 | const withReducer = injectReducer({ key: 'about', reducer }); 50 | const withConnect = connect(mapStateToProps, mapDispatchToProps, mergePropss); 51 | 52 | export default compose(withReducer, withConnect)(About); 53 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | about life 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { routerMiddleware } from 'react-router-redux'; 2 | import { History } from 'history'; 3 | import { fromJS } from 'immutable'; 4 | import { 5 | applyMiddleware, 6 | GenericStoreEnhancer, 7 | compose, 8 | createStore, 9 | Middleware, 10 | MiddlewareAPI, 11 | } from 'redux'; 12 | import 'rxjs'; 13 | import { createEpicMiddleware, ActionsObservable, Epic } from 'redux-observable'; 14 | 15 | import createReducer from './reducers'; 16 | import { LifeStore, Action } from './types'; 17 | import createRootEpics from './epics'; 18 | import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 19 | 20 | const epics = new BehaviorSubject(createRootEpics()); 21 | const rootEpic: Epic = (action$: ActionsObservable, store: MiddlewareAPI) => 22 | epics.mergeMap(epic => 23 | epic(action$, store, {}) 24 | ); 25 | const dependencies = {}; 26 | const epicsMiddleware = createEpicMiddleware(rootEpic, { dependencies }); 27 | 28 | export function injectEpics(key: string, newEpics: Epic, newDependencies?: {}): void { 29 | Object.assign(dependencies, newDependencies); 30 | 31 | epics.next(newEpics); 32 | // newEpics.map((epic: Epic) => epics.next(epic)); 33 | console.log(`${key} page Epic is loaded!`); 34 | } 35 | 36 | export default (initialState = {}, history: History): LifeStore => { 37 | const middlewares: Middleware[] = [ 38 | routerMiddleware(history), 39 | epicsMiddleware, 40 | ]; 41 | 42 | const enhaners: GenericStoreEnhancer[] = [ 43 | applyMiddleware(...middlewares), 44 | ]; 45 | 46 | const composeEnhancers: Function = 47 | (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 48 | ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 49 | shouldHotReload: false, 50 | }) 51 | : compose; 52 | 53 | const store: LifeStore = createStore( 54 | createReducer(), 55 | fromJS(initialState), 56 | composeEnhancers(...enhaners) 57 | ); 58 | 59 | store.injectedReducers = {}; 60 | return store; 61 | }; 62 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right