Skip to content
This repository has been archived by the owner on Oct 26, 2018. It is now read-only.

Commit

Permalink
Tweak the API to handle initial state correctly
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon committed Feb 3, 2016
1 parent b6a9c1b commit 151154f
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 76 deletions.
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,18 @@ const reducer = combineReducers(Object.assign({}, reducers, {
routing: routeReducer
}))

// Sync dispatched route actions to the history
// specify the history to listen to
const reduxRouterMiddleware = syncHistory(browserHistory)
const createStoreWithMiddleware = applyMiddleware(reduxRouterMiddleware)(createStore)

const store = createStoreWithMiddleware(reducer)
const store = createStore(
reducer,
applyMiddleware(reduxRouterMiddleware)
)

// Required for replaying actions from devtools to work
reduxRouterMiddleware.listenForReplays(store)
// begin syncing
reduxRouterMiddleware.syncWith(store, {
urlToState: true, // route changes will appear in state
stateToUrl: false // set to true for time travel in DevTools
})

ReactDOM.render(
<Provider store={store}>
Expand Down Expand Up @@ -140,7 +144,7 @@ Examples from the community:

_Have an example to add? Send us a PR!_

### API
### API (TODO)

#### `syncHistory(history: History) => ReduxMiddleware`

Expand Down
18 changes: 12 additions & 6 deletions examples/basic/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@ const DevTools = createDevTools(
</DockMonitor>
)

const finalCreateStore = compose(
applyMiddleware(middleware),
DevTools.instrument()
)(createStore)
const store = finalCreateStore(reducer)
middleware.listenForReplays(store)
const store = createStore(
reducer,
compose(
applyMiddleware(middleware),
DevTools.instrument()
)
)

middleware.syncWith(store, {
urlToState: true,
stateToUrl: true
})

ReactDOM.render(
<Provider store={store}>
Expand Down
4 changes: 2 additions & 2 deletions examples/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"react-dom": "^0.14.2",
"react-redux": "^4.0.0",
"react-router": "^1.0.0",
"redux": "^3.0.4",
"react-router-redux": "^2.1.0"
"react-router-redux": "^2.1.0",
"redux": "^3.2.1"
},
"devDependencies": {
"babel-core": "^6.1.21",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"karma-webpack": "^1.7.0",
"mocha": "^2.3.4",
"react": "^0.14.3",
"redux": "^3.0.4",
"redux": "^3.2.1",
"redux-devtools": "^3.0.0",
"redux-devtools-dock-monitor": "^1.0.1",
"redux-devtools-log-monitor": "^1.0.1",
Expand Down
103 changes: 61 additions & 42 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,7 @@ export function syncHistory(history) {

history.listen(location => { initialState.location = location })()

function middleware(store) {
unsubscribeHistory = history.listen(location => {
currentKey = location.key
if (syncing) {
// Don't dispatch a new action if we're replaying location.
return
}

store.dispatch(updateLocation(location))
})

connected = true

function middleware() {
return next => action => {
if (action.type !== TRANSITION || !connected) {
return next(action)
Expand All @@ -72,41 +60,72 @@ export function syncHistory(history) {
}
}

middleware.listenForReplays =
(store, selectLocationState = SELECT_LOCATION) => {
const getLocationState = () => selectLocationState(store.getState())
const initialLocation = getLocationState()

unsubscribeStore = store.subscribe(() => {
const location = getLocationState()
middleware.syncWith =
(store, {
urlToState = false,
stateToUrl = false,
selectLocationState = SELECT_LOCATION
} = {}) => {
if (!urlToState && !stateToUrl) {
throw new Error(
'At least one of "urlToState" and "stateToUrl" options must be true.'
)
}

// If we're resetting to the beginning, use the saved initial value. We
// need to dispatch a new action at this point to populate the store
// appropriately.
if (location.key === initialLocation.key) {
history.replace(initialLocation)
return
if (stateToUrl) {
const getLocationState = () => selectLocationState(store.getState())
const initialLocation = getLocationState()

const reconcileLocationWithState = () => {
const location = getLocationState()

// If we're resetting to the beginning, use the saved initial value. We
// need to dispatch a new action at this point to populate the store
// appropriately.
if (location.key === initialLocation.key) {
history.replace(initialLocation)
return
}

// Otherwise, if we need to update the history location, do so without
// dispatching a new action, as we're just bringing history in sync
// with the store.
if (location.key !== currentKey) {
syncing = true
history.transitionTo(location)
syncing = false
}
}

// Otherwise, if we need to update the history location, do so without
// dispatching a new action, as we're just bringing history in sync
// with the store.
if (location.key !== currentKey) {
syncing = true
history.transitionTo(location)
syncing = false
unsubscribeStore = store.subscribe(reconcileLocationWithState)
reconcileLocationWithState()
}

if (urlToState) {
unsubscribeHistory = history.listen(location => {
currentKey = location.key
if (syncing) {
// Don't dispatch a new action if we're replaying location.
return
}

store.dispatch(updateLocation(location))
})

connected = true
}

return () => {
if (stateToUrl) {
unsubscribeStore()
}
})
}

middleware.unsubscribe = () => {
unsubscribeHistory()
if (unsubscribeStore) {
unsubscribeStore()
if (urlToState) {
unsubscribeHistory()
connected = false
}
}
}

connected = false
}

return middleware
}
133 changes: 115 additions & 18 deletions test/createTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@ expect.extend({
}
})

function createSyncedHistoryAndStore(createHistory) {
function createSyncedHistoryAndStore(createHistory, syncOptions, initialState) {
const history = createHistory()
const middleware = syncHistory(history)
const { unsubscribe } = middleware

const createStoreWithMiddleware = applyMiddleware(middleware)(createStore)
const store = createStoreWithMiddleware(combineReducers({
const reducer = combineReducers({
routing: routeReducer
}))
})
const store = createStore(
reducer,
initialState,
applyMiddleware(middleware)
)
const unsubscribe = middleware.syncWith(store, syncOptions)

return { history, store, unsubscribe }
}
Expand Down Expand Up @@ -197,18 +200,17 @@ module.exports = function createTests(createHistory, name, reset = defaultReset)
history.push('/foo')

const middleware = syncHistory(history)
unsubscribe = middleware.unsubscribe

const finalCreateStore = compose(
store = createStore(combineReducers({
routing: routeReducer
}), compose(
applyMiddleware(middleware),
instrument()
)(createStore)
store = finalCreateStore(combineReducers({
routing: routeReducer
}))
))
devToolsStore = store.liftedStore

middleware.listenForReplays(store)
unsubscribe = middleware.syncWith(store, {
stateToUrl: true,
urlToState: true
})
})

afterEach(() => {
Expand Down Expand Up @@ -273,11 +275,103 @@ module.exports = function createTests(createHistory, name, reset = defaultReset)
})
})

describe('initialState', () => {
it('does not respect initialState when syncing url to state', () => {
let synced = createSyncedHistoryAndStore(createHistory, {
urlToState: true
}, {
routing: {
location: {
pathname: '/init',
search: '',
hash: '',
state: null,
action: 'PUSH',
key: 'abcde'
}
}
})

let history = synced.history
let unsubscribe = synced.unsubscribe

let currentPath
const historyUnsubscribe = history.listen(location => {
currentPath = location.pathname
})

expect(currentPath).toEqual('/')
historyUnsubscribe()
unsubscribe()
})

it('respects initialState when syncing state to url', () => {
let synced = createSyncedHistoryAndStore(createHistory, {
stateToUrl: true
}, {
routing: {
location: {
pathname: '/init',
search: '',
hash: '',
state: null,
action: 'PUSH',
key: 'abcde'
}
}
})

let history = synced.history
let unsubscribe = synced.unsubscribe

let currentPath
const historyUnsubscribe = history.listen(location => {
currentPath = location.pathname
})

expect(currentPath).toEqual('/init')
historyUnsubscribe()
unsubscribe()
})

it('respects initialState when syncing both ways', () => {
let synced = createSyncedHistoryAndStore(createHistory, {
stateToUrl: true,
urlToState: true
}, {
routing: {
location: {
pathname: '/init',
search: '',
hash: '',
state: null,
action: 'PUSH',
key: 'abcde'
}
}
})

let history = synced.history
let unsubscribe = synced.unsubscribe

let currentPath
const historyUnsubscribe = history.listen(location => {
currentPath = location.pathname
})

expect(currentPath).toEqual('/init')
historyUnsubscribe()
unsubscribe()
})
})

describe('syncReduxAndRouter', () => {
let history, store, unsubscribe

beforeEach(() => {
let synced = createSyncedHistoryAndStore(createHistory)
let synced = createSyncedHistoryAndStore(createHistory, {
urlToState: true
})
history = synced.history
store = synced.store
unsubscribe = synced.unsubscribe
Expand Down Expand Up @@ -545,7 +639,9 @@ module.exports = function createTests(createHistory, name, reset = defaultReset)
let history, store, unsubscribe

beforeEach(() => {
const synced = createSyncedHistoryAndStore(useQueries(createHistory))
const synced = createSyncedHistoryAndStore(useQueries(createHistory), {
urlToState: true
})
history = synced.history
store = synced.store
unsubscribe = synced.unsubscribe
Expand Down Expand Up @@ -583,7 +679,8 @@ module.exports = function createTests(createHistory, name, reset = defaultReset)

beforeEach(() => {
const synced = createSyncedHistoryAndStore(
() => useBasename(createHistory)({ basename: '/foobar' })
() => useBasename(createHistory)({ basename: '/foobar' }),
{ urlToState: true }
)
history = synced.history
store = synced.store
Expand Down

0 comments on commit 151154f

Please sign in to comment.