Skip to content

Commit

Permalink
Merge pull request #402 from immerjs/produce-with-patches
Browse files Browse the repository at this point in the history
feat: introduced produceWithPatches. Implements #400
  • Loading branch information
mweststrate committed Aug 2, 2019
2 parents 615ca6f + 86be737 commit a16deda
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 2 deletions.
26 changes: 25 additions & 1 deletion __tests__/curry.js
@@ -1,5 +1,5 @@
"use strict"
import produce, {setUseProxies} from "../src/index"
import produce, {setUseProxies, produceWithPatches} from "../src/index"

runTests("proxy", true)
runTests("es5", false)
Expand Down Expand Up @@ -69,4 +69,28 @@ function runTests(name, useProxies) {
expect(spread(base, {y: 1})).toBe(base)
})
})

it("support currying for produceWithPatches", () => {
const increment = produceWithPatches((draft, delta) => {
draft.x += delta
})

expect(increment({x: 5}, 2)).toEqual([
{x: 7},
[
{
op: "replace",
path: ["x"],
value: 7
}
],
[
{
op: "replace",
path: ["x"],
value: 5
}
]
])
})
}
36 changes: 35 additions & 1 deletion __tests__/readme.js
@@ -1,5 +1,9 @@
"use strict"
import produce, {applyPatches, immerable} from "../src/index"
import produce, {
applyPatches,
immerable,
produceWithPatches
} from "../src/index"

describe("readme example", () => {
it("works", () => {
Expand Down Expand Up @@ -168,4 +172,34 @@ describe("readme example", () => {
expect(lunch).toBeInstanceOf(Clock)
expect(diner.toString()).toBe("18:30")
})

test("produceWithPatches", () => {
const result = produceWithPatches(
{
age: 33
},
draft => {
draft.age++
}
)
expect(result).toEqual([
{
age: 34
},
[
{
op: "replace",
path: ["age"],
value: 34
}
],
[
{
op: "replace",
path: ["age"],
value: 33
}
]
])
})
})
41 changes: 41 additions & 0 deletions readme.md
Expand Up @@ -333,6 +333,47 @@ The generated patches are similar (but not the same) to the [RFC-6902 JSON patch
]
```

### `produceWithPatches`

Instead of setting up a patch listener, an easier way to obtain the patches is to use `produceWithPatches`, which has the same signature as `produce`, except that it doesn't return just the next state, but a tuple consisting of `[nextState, patches, inversePatches]`. Like `produce`, `produceWithPatches` supports currying as well.

```javascript
import {produceWithPatches} from "immer"

const [nextState, patches, inversePatches] = produceWithPatches(
{
age: 33
},
draft => {
draft.age++
}
)
```

Which produces:

```javascript
;[
{
age: 34
},
[
{
op: "replace",
path: ["age"],
value: 34
}
],
[
{
op: "replace",
path: ["age"],
value: 33
}
]
]
```

For a more in-depth study, see [Distributing patches and rebasing actions using Immer](https://medium.com/@mweststrate/distributing-state-changes-using-snapshots-patches-and-actions-part-2-2f50d8363988)

Tip: Check this trick to [compress patches](https://medium.com/@david.b.edelstein/using-immer-to-compress-immer-patches-f382835b6c69) produced over time.
Expand Down
43 changes: 43 additions & 0 deletions src/immer.d.ts
Expand Up @@ -109,6 +109,49 @@ export interface IProduce {
export const produce: IProduce
export default produce

/**
* Like `produce`, but instead of just returning the new state,
* a tuple is returned with [nextState, patches, inversePatches]
*
* Like produce, this function supports currying
*/
export interface IProduceWithPatches {
/** Curried producer */
<
Recipe extends (...args: any[]) => any,
Params extends any[] = Parameters<Recipe>,
T = Params[0]
>(
recipe: Recipe
): <Base extends Immutable<T>>(
base: Base,
...rest: Tail<Params>
) => [Produced<Base, ReturnType<Recipe>>, Patch[], Patch[]]
// ^ by making the returned type generic, the actual type of the passed in object is preferred
// over the type used in the recipe. However, it does have to satisfy the immutable version used in the recipe
// Note: the type of S is the widened version of T, so it can have more props than T, but that is technically actually correct!

/** Curried producer with initial state */
<
Recipe extends (...args: any[]) => any,
Params extends any[] = Parameters<Recipe>,
T = Params[0]
>(
recipe: Recipe,
initialState: Immutable<T>
): <Base extends Immutable<T>>(
base?: Base,
...rest: Tail<Params>
) => [Produced<Base, ReturnType<Recipe>>, Patch[], Patch[]]

/** Normal producer */
<Base, D = Draft<Base>, Return = void>(
base: Base,
recipe: (draft: D) => Return
): [Produced<Base, Return>, Patch[], Patch[]]
}
export const produceWithPatches: IProduceWithPatches

/** Use a class type for `nothing` so its type is unique */
declare class Nothing {
// This lets us do `Exclude<T, Nothing>`
Expand Down
16 changes: 16 additions & 0 deletions src/immer.js
Expand Up @@ -91,6 +91,22 @@ export class Immer {
return result !== NOTHING ? result : undefined
}
}
produceWithPatches(arg1, arg2, arg3) {
if (typeof arg1 === "function") {
const self = this
return (state, ...args) =>
this.produceWithPatches(state, draft => arg1(draft, ...args))
}
// non-curried form
if (arg3)
throw new Error("A patch listener cannot be passed to produceWithPatches")
let patches, inversePatches
const nextState = this.produce(arg1, arg2, (p, ip) => {
patches = p
inversePatches = ip
})
return [nextState, patches, inversePatches]
}
createDraft(base) {
if (!isDraftable(base)) {
throw new Error("First argument to `createDraft` must be a plain object, an array, or an immerable object") // prettier-ignore
Expand Down
6 changes: 6 additions & 0 deletions src/index.js
Expand Up @@ -24,6 +24,12 @@ const immer = new Immer()
export const produce = immer.produce
export default produce

/**
* Like `produce`, but `produceWithPatches` always returns a tuple
* [nextState, patches, inversePatches] (instead of just the next state)
*/
export const produceWithPatches = immer.produceWithPatches.bind(immer)

/**
* Pass true to automatically freeze all copies created by Immer.
*
Expand Down

0 comments on commit a16deda

Please sign in to comment.