Skip to content

Commit

Permalink
Projects reducer consumes USER_LOGGED_OUT
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
outoftime committed Apr 3, 2017
1 parent 8b3c3b3 commit 90e1e4d
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 17 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -84,6 +84,7 @@
"react-dom": "^15.4.1",
"react-ga": "^2.1.2",
"react-redux": "^5.0.3",
"reduce-reducers": "^0.1.2",
"redux": "^3.6.0",
"redux-actions": "^1.2.1",
"redux-immutable": "^3.0.11",
Expand Down
2 changes: 1 addition & 1 deletion src/actions/user.js
Expand Up @@ -8,7 +8,7 @@ export const userAuthenticated = createAction(

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

const userLoggedOut = createAction('USER_LOGGED_OUT');
export const userLoggedOut = createAction('USER_LOGGED_OUT');

export function logOut() {
return (dispatch, getState) => {
Expand Down
10 changes: 7 additions & 3 deletions src/reducers/index.js
@@ -1,13 +1,14 @@
import {combineReducers} from 'redux-immutable';
import reduceReducers from 'reduce-reducers';
import user from './user';
import projects from './projects';
import projects, {reduceRoot as reduceRootForProjects} from './projects';
import currentProject from './currentProject';
import errors from './errors';
import runtimeErrors from './runtimeErrors';
import ui from './ui';
import clients from './clients';

const reducers = combineReducers({
const reduceRoot = combineReducers({
user,
projects,
currentProject,
Expand All @@ -17,4 +18,7 @@ const reducers = combineReducers({
clients,
});

export default reducers;
export default reduceReducers(
reduceRoot,
reduceRootForProjects,
);
34 changes: 23 additions & 11 deletions src/reducers/projects.js
Expand Up @@ -64,7 +64,29 @@ function importGist(state, projectKey, gistData) {
);
}

export default function projects(stateIn, action) {
export function reduceRoot(stateIn, action) {
return stateIn.update('projects', (projects) => {
switch (action.type) {
case 'USER_LOGGED_OUT':
{
const currentProjectKey =
stateIn.getIn(['currentProject', 'projectKey']);

if (isNil(currentProjectKey)) {
return new Immutable.Map();
}

return new Immutable.Map().set(
currentProjectKey,
projects.get(currentProjectKey),
);
}
}
return projects;
});
}

export default function reduceProjects(stateIn, action) {
let state;

if (stateIn === undefined) {
Expand Down Expand Up @@ -95,16 +117,6 @@ export default function projects(stateIn, action) {
case 'CHANGE_CURRENT_PROJECT':
return removePristineExcept(state, action.payload.projectKey);

case 'RESET_WORKSPACE':
if (isNil(action.payload.currentProjectKey)) {
return emptyMap;
}

return new Immutable.Map().set(
action.payload.currentProjectKey,
state.get(action.payload.currentProjectKey),
);

case 'GIST_IMPORTED':
return importGist(
state,
Expand Down
17 changes: 16 additions & 1 deletion test/unit/reducers/projects.js
Expand Up @@ -7,14 +7,17 @@ import Immutable from 'immutable';
import reducerTest from '../../helpers/reducerTest';
import {projects as states} from '../../helpers/referenceStates';
import {gistData, project} from '../../helpers/factory';
import reducer from '../../../src/reducers/projects';
import reducer, {
reduceRoot as rootReducer,
} from '../../../src/reducers/projects';
import {
changeCurrentProject,
gistImported,
projectCreated,
projectLoaded,
projectSourceEdited,
} from '../../../src/actions/projects';
import {userLoggedOut} from '../../../src/actions/user';

const now = Date.now();
const projectKey = '12345';
Expand Down Expand Up @@ -136,6 +139,18 @@ tap(project(), projectIn =>
)),
);

tap(initProjects({1: true, 2: true}), projects =>
test('userLoggedOut', reducerTest(
rootReducer,
Immutable.fromJS({currentProject: {projectKey: '1'}, projects}),
userLoggedOut,
Immutable.fromJS({
currentProject: {projectKey: '1'},
projects: projects.take(1),
}),
)),
);

function initProjects(map = {}) {
return reduce(map, (projectsIn, modified, key) => {
const projects = reducer(projectsIn, projectCreated(key));
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Expand Up @@ -6515,7 +6515,7 @@ reduce-function-call@^1.0.1:
dependencies:
balanced-match "^0.4.2"

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

Expand Down

0 comments on commit 90e1e4d

Please sign in to comment.