diff --git a/__tests__/curry.js b/__tests__/curry.js index 04c32c9e..15885b7e 100644 --- a/__tests__/curry.js +++ b/__tests__/curry.js @@ -10,9 +10,6 @@ function runTests(name, useProxies) { it("should check arguments", () => { expect(() => produce()).toThrow(/produce expects 1 or 2 arguments/) - expect(() => produce(() => {}, {})).toThrow( - /the second argument to produce should be a function/ - ) expect(() => produce(new Buffer(""), () => {})).toThrow( /the first argument to an immer producer should be a primitive, plain object or array/ ) @@ -21,7 +18,10 @@ function runTests(name, useProxies) { ) expect(() => produce({}, {})).toThrow(/should be a function/) expect(() => produce({})).toThrow( - /first argument should be a function/ + /if first argument is not a function, the second argument to produce should be a function/ + ) + expect(() => produce(() => {}, () => {})).toThrow( + /if first argument is a function .* the second argument to produce cannot be a function/ ) }) @@ -51,5 +51,17 @@ function runTests(name, useProxies) { expect(reducer(undefined, 3)).toEqual({hello: "world"}) expect(reducer({}, 3)).toEqual({index: 3}) }) + + it("should support passing an initial state as second argument", () => { + const reducer = produce( + (item, index) => { + item.index = index + }, + {hello: "world"} + ) + + expect(reducer(undefined, 3)).toEqual({hello: "world", index: 3}) + expect(reducer({}, 3)).toEqual({index: 3}) + }) }) } diff --git a/readme.md b/readme.md index 668822fa..df9d96f8 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,7 @@ The Immer package exposes a default function that does all the work. `produce(currentState, producer: (draftState) => void): nextState` -There is also a curried overload that is explained [below](#currying) +There is also a curried overload that is explained [below](#currying). ## Example @@ -173,8 +173,7 @@ onBirthDayClick2 = () => { ## Currying -`produce` can be called with one or two arguments. -The one argument version is intended to be used for currying. This means that you get a pre-bound producer that only needs a state to produce the value from. +Passing a function as the first argument to `produce` is intended to be used for currying. This means that you get a pre-bound producer that only needs a state to produce the value from. The producer function gets passed in the draft, and any further arguments that were passed to the curried function. For example: @@ -207,7 +206,22 @@ const byId = produce((draft, action) => { ``` Note that `state` is now factored out (the created reducer will accept a state, and invoke the bound producer with it). -One think to keep in mind; you cannot use this construction to initialize an uninitialized state. E.g. `draft = {}` doesn't do anything useful. + +If you want to initialize an uninitialized state using this construction, you can do so by passing the initial state as second argument to `produce`: + +```javascript +import produce from 'immer' + +const byId = produce((draft, action) => { + switch (action.type) { + case RECEIVE_PRODUCTS: + action.products.forEach(product => { + draft[product.id] = product + }) + }) + } +}, { 1: { id: 1, name: "product-1" } }) +``` ## Auto freezing @@ -221,9 +235,9 @@ One think to keep in mind; you cannot use this construction to initialize an uni ## Returning data from producers It is not needed to return anything from a producer, as Immer will return the (finalized) version of the `draft` anyway. -However, it allowed to just `return draft`. +However, it is allowed to just `return draft`. -It is also allowed to return abritrarily other data from the producer function. But _only_ if you didn't modify the draft. +It is also allowed to return arbitrarily other data from the producer function. But _only_ if you didn't modify the draft. This can be useful to produce an entirely new state. Some examples: ```javascript diff --git a/src/immer.d.ts b/src/immer.d.ts index 40616b45..ceb58885 100644 --- a/src/immer.d.ts +++ b/src/immer.d.ts @@ -1,13 +1,14 @@ /** * Immer takes a state, and runs a function against it. * That function can freely mutate the state, as it will create copies-on-write. - * This means that the original state will stay unchanged, and once the function finishes, the modified state is returned + * This means that the original state will stay unchanged, and once the function finishes, the modified state is returned. * - * If only one argument is passed, this is interpreted as the recipe, and will create a curried function that will execute the recipe - * any time it is called with a base state + * If the first argument is a function, this is interpreted as the recipe, and will create a curried function that will execute the recipe + * any time it is called with the current state. * * @param currentState - the state to start with - * @param thunk - function that receives a proxy of the current state as first argument and which can be freely modified + * @param recipe - function that receives a proxy of the current state as first argument and which can be freely modified + * @param initialState - if a curried function is created and this argument was given, it will be used as fallback if the curried function is called with a state of undefined * @returns The next state: a new state, or the current state if nothing was modified */ export default function( @@ -16,16 +17,20 @@ export default function( ): S // curried invocations export default function( - recipe: (this: S, draftState: S, ...extraArgs: any[]) => void + recipe: (this: S, draftState: S, ...extraArgs: any[]) => void, + initialState?: S ): (currentState: S, ...extraArgs: any[]) => S export default function( - recipe: (this: S, draftState: S, a: A) => void + recipe: (this: S, draftState: S, a: A) => void, + initialState?: S ): (currentState: S, a: A) => S export default function( - recipe: (this: S, draftState: S, a: A, b: B) => void + recipe: (this: S, draftState: S, a: A, b: B) => void, + initialState?: S ): (currentState: S, a: A, b: B) => S export default function( - recipe: (this: S, draftState: S, a: A, b: B, c: C) => void + recipe: (this: S, draftState: S, a: A, b: B, c: C) => void, + initialState?: S ): (currentState: S, a: A, b: B, c: C) => S /** diff --git a/src/immer.js b/src/immer.js index 46dd9b50..b422d6f3 100644 --- a/src/immer.js +++ b/src/immer.js @@ -15,24 +15,35 @@ import {produceEs5} from "./es5" * @returns {any} a new state, or the base state if nothing was modified */ export default function produce(baseState, producer) { + // prettier-ignore + if (arguments.length !== 1 && arguments.length !== 2) throw new Error("produce expects 1 or 2 arguments, got " + arguments.length) + // curried invocation - if (arguments.length === 1) { - const producer = baseState + if (typeof baseState === "function") { // prettier-ignore - if (typeof producer !== "function") throw new Error("if produce is called with 1 argument, the first argument should be a function") + if (typeof producer === "function") throw new Error("if first argument is a function (curried invocation), the second argument to produce cannot be a function") + + const initialState = producer + const recipe = baseState + return function() { const args = arguments - return produce(args[0], draft => { + + const currentState = + args[0] === undefined && initialState !== undefined + ? initialState + : args[0] + + return produce(currentState, draft => { args[0] = draft // blegh! - return producer.apply(draft, args) + return recipe.apply(draft, args) }) } } // prettier-ignore { - if (arguments.length !== 2) throw new Error("produce expects 1 or 2 arguments, got " + arguments.length) - if (typeof producer !== "function") throw new Error("the second argument to produce should be a function") + if (typeof producer !== "function") throw new Error("if first argument is not a function, the second argument to produce should be a function") } // if state is a primitive, don't bother proxying at all and just return whatever the producer returns on that value diff --git a/src/immer.js.flow b/src/immer.js.flow index 9cf0d093..0244675a 100644 --- a/src/immer.js.flow +++ b/src/immer.js.flow @@ -3,13 +3,14 @@ /** * Immer takes a state, and runs a function against it. * That function can freely mutate the state, as it will create copies-on-write. - * This means that the original state will stay unchanged, and once the function finishes, the modified state is returned + * This means that the original state will stay unchanged, and once the function finishes, the modified state is returned. * - * If only one argument is passed, this is interpreted as the recipe, and will create a curried function that will execute the recipe - * any time it is called with a base state + * If the first argument is a function, this is interpreted as the recipe, and will create a curried function that will execute the recipe + * any time it is called with the current state. * * @param currentState - the state to start with - * @param thunk - function that receives a proxy of the current state as first argument and which can be freely modified + * @param recipe - function that receives a proxy of the current state as first argument and which can be freely modified + * @param initialState - if a curried function is created and this argument was given, it will be used as fallback if the curried function is called with a state of undefined * @returns The next state: a new state, or the current state if nothing was modified */ declare export default function produce( @@ -18,16 +19,20 @@ declare export default function produce( ): S // curried invocations declare export default function produce( - recipe: (draftState: S, a: A, b: B, c: C) => void + recipe: (draftState: S, a: A, b: B, c: C) => void, + initialState?: S ): (currentState: S, a: A, b: B, c: C) => S declare export default function produce( - recipe: (draftState: S, a: A, b: B) => void + recipe: (draftState: S, a: A, b: B) => void, + initialState?: S ): (currentState: S, a: A, b: B) => S declare export default function produce( - recipe: (draftState: S, a: A) => void + recipe: (draftState: S, a: A) => void, + initialState?: S ): (currentState: S) => S declare export default function produce( - recipe: (draftState: S, ...extraArgs: any[]) => void + recipe: (draftState: S, ...extraArgs: any[]) => void, + initialState?: S ): (currentState: S, ...extraArgs: any[]) => S /**