Skip to content

Commit

Permalink
Support Immutable.js
Browse files Browse the repository at this point in the history
  • Loading branch information
supasate committed Feb 4, 2017
1 parent ad9e2fa commit b4ef0b8
Show file tree
Hide file tree
Showing 14 changed files with 320 additions and 132 deletions.
1 change: 1 addition & 0 deletions immutable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./lib/immutable')
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"posttest": "npm run lint"
},
"dependencies": {
"immutable": "^3.8.1",
"lodash.topath": "^4.5.2",
"react-router": "^4.0.0-beta.3"
},
"peerDependencies": {
Expand Down
174 changes: 89 additions & 85 deletions src/ConnectedRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,104 +3,108 @@ import { connect } from 'react-redux'
import { Router } from 'react-router'
import { onLocationChanged } from './actions'

/*
* ConnectedRouter listens to a history object passed from props.
* When history is changed, it dispatches action to redux store.
* Then, store will pass props to component to render.
* This creates uni-directional flow from history->store->router->components.
*/
const createConnectedRouter = (structure) => {
const { getIn, toJS } = structure
/*
* ConnectedRouter listens to a history object passed from props.
* When history is changed, it dispatches action to redux store.
* Then, store will pass props to component to render.
* This creates uni-directional flow from history->store->router->components.
*/

export class ConnectedRouter extends Component {
constructor(props, context) {
super(props)
class ConnectedRouter extends Component {
constructor(props, context) {
super(props)

this.inTimeTravelling = false
this.inTimeTravelling = false

// Subscribe to store changes
this.unsubscribe = context.store.subscribe(() => {
// Extract store's location
const {
pathname: pathnameInStore,
search: searchInStore,
hash: hashInStore,
} = context.store.getState().router.location

// Extract history's location
const {
pathname: pathnameInHistory,
search: searchInHistory,
hash: hashInHistory,
} = props.history.location

// If we do time travelling, the location in store is changed but location in history is not changed
if (pathnameInHistory !== pathnameInStore || searchInHistory !== searchInStore || hashInHistory !== hashInStore) {
this.inTimeTravelling = true
// Update history's location to match store's location
props.history.push({
// Subscribe to store changes
this.unsubscribe = context.store.subscribe(() => {
// Extract store's location
const {
pathname: pathnameInStore,
search: searchInStore,
hash: hashInStore,
})
}
})
} = toJS(getIn(context.store.getState(), 'router.location'))
// Extract history's location
const {
pathname: pathnameInHistory,
search: searchInHistory,
hash: hashInHistory,
} = props.history.location

// Listen to history changes
this.unlisten = props.history.listen((location, action) => {
// Dispatch onLocationChanged except when we're in time travelling
if (!this.inTimeTravelling) {
props.onLocationChanged(location, action)
} else {
this.inTimeTravelling = false
}
})
}
// If we do time travelling, the location in store is changed but location in history is not changed
if (pathnameInHistory !== pathnameInStore || searchInHistory !== searchInStore || hashInHistory !== hashInStore) {
this.inTimeTravelling = true
// Update history's location to match store's location
props.history.push({
pathname: pathnameInStore,
search: searchInStore,
hash: hashInStore,
})
}
})

componentWillUnmount() {
this.unlisten()
this.unsubscribe()
}
// Listen to history changes
this.unlisten = props.history.listen((location, action) => {
// Dispatch onLocationChanged except when we're in time travelling
if (!this.inTimeTravelling) {
props.onLocationChanged(location, action)
} else {
this.inTimeTravelling = false
}
})
}

componentWillUnmount() {
this.unlisten()
this.unsubscribe()
}

render() {
const { history, children } = this.props
render() {
const { history, children } = this.props

return (
<Router history={history}>
{ children }
</Router>
)
return (
<Router history={history}>
{ children }
</Router>
)
}
}
}

ConnectedRouter.contextTypes = {
store: PropTypes.shape({
getState: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
}).isRequired,
}
ConnectedRouter.contextTypes = {
store: PropTypes.shape({
getState: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
}).isRequired,
}

ConnectedRouter.propTypes = {
history: PropTypes.shape({
listen: PropTypes.func.isRequired,
location: PropTypes.object.isRequired,
push: PropTypes.func.isRequired,
}).isRequired,
location: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string,
]).isRequired,
action: PropTypes.string.isRequired,
basename: PropTypes.string,
children: PropTypes.oneOfType([ PropTypes.func, PropTypes.node ]),
onLocationChanged: PropTypes.func.isRequired,
}
ConnectedRouter.propTypes = {
history: PropTypes.shape({
listen: PropTypes.func.isRequired,
location: PropTypes.object.isRequired,
push: PropTypes.func.isRequired,
}).isRequired,
location: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string,
]).isRequired,
action: PropTypes.string.isRequired,
basename: PropTypes.string,
children: PropTypes.oneOfType([ PropTypes.func, PropTypes.node ]),
onLocationChanged: PropTypes.func.isRequired,
}

const mapStateToProps = state => ({
action: getIn(state, 'router.action'),
location: getIn(state, 'router.location'),
})

const mapStateToProps = state => ({
action: state.router.action,
location: state.router.location,
})
const mapDispatchToProps = dispatch => ({
onLocationChanged: (location, action) => dispatch(onLocationChanged(location, action))
})

const mapDispatchToProps = dispatch => ({
onLocationChanged: (location, action) => dispatch(onLocationChanged(location, action))
})
return connect(mapStateToProps, mapDispatchToProps)(ConnectedRouter)
}

export default connect(mapStateToProps, mapDispatchToProps)(ConnectedRouter)
export default createConnectedRouter
13 changes: 13 additions & 0 deletions src/createAll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as actions from './actions'
import createConnectedRouter from './ConnectedRouter'
import createConnectRouter from './reducer'
import routerMiddleware from './middleware'

const createAll = structure => ({
...actions,
ConnectedRouter: createConnectedRouter(structure),
connectRouter: createConnectRouter(structure),
routerMiddleware,
})

export default createAll
16 changes: 16 additions & 0 deletions src/immutable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import createAll from './createAll'
import immutableStructure from './structure/immutable'

export const {
LOCATION_CHANGE,
CALL_HISTORY_METHOD,
push,
replace,
go,
goBack,
goForward,
routerActions,
ConnectedRouter,
connectRouter,
routerMiddleware,
} = createAll(immutableStructure)
23 changes: 14 additions & 9 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
export { LOCATION_CHANGE } from './actions'
export connectRouter from './reducer'
import createAll from './createAll'
import plainStructure from './structure/plain'

export {
export const {
LOCATION_CHANGE,
CALL_HISTORY_METHOD,
push, replace, go, goBack, goForward,
routerActions
} from './actions'

export routerMiddleware from './middleware'
export ConnectedRouter from './ConnectedRouter'
push,
replace,
go,
goBack,
goForward,
routerActions,
ConnectedRouter,
connectRouter,
routerMiddleware,
} = createAll(plainStructure)
66 changes: 35 additions & 31 deletions src/reducer.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,46 @@
import { LOCATION_CHANGE } from './actions'

/**
* This reducer will update the state with the most recent location history
* has transitioned to.
*/
const routerReducer = (state, { type, payload } = {}) => {
if (type === LOCATION_CHANGE) {
return {
...state,
...payload,
const createConnectRouter = (structure) => {
const {
filterNotRouter,
fromJS,
getIn,
merge,
setIn,
} = structure
/**
* This reducer will update the state with the most recent location history
* has transitioned to.
*/
const routerReducer = (state, { type, payload } = {}) => {
if (type === LOCATION_CHANGE) {
return merge(state, payload)
}
}

return state
}

const connectRouter = (history) => {
const initialRouterState = {
location: history.location,
action: history.action,
return state
}
// Wrap a root reducer and return a new root reducer with router state
return (rootReducer) => (state, action) => {
let routerState = initialRouterState

// Extract router state
if (state) {
const { router, ...rest} = state
routerState = router || routerState
state = rest
}
const reducerResults = rootReducer(state, action)
const connectRouter = (history) => {
const initialRouterState = fromJS({
location: history.location,
action: history.action,
})
// Wrap a root reducer and return a new root reducer with router state
return (rootReducer) => (state, action) => {
let routerState = initialRouterState

// Extract router state
if (state) {
routerState = getIn(state, 'router') || routerState
state = filterNotRouter(state)
}
const reducerResults = rootReducer(state, action)

return {
...reducerResults,
router: routerReducer(routerState, action)
return setIn(reducerResults, 'router', routerReducer(routerState, action))
}
}

return connectRouter
}

export default connectRouter
export default createConnectRouter
10 changes: 10 additions & 0 deletions src/structure/immutable/getIn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Iterable } from 'immutable'
import toPath from 'lodash.topath'
import plainGetIn from '../plain/getIn'

const getIn = (state, field) =>
Iterable.isIterable(state)
? state.getIn(toPath(field))
: plainGetIn(state, field)

export default getIn
15 changes: 15 additions & 0 deletions src/structure/immutable/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Iterable, fromJS } from 'immutable'
import getIn from './getIn'
import setIn from './setIn'

const structure = {
filterNotRouter: (state) => state.filterNot((v, k) => k === 'router'),
fromJS: jsValue => fromJS(jsValue, (key, value) =>
Iterable.isIndexed(value) ? value.toList() : value.toMap()),
getIn,
merge: (state, payload) => state.merge(payload),
setIn,
toJS: value => Iterable.isIterable(value) ? value.toJS() : value,
}

export default structure
Loading

0 comments on commit b4ef0b8

Please sign in to comment.