Immer Exploration

node v8.15.0
version: 1.1.0
endpointsharetweet
Frontend Deep Dive 12/11/18
const immer = require('immer');
Basic Examples
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);
// How does `immer` work? // Proxies! // Here's a very basic Proxy example: const myPet = { name: 'Buster' }; let setCount = 0; const handler = { get: (target, prop) => { return `Proxy: ${target[prop]}`; }, set: (target, prop, value) => { setCount = setCount + 1; target[prop] = value; } }; const proxiedPet = new Proxy(myPet, handler); console.log(myPet.name); console.log(proxiedPet.name);
console.log({ setCount }); proxiedPet.name = 'Vickie'; console.log({ setCount });
// 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.
Loading…

no comments

    sign in to comment