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') + }) +})