Last active
July 5, 2016 16:22
-
-
Save nicolashery/158dbcf8314a1ddd5e27 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // We can mentally separate "core state" from "derived state". | |
| // Core state is where you can put normalized data (for relational data): | |
| // it is the "single source of truth". | |
| // Derived state is where you shape the core state into a representation that's | |
| // closer to what you need in your views and components. | |
| var coreState = { | |
| venuesById: { | |
| 'v1': {id: 'v1', name: 'Hipster Coffee House'}, | |
| 'v2': {id: 'v2', name: 'Veggies For Everyone'} | |
| }, | |
| listsById: { | |
| 'l1': {id: 'l1', name: 'My Favorite Venues', venues: ['v1']}, | |
| 'l2': {id: 'l2', name: 'My Other List', venues: ['v2']} | |
| }, | |
| sharedListIds: ['l1'] | |
| }; | |
| var derivedState = { | |
| listsWithVenues: [ | |
| { | |
| id: 'l1', | |
| name: 'My Favorite Venues', | |
| venues: [ | |
| {id: 'v1', name: 'Hipster Coffee House'}, | |
| {id: 'v2', name: 'Veggies For Everyone'} | |
| ] | |
| }, | |
| { | |
| id: 'l2', | |
| name: 'My Other List', | |
| venues: [ | |
| {id: 'v2', name: 'Veggies For Everyone'} | |
| ] | |
| } | |
| ], | |
| sharedListsWithVenues: [ | |
| { | |
| id: 'l1', | |
| name: 'My Favorite Venues', | |
| venues: [ | |
| {id: 'v1', name: 'Hipster Coffee House'}, | |
| {id: 'v2', name: 'Veggies For Everyone'} | |
| ] | |
| } | |
| ] | |
| }; | |
| var state = _.assign({}, coreState, derivedState); | |
| var action = { | |
| type: 'ADD_VENUE_TO_LIST', | |
| venueId: 'v2', | |
| listId: 'l1' | |
| }; | |
| // When an action occurs, core state will be updated using the action, and then | |
| // derived state can be computed by applying projections to the new state. | |
| // You can think of: | |
| // coreState = f(state, action) | |
| // derivedState = f(state) | |
| // Core state reducers | |
| function venuesById(state, action) { | |
| // ... | |
| return state; | |
| } | |
| function listsById(state, action) { | |
| switch (action.type) { | |
| case 'ADD_VENUE_TO_LIST': | |
| let list = _.cloneDeep(state[action.listId]); | |
| list.venues.push(action.venueId); | |
| let newState = _.clone(state); | |
| newState[list.id] = list; | |
| return newState; | |
| // ... | |
| default: | |
| return state; | |
| } | |
| } | |
| function sharedListIds(state, action) { | |
| // ... | |
| return state; | |
| } | |
| // Projections for derived state | |
| var listsWithVenues = ['listsById', 'venuesById', function(state) { | |
| return _.map(state.listsById, list => { | |
| var venues = _.map(list.venues, venueId => state.venuesById[venueId]); | |
| list = _.assign({}, list, {venues: venues}); | |
| return list; | |
| }); | |
| }]; | |
| var sharedListsWithVenues = ['sharedListIds', 'listsWithVenues', function(state) { | |
| return _.filter(state.listsWithVenues, list => | |
| state.sharedListIds.indexOf(list.id) > -1 | |
| ); | |
| }]; | |
| // Notice that projections depend on a certain subset or "slice" of state. | |
| // We declare those dependencies as the first elements of an array, the last | |
| // element being the actual projection function. | |
| // Also notice that a projection can depend on other projections | |
| // ("sharedListsWithVenues" depends on "listsWithVenues" in our example). | |
| // We can use the core state "reducers" and the derived state "projections" | |
| // in a "main reducer" (assume we are using the Redux library) | |
| function mainReducer1(state, action) { | |
| // First we compute core state | |
| var newState = { | |
| venuesById: venuesById(state.venuesById, action), | |
| listsById: listsById(state.listsById, action), | |
| sharedListIds: sharedListIds(state.sharedListIds, action) | |
| }; | |
| // Then we compute derived state, manually fullfilling the dependencies | |
| // of each projection | |
| newState = _.assign(newState, { | |
| listsWithVenues: _.last(listsWithVenues)({ | |
| listsById: newState.listsById, | |
| venuesById: newState.venuesById | |
| }) | |
| }); | |
| newState = _.assign(newState, { | |
| sharedListsWithVenues: _.last(sharedListsWithVenues)({ | |
| sharedListIds: newState.sharedListIds, | |
| listsWithVenues: newState.listsWithVenues | |
| }) | |
| }); | |
| return newState; | |
| } | |
| // Since our projections declare their dependencies, we can use a function | |
| // (much like the "auto" function from the "async" library) that will figure | |
| // out what needs to be computed first etc. | |
| function mainReducer2(state, action) { | |
| var newState = auto({ | |
| // Core state | |
| // Give "auto" plain functions with no dependencies | |
| venuesById: venuesById.bind(null, state.venuesById, action), | |
| listsById: listsById.bind(null, state.listsById, action), | |
| sharedListIds: sharedListIds.bind(null, state.sharedListIds, action), | |
| // Derived state | |
| // Give "auto" arrays with dependencies and the projection function, | |
| // it will figure out in which order to run them | |
| listsWithVenues: listsWithVenues, | |
| sharedListsWithVenues: sharedListsWithVenues | |
| }); | |
| return newState; | |
| } | |
| // We can even go a step further, and "memoize" our projections | |
| // (since we know which slice of state they depend on). | |
| // This way they won't get re-computed on every single action, giving us | |
| // a performance optimization for those that are more resource-intensive. | |
| var listsWithVenuesMemoized = memoizeProjection(listsWithVenues); | |
| var sharedListsWithVenuesMemoized = memoizeProjection(sharedListsWithVenues); | |
| function mainReducer3(state, action) { | |
| var newState = auto({ | |
| // Core state | |
| venuesById: venuesById.bind(null, state.venuesById, action), | |
| listsById: listsById.bind(null, state.listsById, action), | |
| sharedListIds: sharedListIds.bind(null, state.sharedListIds, action), | |
| // Derived state | |
| // Projections will only be re-run if their slice of state changes | |
| listsWithVenues: listsWithVenuesMemoized, | |
| sharedListsWithVenues: sharedListsWithVenuesMemoized | |
| }); | |
| return newState; | |
| } | |
| // Utilities | |
| // ----------------------------------------------- | |
| function memoizeProjection(projection) { | |
| projection = ensureArray(projection); | |
| var dependencies = projection.slice(0, projection.length - 1); | |
| var projectionFn = projection[projection.length - 1]; | |
| var cache = { | |
| state: null, | |
| result: null | |
| }; | |
| var sliceState = function(state) { return _.pick(state, dependencies); }; | |
| if (!dependencies.length) { | |
| sliceState = _.identity; | |
| } | |
| function memoized(state) { | |
| var stateSlice = sliceState(state); | |
| var prevStateSlice = sliceState(cache.state); | |
| if (shallowEqualImmutable(stateSlice, prevStateSlice)) { | |
| return cache.result; | |
| } | |
| var result = projectionFn(state); | |
| cache.result = result; | |
| cache.state = state; | |
| return result; | |
| } | |
| memoized.cache = cache; | |
| if (!dependencies.length) { | |
| return memoized; | |
| } | |
| return [...dependencies, memoized]; | |
| } | |
| // Inspired by https://github.com/caolan/async#auto | |
| function auto(tasks) { | |
| var keys = _.keys(tasks); | |
| if (!keys.length) { | |
| return; | |
| } | |
| var results = {}; | |
| var listeners = []; | |
| function addListener(fn) { | |
| listeners.unshift(fn); | |
| } | |
| function removeListener(fn) { | |
| var idx = _.indexOf(listeners, fn); | |
| if (idx >= 0) listeners.splice(idx, 1); | |
| } | |
| function taskComplete(key, taskResult) { | |
| results[key] = taskResult; | |
| _.forEach(listeners.slice(0), function(fn) { | |
| fn(); | |
| }); | |
| } | |
| _.forEach(keys, function(k) { | |
| var task = _.isArray(tasks[k]) ? tasks[k] : [tasks[k]]; | |
| var requires = task.slice(0, task.length - 1); | |
| // Prevent dead-locks | |
| var len = requires.length; | |
| var dep; | |
| while (len--) { | |
| if (!(dep = tasks[requires[len]])) { | |
| throw new Error('Has inexistant dependency'); | |
| } | |
| if (_.isArray(dep) && _.indexOf(dep, k) >= 0) { | |
| throw new Error('Has cyclic dependencies'); | |
| } | |
| } | |
| function taskArg() { | |
| return _.pick(results, requires); | |
| } | |
| function ready() { | |
| return _.reduce(requires, function(a, x) { | |
| return (a && results.hasOwnProperty(x)); | |
| }, true) && !results.hasOwnProperty(k); | |
| } | |
| var taskResult; | |
| if (ready()) { | |
| taskResult = task[task.length - 1](taskArg()); | |
| taskComplete(k, taskResult); | |
| } else { | |
| addListener(listener); | |
| } | |
| function listener() { | |
| if (ready()) { | |
| removeListener(listener); | |
| taskResult = task[task.length - 1](taskArg()); | |
| taskComplete(k, taskResult); | |
| } | |
| } | |
| }); | |
| return results; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment