Skip to content

Commit

Permalink
Merge pull request #121 from pkerschbaum/feature/initial-state-for-cu…
Browse files Browse the repository at this point in the history
…rry-fn

resolve #111: allow passing initial state to curried producer function
  • Loading branch information
mweststrate committed Mar 17, 2018
2 parents 1b381ff + 202d54c commit 2cb5496
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 33 deletions.
20 changes: 16 additions & 4 deletions __tests__/curry.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/
)
Expand All @@ -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/
)
})

Expand Down Expand Up @@ -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})
})
})
}
26 changes: 20 additions & 6 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
21 changes: 13 additions & 8 deletions src/immer.d.ts
Original file line number Diff line number Diff line change
@@ -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<S = any>(
Expand All @@ -16,16 +17,20 @@ export default function<S = any>(
): S
// curried invocations
export default function<S = any>(
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<S = any, A = any>(
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<S = any, A = any, B = any>(
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<S = any, A = any, B = any, C = any>(
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

/**
Expand Down
25 changes: 18 additions & 7 deletions src/immer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 13 additions & 8 deletions src/immer.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -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<S>(
Expand All @@ -18,16 +19,20 @@ declare export default function produce<S>(
): S
// curried invocations
declare export default function produce<S, A, B, C>(
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<S, A, B>(
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<S, A>(
recipe: (draftState: S, a: A) => void
recipe: (draftState: S, a: A) => void,
initialState?: S
): (currentState: S) => S
declare export default function produce<S>(
recipe: (draftState: S, ...extraArgs: any[]) => void
recipe: (draftState: S, ...extraArgs: any[]) => void,
initialState?: S
): (currentState: S, ...extraArgs: any[]) => S

/**
Expand Down

0 comments on commit 2cb5496

Please sign in to comment.