import{ createStore, applyMiddleware, compose }from'redux';import thunk from'redux-thunk';import{ routerMiddleware }from'react-router-redux';import history from'./history';import rootReducer from'./rootReducer';const router =routerMiddleware(history);// NOTE: Do not change middleares delaration pattern since rekit plugins may register middlewares to it.const middlewares =[
thunk,
router,];letdevToolsExtension= f => f;/* istanbul ignore if */if(process.env.NODE_ENV==='development'){const{ createLogger }=require('redux-logger');const logger =createLogger({ collapsed:true});
middlewares.push(logger);if(window.devToolsExtension){
devToolsExtension = window.devToolsExtension();}}exportdefaultfunctionconfigureStore(initialState){const store =createStore(rootReducer, initialState,compose(applyMiddleware(...middlewares),
devToolsExtension
));/* istanbul ignore if */if(module.hot){// Enable Webpack hot module replacement for reducers
module.hot.accept('./rootReducer',()=>{const nextRootReducer =require('./rootReducer').default;// eslint-disable-line
store.replaceReducer(nextRootReducer);});}return store;}
history.js
import createHistory from'history/createBrowserHistory';// A singleton history object for easy API navigationconst history =createHistory();exportdefault history;
rootReducer.js
import{ combineReducers }from'redux';import{ routerReducer }from'react-router-redux';import homeReducer from'../features/home/redux/reducer';import commonReducer from'../features/common/redux/reducer';import examplesReducer from'../features/examples/redux/reducer';// NOTE 1: DO NOT CHANGE the 'reducerMap' name and the declaration pattern.// This is used for Rekit cmds to register new features, remove features, etc.// NOTE 2: always use the camel case of the feature folder name as the store branch name// So that it's easy for others to understand it and Rekit could manage them.const reducerMap ={
router: routerReducer,
home: homeReducer,
common: commonReducer,
examples: examplesReducer,};exportdefaultcombineReducers(reducerMap);
routeConfig.js
import{ App }from'../features/home';import{ PageNotFound }from'../features/common';import homeRoute from'../features/home/route';import commonRoute from'../features/common/route';import examplesRoute from'../features/examples/route';import _ from'lodash';// NOTE: DO NOT CHANGE the 'childRoutes' name and the declaration pattern.// This is used for Rekit cmds to register routes config for new features, and remove config when remove features, etc.const childRoutes =[
homeRoute,
commonRoute,
examplesRoute,];const routes =[{
path:'/',
component: App,
childRoutes:[...childRoutes,{ path:'*', name:'Page not found', component: PageNotFound },].filter(r => r.component ||(r.childRoutes && r.childRoutes.length >0)),}];// Handle isIndex property of route config:// Dupicate it and put it as the first route rule.functionhandleIndexRoute(route){if(!route.childRoutes ||!route.childRoutes.length){return;}const indexRoute = _.find(route.childRoutes,(child => child.isIndex));if(indexRoute){const first ={...indexRoute };
first.path ='';
first.exact =true;
first.autoIndexRoute =true;// mark it so that the simple nav won't show it.
route.childRoutes.unshift(first);}
route.childRoutes.forEach(handleIndexRoute);}
routes.forEach(handleIndexRoute);exportdefault routes;
index.js
import React from'react';import{ AppContainer }from'react-hot-loader';import{ render }from'react-dom';import configStore from'./common/configStore';import routeConfig from'./common/routeConfig';import Root from'./Root';const store =configStore();functionrenderApp(app){render(<AppContainer>{app}</AppContainer>,
document.getElementById('root'));}renderApp(<Root store={store} routeConfig={routeConfig}/>);// Hot Module Replacement API/* istanbul ignore if */if(module.hot){
module.hot.accept('./common/routeConfig',()=>{const nextRouteConfig =require('./common/routeConfig').default;// eslint-disable-linerenderApp(<Root store={store} routeConfig={nextRouteConfig}/>);});
module.hot.accept('./Root',()=>{const nextRoot =require('./Root').default;// eslint-disable-linerenderApp(<Root store={store} routeConfig={routeConfig}/>);});}
Root.js
/* This is the Root component mainly initializes Redux and React Router. */import React from'react';import PropTypes from'prop-types';import{ Provider }from'react-redux';import{ Switch, Route }from'react-router-dom';import{ ConnectedRouter }from'react-router-redux';import history from'./common/history';functionrenderRouteConfigV3(routes, contextPath){// Resolve route config object in React Router v3.const children =[];// children component listconstrenderRoute=(item, routeContextPath)=>{let newContextPath;if(/^\//.test(item.path)){
newContextPath = item.path;}else{
newContextPath =`${routeContextPath}/${item.path}`;}
newContextPath = newContextPath.replace(/\/+/g,'/');if(item.component && item.childRoutes){const childRoutes =renderRouteConfigV3(item.childRoutes, newContextPath);
children.push(<Route
key={newContextPath}
render={props =><item.component {...props}>{childRoutes}</item.component>}
path={newContextPath}/>);}elseif(item.component){
children.push(<Route key={newContextPath} component={item.component} path={newContextPath} exact />);}elseif(item.childRoutes){
item.childRoutes.forEach(r =>renderRoute(r, newContextPath));}};
routes.forEach(item =>renderRoute(item, contextPath));debugger// Use Switch so that only the first matched route is rendered.return<Switch>{children}</Switch>;}exportdefaultclassRootextendsReact.Component{static propTypes ={
store: PropTypes.object.isRequired,
routeConfig: PropTypes.array.isRequired,};render(){const children =renderRouteConfigV3(this.props.routeConfig,'/');debuggerreturn(<Provider store={this.props.store}><ConnectedRouter history={history}>{children}</ConnectedRouter></Provider>);}}
component.jsx
import React,{ Component }from'react';import PropTypes from'prop-types';import{ Link }from'react-router-dom';import{ bindActionCreators }from'redux';import{ connect }from'react-redux';import*as actions from'./redux/actions';exportclassSidePanelextendsComponent{static propTypes ={
examples: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,};render(){return(<div className="examples-side-panel"><ul><li><Link to="/examples">Welcome</Link></li><li><Link to="/examples/counter">Counter Demo</Link></li><li><Link to="/examples/reddit">Reddit API Demo</Link></li><li><Link to="/">Back to start page</Link></li></ul><div className="memo">
This is a Rekit feature that contains some examples for you to quick learn how Rekit works. To remove it just
delete the feature.</div></div>);}}/* istanbul ignore next */functionmapStateToProps(state){return{
examples: state.examples,};}/* istanbul ignore next */functionmapDispatchToProps(dispatch){return{
actions:bindActionCreators({...actions }, dispatch),};}exportdefaultconnect(mapStateToProps, mapDispatchToProps)(SidePanel);
// Initial state is the place you define all initial values for the Redux store of the feature.// In the 'standard' way, initialState is defined in reducers: http://redux.js.org/docs/basics/Reducers.html// But when application grows, there will be multiple reducers files, it's not intuitive what data is managed by the whole store.// So Rekit extracts the initial state definition into a separate module so that you can have// a quick view about what data is used for the feature, at any time.// NOTE: initialState constant is necessary so that Rekit could auto add initial state when creating async actions.const initialState ={
count:0,
redditList:[],
fetchRedditListPending:false,
fetchRedditListError:null,};exportdefault initialState;
component/redux/reducer.js
// This is the root reducer of the feature. It is used for:// 1. Load reducers from each action in the feature and process them one by one.// Note that this part of code is mainly maintained by Rekit, you usually don't need to edit them.// 2. Write cross-topic reducers. If a reducer is not bound to some specific action.// Then it could be written here.// Learn more from the introduction of this approach:// https://medium.com/@nate_wang/a-new-approach-for-managing-redux-actions-91c26ce8b5da.import initialState from'./initialState';import{ reducer as counterPlusOneReducer }from'./counterPlusOne';import{ reducer as counterMinusOneReducer }from'./counterMinusOne';import{ reducer as counterResetReducer }from'./counterReset';import{ reducer as fetchRedditListReducer }from'./fetchRedditList';const reducers =[
counterPlusOneReducer,
counterMinusOneReducer,
counterResetReducer,
fetchRedditListReducer,];exportdefaultfunctionreducer(state = initialState, action){let newState;switch(action.type){// Handle cross-topic actions heredefault:
newState = state;break;}return reducers.reduce((s, r)=>r(s, action), newState);}
component/redux/xxxService.js
import axios from'axios';import{EXAMPLES_FETCH_REDDIT_LIST_BEGIN,EXAMPLES_FETCH_REDDIT_LIST_SUCCESS,EXAMPLES_FETCH_REDDIT_LIST_FAILURE,EXAMPLES_FETCH_REDDIT_LIST_DISMISS_ERROR,}from'./constants';// Rekit uses redux-thunk for async actions by default: https://github.com/gaearon/redux-thunk// If you prefer redux-saga, you can use rekit-plugin-redux-saga: https://github.com/supnate/rekit-plugin-redux-sagaexportfunctionfetchRedditList(args ={}){return dispatch =>{// optionally you can have getState as the second argumentdispatch({
type:EXAMPLES_FETCH_REDDIT_LIST_BEGIN,});// Return a promise so that you could control UI flow without states in the store.// For example: after submit a form, you need to redirect the page to another when succeeds or show some errors message if fails.// It's hard to use state to manage it, but returning a promise allows you to easily achieve it.// e.g.: handleSubmit() { this.props.actions.submitForm(data).then(()=> {}).catch(() => {}); }const promise =newPromise((resolve, reject)=>{// doRequest is a placeholder Promise. You should replace it with your own logic.// See the real-word example at: https://github.com/supnate/rekit/blob/master/src/features/home/redux/fetchRedditReactjsList.js// args.error here is only for test coverage purpose.const doRequest = axios.get('http://www.reddit.com/r/reactjs.json');
doRequest.then(
res =>{dispatch({
type:EXAMPLES_FETCH_REDDIT_LIST_SUCCESS,
data: res.data,});resolve(res);},// Use rejectHandler as the second argument so that render errors won't be caught.
err =>{dispatch({
type:EXAMPLES_FETCH_REDDIT_LIST_FAILURE,
data:{ error: err },});reject(err);});});return promise;};}// Async action saves request error by default, this method is used to dismiss the error info.// If you don't want errors to be saved in Redux store, just ignore this method.exportfunctiondismissFetchRedditListError(){return{
type:EXAMPLES_FETCH_REDDIT_LIST_DISMISS_ERROR,};}exportfunctionreducer(state, action){switch(action.type){caseEXAMPLES_FETCH_REDDIT_LIST_BEGIN:// Just after a request is sentreturn{...state,
fetchRedditListPending:true,
fetchRedditListError:null,};caseEXAMPLES_FETCH_REDDIT_LIST_SUCCESS:// The request is successreturn{...state,
redditList: action.data.data.children,
fetchRedditListPending:false,
fetchRedditListError:null,};caseEXAMPLES_FETCH_REDDIT_LIST_FAILURE:// The request is failedreturn{...state,
fetchRedditListPending:false,
fetchRedditListError: action.data.error,};caseEXAMPLES_FETCH_REDDIT_LIST_DISMISS_ERROR:// Dismiss the request failure errorreturn{...state,
fetchRedditListError:null,};default:return state;}}
index -> route.js
// This is the JSON way to define React Router rules in a Rekit app.// Learn more from: http://rekit.js.org/docs/routing.htmlimport{
WelcomePage,
CounterPage,
RedditListPage,
Layout,}from'./';exportdefault{
path:'examples',
name:'Examples',
component: Layout,
childRoutes:[{ path:'', name:'Welcome page', component: WelcomePage },{ path:'counter', name:'Counter page', component: CounterPage },{ path:'reddit', name:'Reddit list page', component: RedditListPage },],};