Repository: effector/effector (ts)

Issue:
`useUnit` returns stale value when used on dynamically changing Event
**What is the current behavior:**
When you change the Event provided to `useUnit` as an argument, the hook returns a stale value.

Please see the reproduction: https://share.effector.dev/jr0mXZYS

**What is the expected behavior:**
Like `useEvent`, `useUnit` should return an up-to-date value with the new callback.

**Which versions of effector packages?**
`master`, `effector v22.8.5` & `effector-react v22.5.1` at the moment of writing

Pull request:
Fix useUnit bug on event change
Fix #900

Patch:
diff --git a/src/react/apiBase.ts b/src/react/apiBase.ts
index 20164057..0be56a80 100644
--- a/src/react/apiBase.ts
+++ b/src/react/apiBase.ts
@@ -65,25 +65,40 @@ export function useUnitBase<Shape extends {[key: string]: Unit<any>}>(
     justSubscribed: false,
     scope,
   })
-  const [eventsShape, storeKeys, storeValues] = React.useMemo(() => {
-    flagsRef.current.stale = true
-    const shape = Array.isArray(normShape) ? [] : ({} as any)
-    const storeKeys: string[] = []
-    const storeValues: Array<Store<any>> = []
-    for (const key in normShape) {
-      const unit = normShape[key]
-      if (!is.unit(unit)) throwError('expect useUnit argument to be a unit')
-      if (is.event(unit) || is.effect(unit)) {
-        shape[key] = scope ? scopeBind(unit as Event<any>, {scope}) : unit
-      } else {
-        shape[key] = null
-        storeKeys.push(key)
-        storeValues.push(unit as Store<any>)
+  const [eventsShape, storeKeys, storeValues, eventKeys, eventValues] =
+    React.useMemo(() => {
+      flagsRef.current.stale = true
+      const shape = Array.isArray(normShape) ? [] : ({} as any)
+      const storeKeys: string[] = []
+      const storeValues: Array<Store<any>> = []
+      const eventKeys: string[] = []
+      const eventValues: Array<Unit<any>> = []
+      for (const key in normShape) {
+        const unit = normShape[key]
+        if (!is.unit(unit)) throwError('expect useUnit argument to be a unit')
+        if (is.event(unit) || is.effect(unit)) {
+          shape[key] = null
+          eventKeys.push(key)
+          eventValues.push(unit)
+        } else {
+          shape[key] = null
+          storeKeys.push(key)
+          storeValues.push(unit as Store<any>)
+        }
       }
-    }
-    return [shape, storeKeys, storeValues]
-  }, [flagsRef, scope, ...Object.keys(normShape), ...Object.values(normShape)])
-  const stateRef = React.useRef({value: eventsShape, storeKeys})
+      return [shape, storeKeys, storeValues, eventKeys, eventValues]
+    }, [
+      flagsRef,
+      scope,
+      ...Object.keys(normShape),
+      ...Object.values(normShape),
+    ])
+  const stateRef = React.useRef({
+    value: eventsShape,
+    storeKeys,
+    eventKeys,
+    eventValues,
+  })
   const subscribe = React.useCallback(
     (cb: () => void) => {
       const flags = flagsRef.current
@@ -110,19 +125,24 @@ export function useUnitBase<Shape extends {[key: string]: Unit<any>}>(
     let resultValue
     let changed = false
     const oldVal = state.value
-    const oldKeys = state.storeKeys
+    const oldStoreKeys = state.storeKeys
+    const oldEventKeys = state.eventKeys
+    const oldEventValues = state.eventValues
     const scopeChanged = scope !== flags.scope
     if (flags.stale || flags.justSubscribed || scopeChanged) {
       changed = !flags.justSubscribed || scopeChanged
       resultValue = isList ? [...eventsShape] : {...eventsShape}
-      if (oldKeys.length !== storeKeys.length) {
+      if (
+        oldStoreKeys.length !== storeKeys.length ||
+        oldEventKeys.length !== eventKeys.length
+      ) {
         changed = true
       }
       for (let i = 0; i < storeKeys.length; i++) {
         const updatedValue = stateReader(storeValues[i], scope)
         const key = storeKeys[i]
         if (!changed) {
-          if (!oldKeys.includes(key)) {
+          if (!oldStoreKeys.includes(key)) {
             changed = true
           } else {
             changed = oldVal[key] !== updatedValue
@@ -130,16 +150,32 @@ export function useUnitBase<Shape extends {[key: string]: Unit<any>}>(
         }
         resultValue[key] = updatedValue
       }
+      for (let i = 0; i < eventKeys.length; i++) {
+        const updatedValue = eventValues[i]
+        const key = eventKeys[i]
+        if (!changed) {
+          if (!oldEventKeys.includes(key)) {
+            changed = true
+          } else {
+            changed = oldEventValues[oldEventKeys.indexOf(key)] !== updatedValue
+          }
+        }
+        resultValue[key] = scope
+          ? scopeBind(updatedValue as Event<any>, {scope})
+          : updatedValue
+      }
     }
     if (changed) {
       state.value = resultValue
     }
     state.storeKeys = storeKeys
+    state.eventKeys = eventKeys
+    state.eventValues = eventValues
     flags.stale = false
     flags.justSubscribed = !changed
     flags.scope = scope
     return isSingleUnit ? state.value.unit : state.value
-  }, [subscribe, storeValues, scope, stateRef, flagsRef])
+  }, [subscribe, storeValues, eventValues, scope, stateRef, flagsRef])
   return useSyncExternalStore(subscribe, read, read)
 }
 


Test patch:
diff --git a/src/react/__tests__/base/useUnit.test.tsx b/src/react/__tests__/base/useUnit.test.tsx
index c7042d55..04bd28d3 100644
--- a/src/react/__tests__/base/useUnit.test.tsx
+++ b/src/react/__tests__/base/useUnit.test.tsx
@@ -473,6 +473,43 @@ describe('useUnit', () => {
       ]
     `)
   })
+  it('should support dynamic change of event', async () => {
+    const fnL = jest.fn()
+    const fnR = jest.fn()
+    const left = createEvent()
+    const right = createEvent()
+    left.watch(() => fnL())
+    right.watch(() => fnR())
+
+    const View = () => {
+      const [which, setWhich] = React.useState(true)
+      const event = which ? left : right
+      const testUnit = useUnit(event)
+      const onSwitch = () => setWhich(!which)
+      return (
+        <div>
+          <button id="run" onClick={testUnit}>
+            run
+          </button>
+          <button id="switch" onClick={onSwitch}>
+            Switch
+          </button>
+        </div>
+      )
+    }
+    await render(<View />)
+    await act(async () => {
+      container.firstChild.querySelector('#run').click()
+    })
+    await act(async () => {
+      container.firstChild.querySelector('#switch').click()
+    })
+    await act(async () => {
+      container.firstChild.querySelector('#run').click()
+    })
+    expect(fnL).toBeCalledTimes(1)
+    expect(fnR).toBeCalledTimes(1)
+  })
   it('should support dynamic change of store positions', async () => {
     const upA = createEvent()
     const upB = createEvent()