Repository: pmndrs/zustand
Pull request #241 (2020-11-13 15:56:06)

Expose previous value in subscribe
Decided to go ahead with my proposal of #240. I found the second argument was used for the error in case there's an error, but that's undocumented, so I am not sure how much of a breaking change this would be.

codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/pmndrs/zustand/pr/241/builds/72921) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit f1534068c6292763ef88a402d51f110185c20936:

|Sandbox| Source |
|--|--|
|[React](https://codesandbox.io/s/react-vbc5q)| Configuration |
|[React Typescript](https://codesandbox.io/s/react-typescript-w7oks)| Configuration |

dai-shi: We have some discussions in #208 and I personally think it's ok to make a breaking change (even without major version up as it's an undocumented behavior). I wish original authors respond about this. @drcmda @JeremyRH 


Tirzono: @dai-shi of course, not a problem. I pushed the changes, let me know if you think it needs more changes.
dai-shi: @Tirzono I merged a test #237 into master. Would you merge master and check if it passes?
dai-shi: Thanks! Merging this.

Patch:
diff --git a/readme.md b/readme.md
index 6327734..c10d5dc 100644
--- a/readme.md
+++ b/readme.md
@@ -175,12 +175,15 @@ const unsub1 = useStore.subscribe(console.log)
 const unsub2 = useStore.subscribe(console.log, state => state.paw)
 // Subscribe also supports an optional equality function
 const unsub3 = useStore.subscribe(console.log, state => [state.paw, state.fur], shallow)
+// Subscribe also exposes the previous value
+const unsub4 = useStore.subscribe((paw, previousPaw) => console.log(paw, previousPaw), state => state.paw)
 // Updating state, will trigger listeners
 useStore.setState({ paw: false })
 // Unsubscribe listeners
 unsub1()
 unsub2()
 unsub3()
+unsub4()
 // Destroying the store (removing all listeners)
 useStore.destroy()
 
diff --git a/src/vanilla.ts b/src/vanilla.ts
index 570def4..5c08b92 100644
--- a/src/vanilla.ts
+++ b/src/vanilla.ts
@@ -4,8 +4,8 @@ export type PartialState<T extends State> =
   | ((state: T) => Partial<T>)
 export type StateSelector<T extends State, U> = (state: T) => U
 export type EqualityChecker<T> = (state: T, newState: unknown) => boolean
-export type StateListener<T> = (state: T) => void
-export type StateSliceListener<T> = (state: T | null, error?: Error) => void
+export type StateListener<T> = (state: T, previousState: T) => void
+export type StateSliceListener<T> = (slice: T, previousSlice: T) => void
 export interface Subscribe<T extends State> {
   (listener: StateListener<T>): () => void
   <StateSlice>(
@@ -41,10 +41,11 @@ export default function create<TState extends State>(
   const setState: SetState<TState> = (partial, replace) => {
     const nextState = typeof partial === 'function' ? partial(state) : partial
     if (nextState !== state) {
+      const previousState = state
       state = replace
         ? (nextState as TState)
         : Object.assign({}, state, nextState)
-      listeners.forEach((listener) => listener(state))
+      listeners.forEach((listener) => listener(state, previousState))
     }
   }
 
@@ -57,16 +58,10 @@ export default function create<TState extends State>(
   ) => {
     let currentSlice: StateSlice = selector(state)
     function listenerToAdd() {
-      // Selector or equality function could throw but we don't want to stop
-      // the listener from being called.
-      // https://github.com/react-spring/zustand/pull/37
-      try {
-        const newStateSlice = selector(state)
-        if (!equalityFn(currentSlice, newStateSlice)) {
-          listener((currentSlice = newStateSlice))
-        }
-      } catch (error) {
-        listener(null, error)
+      const nextSlice = selector(state)
+      if (!equalityFn(currentSlice, nextSlice)) {
+        const previousSlice = currentSlice
+        listener((currentSlice = nextSlice), previousSlice)
       }
     }
     listeners.add(listenerToAdd)


Test patch:
diff --git a/tests/basic.test.tsx b/tests/basic.test.tsx
index c17f590..e734af6 100644
--- a/tests/basic.test.tsx
+++ b/tests/basic.test.tsx
@@ -412,7 +412,10 @@ it('can subscribe to the store', () => {
   setState({ value: initialState.value + 1 })
   unsub()
   expect(listener).toHaveBeenCalledTimes(1)
-  expect(listener).toHaveBeenCalledWith(initialState.value + 1)
+  expect(listener).toHaveBeenCalledWith(
+    initialState.value + 1,
+    initialState.value
+  )
 
   // Should not be called when equality checker returns true
   unsub = subscribe(
@@ -435,7 +438,10 @@ it('can subscribe to the store', () => {
   setState({ value: initialState.value + 2 })
   unsub()
   expect(listener).toHaveBeenCalledTimes(1)
-  expect(listener).toHaveBeenCalledWith(initialState.value + 2)
+  expect(listener).toHaveBeenCalledWith(
+    initialState.value + 2,
+    initialState.value + 2
+  )
 
   // Should keep consistent behavior with equality check
   const isRoughEqual = (x: number, y: number) => Math.abs(x - y) < 1
@@ -448,7 +454,7 @@ it('can subscribe to the store', () => {
       // skip assuming values are equal
       return
     }
-    listener(s.value)
+    listener(s.value, prevValue)
     prevValue = s.value
   })
   const unsub2 = subscribe(listener2, (s) => s.value, isRoughEqual as any)
@@ -457,9 +463,9 @@ it('can subscribe to the store', () => {
   unsub()
   unsub2()
   expect(listener).toHaveBeenCalledTimes(1)
-  expect(listener).toHaveBeenCalledWith(1)
+  expect(listener).toHaveBeenCalledWith(1, 0)
   expect(listener2).toHaveBeenCalledTimes(1)
-  expect(listener2).toHaveBeenCalledWith(1)
+  expect(listener2).toHaveBeenCalledWith(1, 0)
 })
 
 it('can destroy the store', () => {