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

Commit 46c3228

Browse files
committed
Merge pull request #40 from jlongster/experimental-sync-logic
Experimental sync logic
2 parents abe1767 + c72dd57 commit 46c3228

File tree

4 files changed

+217
-22
lines changed

4 files changed

+217
-22
lines changed

.babelrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["es2015"]
3+
}

package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
],
1313
"license": "MIT",
1414
"scripts": {
15-
"build": "mkdir -p lib && babel ./src/index.js --plugins transform-object-assign --presets babel-preset-es2015 --out-file ./lib/index.js",
15+
"build": "mkdir -p lib && babel ./src/index.js --plugins transform-object-assign --out-file ./lib/index.js",
16+
"test": "mocha --compilers js:babel-core/register --recursive",
1617
"prepublish": "npm run build"
1718
},
1819
"tags": [
@@ -26,7 +27,12 @@
2627
],
2728
"devDependencies": {
2829
"babel-cli": "^6.1.2",
30+
"babel-core": "^6.2.1",
2931
"babel-plugin-transform-object-assign": "^6.0.14",
30-
"babel-preset-es2015": "^6.1.2"
32+
"babel-preset-es2015": "^6.1.2",
33+
"expect": "^1.13.0",
34+
"history": "^1.13.1",
35+
"mocha": "^2.3.4",
36+
"redux": "^3.0.4"
3137
}
3238
}

src/index.js

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,28 @@ const SELECT_STATE = state => state.routing;
66

77
// Action creator
88

9-
function updatePath(path, noRouterUpdate) {
9+
function updatePath(path, avoidRouterUpdate) {
1010
return {
1111
type: UPDATE_PATH,
1212
path: path,
13-
noRouterUpdate: noRouterUpdate
13+
avoidRouterUpdate: !!avoidRouterUpdate
1414
}
1515
}
1616

1717
// Reducer
1818

19-
const initialState = typeof window === 'undefined' ? {} : {
20-
path: locationToString(window.location)
19+
const initialState = {
20+
changeId: 1,
21+
path: (typeof window !== 'undefined') ?
22+
locationToString(window.location) :
23+
'/'
2124
};
2225

2326
function update(state=initialState, action) {
2427
if(action.type === UPDATE_PATH) {
2528
return Object.assign({}, state, {
2629
path: action.path,
27-
noRouterUpdate: action.noRouterUpdate
30+
changeId: state.changeId + (action.avoidRouterUpdate ? 0 : 1)
2831
});
2932
}
3033
return state;
@@ -37,8 +40,8 @@ function locationToString(location) {
3740
}
3841

3942
function syncReduxAndRouter(history, store, selectRouterState = SELECT_STATE) {
40-
let lastRoute;
4143
const getRouterState = () => selectRouterState(store.getState());
44+
let lastChangeId = 0;
4245

4346
if(!getRouterState()) {
4447
throw new Error(
@@ -48,26 +51,22 @@ function syncReduxAndRouter(history, store, selectRouterState = SELECT_STATE) {
4851
}
4952

5053
const unsubscribeHistory = history.listen(location => {
51-
const newLocation = locationToString(location);
52-
// Avoid dispatching an action if the store is already up-to-date,
53-
// even if `history` wouldn't do anything if the location is the same
54-
if(getRouterState().path !== newLocation) {
55-
lastRoute = newLocation;
56-
store.dispatch(updatePath(newLocation));
54+
const routePath = locationToString(location);
55+
56+
// Avoid dispatching an action if the store is already up-to-date
57+
if(getRouterState().path !== routePath) {
58+
store.dispatch(updatePath(routePath, { avoidRouterUpdate: true }));
5759
}
5860
});
5961

6062
const unsubscribeStore = store.subscribe(() => {
6163
const routing = getRouterState();
6264

63-
// Don't update the router if the routing state hasn't changed or the new routing path
64-
// is already the current location.
65-
// The `noRouterUpdate` flag can be set to avoid updating altogether,
66-
// which is useful for things like loading snapshots or very special
67-
// edge cases.
68-
if(lastRoute !== routing.path && routing.path !== locationToString(window.location) &&
69-
!routing.noRouterUpdate) {
70-
lastRoute = routing.path;
65+
// Only update the router once per `updatePath` call. This is
66+
// indicated by the `changeId` state; when that number changes, we
67+
// should call `pushState`.
68+
if(lastChangeId !== routing.changeId) {
69+
lastChangeId = routing.changeId;
7170
history.pushState(null, routing.path);
7271
}
7372
});

test/index.js

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
const expect = require('expect');
2+
const { updatePath, UPDATE_PATH, routeReducer, syncReduxAndRouter } = require('../src/index');
3+
const { createStore, combineReducers } = require('redux');
4+
const { createMemoryHistory: createHistory } = require('history');
5+
6+
function createSyncedHistoryAndStore() {
7+
const store = createStore(combineReducers({
8+
routing: routeReducer
9+
}));
10+
const history = createHistory();
11+
syncReduxAndRouter(history, store);
12+
return { history, store };
13+
}
14+
15+
describe('updatePath', () => {
16+
it('creates actions', () => {
17+
expect(updatePath('/foo')).toEqual({
18+
type: UPDATE_PATH,
19+
path: '/foo',
20+
avoidRouterUpdate: false
21+
});
22+
23+
expect(updatePath('/foo', { avoidRouterUpdate: true })).toEqual({
24+
type: UPDATE_PATH,
25+
path: '/foo',
26+
avoidRouterUpdate: true
27+
});
28+
});
29+
});
30+
31+
describe('routeReducer', () => {
32+
const state = {
33+
path: '/foo',
34+
changeId: 1
35+
};
36+
37+
it('updates the path', () => {
38+
expect(routeReducer(state, {
39+
type: UPDATE_PATH,
40+
path: '/bar'
41+
})).toEqual({
42+
path: '/bar',
43+
changeId: 2
44+
});
45+
});
46+
47+
it('respects `avoidRouterUpdate` flag', () => {
48+
expect(routeReducer(state, {
49+
type: UPDATE_PATH,
50+
path: '/bar',
51+
avoidRouterUpdate: true
52+
})).toEqual({
53+
path: '/bar',
54+
changeId: 1
55+
});
56+
});
57+
});
58+
59+
describe('syncReduxAndRouter', () => {
60+
it('syncs router -> redux', () => {
61+
const { history, store } = createSyncedHistoryAndStore();
62+
expect(store.getState().routing.path).toEqual('/');
63+
64+
history.pushState(null, '/foo');
65+
expect(store.getState().routing.path).toEqual('/foo');
66+
67+
history.pushState(null, '/bar');
68+
expect(store.getState().routing.path).toEqual('/bar');
69+
70+
history.pushState(null, '/bar?query=1');
71+
expect(store.getState().routing.path).toEqual('/bar?query=1');
72+
73+
history.pushState(null, '/bar?query=1#hash=2');
74+
expect(store.getState().routing.path).toEqual('/bar?query=1#hash=2');
75+
});
76+
77+
it('syncs redux -> router', () => {
78+
const { history, store } = createSyncedHistoryAndStore();
79+
expect(store.getState().routing).toEqual({
80+
path: '/',
81+
changeId: 1
82+
});
83+
84+
store.dispatch(updatePath('/foo'));
85+
expect(store.getState().routing).toEqual({
86+
path: '/foo',
87+
changeId: 2
88+
});
89+
90+
store.dispatch(updatePath('/bar'));
91+
expect(store.getState().routing).toEqual({
92+
path: '/bar',
93+
changeId: 3
94+
});
95+
96+
store.dispatch(updatePath('/bar?query=1'));
97+
expect(store.getState().routing).toEqual({
98+
path: '/bar?query=1',
99+
changeId: 4
100+
});
101+
102+
store.dispatch(updatePath('/bar?query=1#hash=2'));
103+
expect(store.getState().routing).toEqual({
104+
path: '/bar?query=1#hash=2',
105+
changeId: 5
106+
});
107+
});
108+
109+
it('updates the router even if path is the same', () => {
110+
const { history, store } = createSyncedHistoryAndStore();
111+
expect(store.getState().routing).toEqual({
112+
path: '/',
113+
changeId: 1
114+
});
115+
116+
store.dispatch(updatePath('/foo'));
117+
expect(store.getState().routing).toEqual({
118+
path: '/foo',
119+
changeId: 2
120+
});
121+
122+
store.dispatch(updatePath('/foo'));
123+
expect(store.getState().routing).toEqual({
124+
path: '/foo',
125+
changeId: 3
126+
});
127+
});
128+
129+
it('does not update the router for other state changes', () => {
130+
const { history, store } = createSyncedHistoryAndStore();
131+
store.dispatch({
132+
type: 'RANDOM_ACTION',
133+
value: 5
134+
});
135+
136+
expect(store.getState().routing).toEqual({
137+
path: '/',
138+
changeId: 1
139+
});
140+
});
141+
142+
it('only updates the router once when dispatching from `listenBefore`', () => {
143+
const { history, store } = createSyncedHistoryAndStore();
144+
expect(store.getState().routing).toEqual({
145+
path: '/',
146+
changeId: 1
147+
});
148+
149+
history.listenBefore(location => {
150+
expect(location.pathname).toEqual('/foo');
151+
store.dispatch({
152+
type: 'RANDOM_ACTION',
153+
value: 5
154+
});
155+
});
156+
157+
store.dispatch(updatePath('/foo'));
158+
expect(store.getState().routing).toEqual({
159+
path: '/foo',
160+
changeId: 2
161+
});
162+
});
163+
164+
it('allows updating the route from within `listenBefore`', () => {
165+
const { history, store } = createSyncedHistoryAndStore();
166+
expect(store.getState().routing).toEqual({
167+
path: '/',
168+
changeId: 1
169+
});
170+
171+
history.listenBefore(location => {
172+
if(location.pathname === '/foo') {
173+
expect(store.getState().routing).toEqual({
174+
path: '/foo',
175+
changeId: 2
176+
});
177+
store.dispatch(updatePath('/bar'));
178+
}
179+
});
180+
181+
store.dispatch(updatePath('/foo'));
182+
expect(store.getState().routing).toEqual({
183+
path: '/bar',
184+
changeId: 3
185+
});
186+
})
187+
});

0 commit comments

Comments
 (0)