Skip to content

Commit 0f3dbb1

Browse files
committed
Projects reducer consumes USER_LOGGED_OUT
When the user logs out, we want to clear all projects out of the list except for the current project. This requires the projects reducer to know what the current project is. There are several ways to solve this problem without giving the projects reducer access to the rest of the store: 1. Have the current project key passed in the action payload by the point of dispatch (e.g. from the Workspace component) 2. Use a thunk action creator that can introspect the current state and then add the current project key to a dispatched action payload 3. Duplicate the information in the `currentProject` subtree, also marking the object in `projects` as `current` 4. Don’t bother trimming the projects store–just enforce a rule that only the current project is visible using a selector However, each of these approaches has significant disadvantages: 1. The fact that a reducer needs to know about the current project when consuming `USER_LOGGED_OUT` is an implementation detail of the store; the component that initially dispatches the action should not need to know this 2. Thunk action creators are considered harmful and are being removed from our code 3. It’s a very good idea to keep the Redux store fully normalized. 4. This approach would lead to an incoherent store state, and we’d have roughly the same problem when the user logs in. Contra the author of [this highly upvoted GitHub issue comment](reduxjs/redux#601 (comment)), I don’t think it’s an antipattern for reducers to have access to the entire store. In fact, this is the great strength of Redux—we’re able to model all state transitions based on a universally-agreed-upon answer to the question “what is the current state of the world?” The `combineReducers` approach to isolating the reducer logic for different parts of the subtree is a very useful tool for organizing code; it’s not a mandate to only organize code that way. Further, options 1 and 2 above feel a bit ridiculous because, fundamentally, **reducers do have access to the entire state**. Why would we jump through hoops just to give the reducer information it already has access to? So: establish a pattern that reducer modules may export a named `reduceRoot` function, which takes the entire state and performs reductions on it. The top-level root reducer will import this function and apply it to the state *after running the isolated reducers* using the `reduce-reducers` module.
1 parent 8b3c3b3 commit 0f3dbb1

File tree

6 files changed

+49
-7
lines changed

6 files changed

+49
-7
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"react-dom": "^15.4.1",
8585
"react-ga": "^2.1.2",
8686
"react-redux": "^5.0.3",
87+
"reduce-reducers": "^0.1.2",
8788
"redux": "^3.6.0",
8889
"redux-actions": "^1.2.1",
8990
"redux-immutable": "^3.0.11",

src/actions/user.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const userAuthenticated = createAction(
88

99
const resetWorkspace = createAction('RESET_WORKSPACE', identity);
1010

11-
const userLoggedOut = createAction('USER_LOGGED_OUT');
11+
export const userLoggedOut = createAction('USER_LOGGED_OUT');
1212

1313
export function logOut() {
1414
return (dispatch, getState) => {

src/reducers/index.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import {combineReducers} from 'redux-immutable';
2+
import reduceReducers from 'reduce-reducers';
23
import user from './user';
3-
import projects from './projects';
4+
import projects, {reduceRoot as reduceRootForProjects} from './projects';
45
import currentProject from './currentProject';
56
import errors from './errors';
67
import runtimeErrors from './runtimeErrors';
78
import ui from './ui';
89
import clients from './clients';
910

10-
const reducers = combineReducers({
11+
const reduceRoot = combineReducers({
1112
user,
1213
projects,
1314
currentProject,
@@ -17,4 +18,7 @@ const reducers = combineReducers({
1718
clients,
1819
});
1920

20-
export default reducers;
21+
export default reduceReducers(
22+
reduceRoot,
23+
reduceRootForProjects,
24+
);

src/reducers/projects.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,29 @@ function importGist(state, projectKey, gistData) {
6464
);
6565
}
6666

67-
export default function projects(stateIn, action) {
67+
export function reduceRoot(stateIn, action) {
68+
return stateIn.update('projects', (projects) => {
69+
switch (action.type) {
70+
case 'USER_LOGGED_OUT':
71+
{
72+
const currentProjectKey =
73+
stateIn.getIn(['currentProject', 'projectKey']);
74+
75+
if (isNil(currentProjectKey)) {
76+
return new Immutable.Map();
77+
}
78+
79+
return new Immutable.Map().set(
80+
currentProjectKey,
81+
projects.get(currentProjectKey),
82+
);
83+
}
84+
}
85+
return projects;
86+
});
87+
}
88+
89+
export default function reduceProjects(stateIn, action) {
6890
let state;
6991

7092
if (stateIn === undefined) {

test/unit/reducers/projects.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ import Immutable from 'immutable';
77
import reducerTest from '../../helpers/reducerTest';
88
import {projects as states} from '../../helpers/referenceStates';
99
import {gistData, project} from '../../helpers/factory';
10-
import reducer from '../../../src/reducers/projects';
10+
import reducer, {
11+
reduceRoot as rootReducer,
12+
} from '../../../src/reducers/projects';
1113
import {
1214
changeCurrentProject,
1315
gistImported,
1416
projectCreated,
1517
projectLoaded,
1618
projectSourceEdited,
1719
} from '../../../src/actions/projects';
20+
import {userLoggedOut} from '../../../src/actions/user';
1821

1922
const now = Date.now();
2023
const projectKey = '12345';
@@ -136,6 +139,18 @@ tap(project(), projectIn =>
136139
)),
137140
);
138141

142+
tap(initProjects({1: true, 2: true}), projects =>
143+
test('userLoggedOut', reducerTest(
144+
rootReducer,
145+
Immutable.fromJS({currentProject: {projectKey: '1'}, projects}),
146+
userLoggedOut,
147+
Immutable.fromJS({
148+
currentProject: {projectKey: '1'},
149+
projects: projects.take(1),
150+
}),
151+
)),
152+
);
153+
139154
function initProjects(map = {}) {
140155
return reduce(map, (projectsIn, modified, key) => {
141156
const projects = reducer(projectsIn, projectCreated(key));

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6515,7 +6515,7 @@ reduce-function-call@^1.0.1:
65156515
dependencies:
65166516
balanced-match "^0.4.2"
65176517

6518-
reduce-reducers@^0.1.0:
6518+
reduce-reducers@^0.1.0, reduce-reducers@^0.1.2:
65196519
version "0.1.2"
65206520
resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.1.2.tgz#fa1b4718bc5292a71ddd1e5d839c9bea9770f14b"
65216521

0 commit comments

Comments
 (0)