From 0db9961c8300fe7839385df12383279311efd33f Mon Sep 17 00:00:00 2001 From: Todd Kennedy Date: Tue, 14 Jun 2016 21:33:55 -0700 Subject: [PATCH] Make state immutable in reducers/effects Call Object.freeze on state when passing it into to the reducers or effects so that state cannot be modified except when being returned from an emitter. Add tests to prove state is immutable in these scenarios. --- README.md | 4 +++ index.js | 8 +++--- tests/z-disabled-browser.js | 55 +++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 tests/z-disabled-browser.js diff --git a/README.md b/README.md index f43d5201..6b444325 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,10 @@ and has access to the full application state. Try and keep the logic in these bulk of your logic will safely shielded, with only a few points touching every part of your application. +Within the scope of `reducers` and `effects`, you can access the current `state`, +however, the object is frozen, disallowing changes. To update the state, you +must return an object with your updates from a `reducer`. + ## Effects Side effects are done through `effects` declared in `app.model()`. Unlike `reducers` they cannot modify the state by returning objects, but get a diff --git a/index.js b/index.js index 473f81ed..7a146846 100644 --- a/index.js +++ b/index.js @@ -139,19 +139,19 @@ function choo () { const _reducers = ns ? reducers[ns] : reducers if (_reducers && _reducers[action.type]) { if (ns) { - const reducedState = _reducers[action.type](action, state[ns]) + const reducedState = _reducers[action.type](action, Object.freeze(state[ns])) if (!newState[ns]) newState[ns] = {} mutate(newState[ns], xtend(state[ns], reducedState)) } else { - mutate(newState, reducers[action.type](action, state)) + mutate(newState, reducers[action.type](action, Object.freeze(state))) } reducersCalled = true } const _effects = ns ? effects[ns] : effects if (_effects && _effects[action.type]) { - if (ns) _effects[action.type](action, state[ns], send) - else _effects[action.type](action, state, send) + if (ns) _effects[action.type](action, Object.freeze(state[ns]), send) + else _effects[action.type](action, Object.freeze(state), send) effectsCalled = true } diff --git a/tests/z-disabled-browser.js b/tests/z-disabled-browser.js new file mode 100644 index 00000000..26792ee7 --- /dev/null +++ b/tests/z-disabled-browser.js @@ -0,0 +1,55 @@ +const tape = require('tape') +const choo = require('../') + +tape('should render on the client', {skip: true}, function (t) { + t.test('state should not be mutable', function (t) { + t.plan(3) + + const app = choo() + const state = { + foo: 'baz', + beep: 'boop' + } + app.model({ + state: state, + reducers: { + mutate: (action, state) => { + state.foo = 'zap' + return {} + }, + noMutate: (action, state) => { + return {beep: 'poob'} + } + }, + effects: { + effectMutate: (action, state) => { + state.foo = 'zap' + } + } + }) + + app.router((route) => [ + route('/', function (params, state, send) { + const okd = (evt) => { + if (evt.key === 'a') { + send('mutate') + } else { + send('noMutate') + } + } + return choo.view`${state.foo}:${state.beep}` + }) + ]) + document.body.appendChild(app.start()) + const $el = document.querySelector('.test') + const mutate = new window.KeyboardEvent('keydown', {key: 'a'}) + const noMutate = new window.KeyboardEvent('keydown', {key: 'b'}) + const effectMutate = new window.KeyboardEvent('keydown', {key: 'c'}) + $el.dispatchEvent(mutate) + t.equal($el.innerText, 'baz:boop', 'state did not mutate') + $el.dispatchEvent(noMutate) + t.equal($el.innerText, 'baz:poob', 'state was updated from a return') + $el.dispatchEvent(effectMutate) + t.equal($el.innerText, 'baz:poob', 'state was not updated from an effect') + }) +})