Skip to content

Commit 58ee31c

Browse files
feat(core): init store with sync location reducer
1 parent 5758b2c commit 58ee31c

File tree

8 files changed

+177
-17
lines changed

8 files changed

+177
-17
lines changed

src/containers/AppContainer.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
import React, { Component, PropTypes } from 'react'
2-
import { Router } from 'react-router'
2+
import { browserHistory, Router } from 'react-router'
33
import { Provider } from 'react-redux'
44

55
class AppContainer extends Component {
66
static propTypes = {
7-
history : PropTypes.object.isRequired,
8-
routes : PropTypes.object.isRequired,
9-
store : PropTypes.object.isRequired
7+
routes : PropTypes.object.isRequired,
8+
store : PropTypes.object.isRequired
9+
}
10+
11+
shouldComponentUpdate () {
12+
return false
1013
}
1114

1215
render () {
13-
const { history, routes, store } = this.props
16+
const { routes, store } = this.props
1417

1518
return (
1619
<Provider store={store}>
1720
<div style={{ height: '100%' }}>
18-
<Router history={history} children={routes} />
21+
<Router history={browserHistory} children={routes} />
1922
</div>
2023
</Provider>
2124
)

src/main.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import React from 'react'
22
import ReactDOM from 'react-dom'
3-
import { browserHistory } from 'react-router'
43
import createStore from './store/createStore'
54
import AppContainer from './containers/AppContainer'
65

76
// ========================================================
8-
// Store and History Instantiation
7+
// Store Instantiation
98
// ========================================================
109
const initialState = window.___INITIAL_STATE__
1110
const store = createStore(initialState)
@@ -19,11 +18,7 @@ let render = () => {
1918
const routes = require('./routes/index').default(store)
2019

2120
ReactDOM.render(
22-
<AppContainer
23-
store={store}
24-
history={browserHistory}
25-
routes={routes}
26-
/>,
21+
<AppContainer store={store} routes={routes} />,
2722
MOUNT_NODE
2823
)
2924
}
@@ -58,12 +53,12 @@ if (__DEV__) {
5853
}
5954

6055
// Setup hot module replacement
61-
module.hot.accept('./routes/index', () => {
62-
setTimeout(() => {
56+
module.hot.accept('./routes/index', () =>
57+
setImmediate(() => {
6358
ReactDOM.unmountComponentAtNode(MOUNT_NODE)
6459
render()
6560
})
66-
})
61+
)
6762
}
6863
}
6964

src/store/createStore.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { applyMiddleware, compose, createStore } from 'redux'
22
import thunk from 'redux-thunk'
3+
import { browserHistory } from 'react-router'
34
import makeRootReducer from './reducers'
5+
import { updateLocation } from './location'
46

57
export default (initialState = {}) => {
68
// ======================================================
@@ -32,6 +34,9 @@ export default (initialState = {}) => {
3234
)
3335
store.asyncReducers = {}
3436

37+
// To unsubscribe, invoke `store.unsubscribeHistory()` anytime
38+
store.unsubscribeHistory = browserHistory.listen(updateLocation(store))
39+
3540
if (module.hot) {
3641
module.hot.accept('./reducers', () => {
3742
const reducers = require('./reducers').default

src/store/location.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// ------------------------------------
2+
// Constants
3+
// ------------------------------------
4+
export const LOCATION_CHANGE = 'LOCATION_CHANGE'
5+
6+
// ------------------------------------
7+
// Actions
8+
// ------------------------------------
9+
export function locationChange (location = '/') {
10+
return {
11+
type : LOCATION_CHANGE,
12+
payload : location
13+
}
14+
}
15+
16+
// ------------------------------------
17+
// Specialized Action Creator
18+
// ------------------------------------
19+
export const updateLocation = ({ dispatch }) => {
20+
return (nextLocation) => dispatch(locationChange(nextLocation))
21+
}
22+
23+
// ------------------------------------
24+
// Reducer
25+
// ------------------------------------
26+
const initialState = null
27+
export default function locationReducer (state = initialState, action) {
28+
return action.type === LOCATION_CHANGE
29+
? action.payload
30+
: state
31+
}

src/store/reducers.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { combineReducers } from 'redux'
2+
import locationReducer from './location'
23

4+
// ========================================================
5+
// Render Setup
6+
// ========================================================
37
export const makeRootReducer = (asyncReducers) => {
48
return combineReducers({
9+
location: locationReducer,
510
...asyncReducers
611
})
712
}

tests/store/createStore.spec.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {
2+
default as createStore
3+
} from 'store/createStore'
4+
5+
describe('(Store) createStore', () => {
6+
let store
7+
8+
before(() => {
9+
store = createStore()
10+
})
11+
12+
it('should have an empty asyncReducers object', () => {
13+
expect(store.asyncReducers).to.be.an('object')
14+
expect(store.asyncReducers).to.be.empty
15+
})
16+
17+
describe('(Location)', () => {
18+
it('store should be initialized with Location state', () => {
19+
const location = {
20+
pathname : '/echo'
21+
}
22+
store.dispatch({
23+
type : 'LOCATION_CHANGE',
24+
payload : location
25+
})
26+
const locationState = store.getState().location
27+
expect(store.getState().location).to.deep.equal(location)
28+
})
29+
})
30+
31+
})

tests/store/location.spec.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {
2+
LOCATION_CHANGE,
3+
locationChange,
4+
updateLocation,
5+
default as locationReducer
6+
} from 'store/location'
7+
8+
describe('(Internal Module) Location', () => {
9+
it('Should export a constant LOCATION_CHANGE.', () => {
10+
expect(LOCATION_CHANGE).to.equal('LOCATION_CHANGE')
11+
})
12+
13+
describe('(Reducer)', () => {
14+
it('Should be a function.', () => {
15+
expect(locationReducer).to.be.a('function')
16+
})
17+
18+
it('Should initialize with a state of null.', () => {
19+
expect(locationReducer(undefined, {})).to.equal(null)
20+
})
21+
22+
it('Should return the previous state if an action was not matched.', () => {
23+
let state = locationReducer(undefined, {})
24+
expect(state).to.equal(null)
25+
state = locationReducer(state, { type: '@@@@@@@' })
26+
expect(state).to.equal(null)
27+
28+
const locationState = { pathname: '/yup' }
29+
state = locationReducer(state, locationChange(locationState))
30+
expect(state).to.equal(locationState)
31+
state = locationReducer(state, { type: '@@@@@@@' })
32+
expect(state).to.equal(locationState)
33+
})
34+
})
35+
36+
describe('(Action Creator) locationChange', () => {
37+
it('Should be exported as a function.', () => {
38+
expect(locationChange).to.be.a('function')
39+
})
40+
41+
it('Should return an action with type "LOCATION_CHANGE".', () => {
42+
expect(locationChange()).to.have.property('type', LOCATION_CHANGE)
43+
})
44+
45+
it('Should assign the first argument to the "payload" property.', () => {
46+
const locationState = { pathname: '/yup' }
47+
expect(locationChange(locationState)).to.have.property('payload', locationState)
48+
})
49+
50+
it('Should default the "payload" property to "/" if not provided.', () => {
51+
expect(locationChange()).to.have.property('payload', '/')
52+
})
53+
})
54+
55+
describe('(Specialized Action Creator) updateLocation', () => {
56+
let _globalState
57+
let _dispatchSpy
58+
let _getStateSpy
59+
60+
beforeEach(() => {
61+
_globalState = {
62+
location : locationReducer(undefined, {})
63+
}
64+
_dispatchSpy = sinon.spy((action) => {
65+
_globalState = {
66+
..._globalState,
67+
location : locationReducer(_globalState.location, action)
68+
}
69+
})
70+
_getStateSpy = sinon.spy(() => {
71+
return _globalState
72+
})
73+
74+
})
75+
76+
it('Should be exported as a function.', () => {
77+
expect(updateLocation).to.be.a('function')
78+
})
79+
80+
it('Should return a function (is a thunk).', () => {
81+
expect(updateLocation({ dispatch: _dispatchSpy })).to.be.a('function')
82+
})
83+
84+
it('Should call dispatch exactly once.', () => {
85+
const update = updateLocation({ dispatch: _dispatchSpy })('/')
86+
expect(_dispatchSpy.should.have.been.calledOnce)
87+
})
88+
})
89+
90+
})

tests/test-bundler.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,6 @@ const testsToRun = testsContext.keys().filter(inManifest)
3232

3333
// require all `src/**/*.js` except for `main.js` (for isparta coverage reporting)
3434
if (__COVERAGE__) {
35-
const componentsContext = require.context('../src/', true, /^((?!main).)*\.js$/)
35+
const componentsContext = require.context('../src/', true, /^((?!main|reducers).)*\.js$/)
3636
componentsContext.keys().forEach(componentsContext)
3737
}

0 commit comments

Comments
 (0)