const initialState = {
cartId: 123,
items: [{ sku: 1, name: 'apple' }],
owner: { name: 'James' }
};
const nextState1 = immer.produce(initialState, (draft) => {
// do nothing
});
// We didn't touch the draft, so the produced state is the same
console.log(`equal? ${nextState1 === initialState}`);
const nextState2 = immer.produce(initialState, (draft) => {
// mutate the draft
draft.items.push({ sku: 2, name: 'rice' });
});
// The states are different now
console.log(`equal? ${nextState2 === initialState}`);
// Any internal references are still shared ("structural sharing")
console.log(`equal owner? ${nextState2.owner === initialState.owner}`);
nextState2
Usage with Redux
// "Traditional" reducer
// Example based on user-reducer.js from account app
const _ = require('lodash');
function userReducer(userState, action) {
switch (action.type) {
case 'CREATE_TOKEN': {
const nextState = _.assign({}, userState, {
tokens: [].concat(userState.tokens, action.tokenMetadata)
});
return nextState;
}
// other cases ...
default: { return userState; }
}
}
// What exactly is the CREATE_TOKEN handler doing?
const initUserState = {
authenticatedUser: { id: 1, name: 'James' },
tokens: [],
};
const createTokenAction = {
type: 'CREATE_TOKEN',
tokenMetadata: { id: 'abc123' }
};
const nextUserState = userReducer(initUserState, createTokenAction);
function userReducerWithImmer(userState, action) {
return immer.produce(userState, (draft) => {
switch (action.type) {
case 'CREATE_TOKEN': {
draft.tokens.push(action.tokenMetadata);
return;
}
// other cases ...
// default case is not needed
}
});
}
// The CREATE_TOKEN handler is much more readable/idiomatic
const initUserState2 = {
authenticatedUser: { id: 2, name: 'Tayor' },
tokens: [],
};
const createTokenAction2 = {
type: 'CREATE_TOKEN',
tokenMetadata: { id: 'def456' }
};
const nextUserState2 = userReducerWithImmer(initUserState2, createTokenAction2);
// We could also use the curried form of `produce` for even less code
const userReducerWithImmer2 = immer.produce((draft, action) => {
switch (action.type) {
case 'CREATE_TOKEN': {
draft.tokens.push(action.tokenMetadata);
return;
}
// other cases ...
}
});
userReducerWithImmer2(initUserState2, createTokenAction2);
// Let's look at the `set` Proxy handler from immer:
// https://github.com/mweststrate/immer/blob/d157d7a7d32c54ae0af27cb2a3c96e1e596cf537/src/proxy.js#L93-L107
function set(state, prop, value) {
if (!state.modified) {
// Optimize based on value's truthiness. Truthy values are guaranteed to
// never be undefined, so we can avoid the `in` operator. Lastly, truthy
// values may be proxies, but falsy values are never proxies.
const isUnchanged = value
? is(state.base[prop], value) || value === state.proxies[prop]
: is(state.base[prop], value) && prop in state.base
if (isUnchanged) return true
markChanged(state)
}
state.assigned[prop] = true
state.copy[prop] = value
return true
}
// and a few helper functions
function shallowCopy(value) {
if (Array.isArray(value)) return value.slice()
const target = value.__proto__ === undefined ? Object.create(null) : {}
return Object.assign(target, value)
}
function markChanged(state) {
if (!state.modified) {
state.modified = true
state.copy = shallowCopy(state.base)
// copy the proxies over the base-copy
Object.assign(state.copy, state.proxies) // yup that works for arrays as well
if (state.parent) markChanged(state.parent)
}
}
function is(x, y) {
if (x === y) {
return x !== 0 || 1 / x === 1 / y
} else {
return x !== x && y !== y
}
}
// immer.produce's initial `state` is created as so:
function createState(parent, base) {
return {
modified: false, // this tree is modified (either this object or one of it's children)
assigned: {}, // true: value was assigned to these props, false: was removed
finalized: false,
parent,
base,
copy: undefined,
proxies: {}
}
}
const immerState = createState(undefined, { name: 'Mario' });
// Now, let's call the `set` handler
set(immerState, 'name', 'Luigi');
// and see what the state looks like:
immerState
// We see that the `copy` now has our modified property
// and that the `modified` flag has been set to `true`.
// This _basically_ shows how immer works under the hood.
// At the end of `produce`'s callback, its internal state
// is traversed and any assigned properties are
// set to the modified copy's.
// All unassigned properties are set (=) to the base's.