react-native#unstable_batchedUpdates JavaScript Examples

The following examples show how to use react-native#unstable_batchedUpdates. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: batch-integration.js    From Learning-Redux with MIT License 4 votes vote down vote up
describe('React Native', () => {
  const propMapper = (prop) => {
    switch (typeof prop) {
      case 'object':
      case 'boolean':
        return JSON.stringify(prop)
      case 'function':
        return '[function ' + prop.name + ']'
      default:
        return prop
    }
  }
  class Passthrough extends Component {
    render() {
      return (
        <View>
          {Object.keys(this.props).map((prop) => (
            <View title="prop" testID={prop} key={prop}>
              {propMapper(this.props[prop])}
            </View>
          ))}
        </View>
      )
    }
  }

  function stringBuilder(prev = '', action) {
    return action.type === 'APPEND' ? prev + action.body : prev
  }

  afterEach(() => rtl.cleanup())

  describe('batch', () => {
    it('batch should be RN unstable_batchedUpdates', () => {
      expect(batch).toBe(unstable_batchedUpdates)
    })
  })

  describe('useIsomorphicLayoutEffect', () => {
    it('useIsomorphicLayoutEffect should be useLayoutEffect', () => {
      expect(useIsomorphicLayoutEffect).toBe(useLayoutEffect)
    })
  })

  describe('Subscription and update timing correctness', () => {
    it('should pass state consistently to mapState', () => {
      const store = createStore(stringBuilder)

      rtl.act(() => {
        store.dispatch({ type: 'APPEND', body: 'a' })
      })

      let childMapStateInvokes = 0

      @connect((state) => ({ state }))
      class Container extends Component {
        emitChange() {
          store.dispatch({ type: 'APPEND', body: 'b' })
        }

        render() {
          return (
            <View>
              <Button
                title="change"
                testID="change-button"
                onPress={this.emitChange.bind(this)}
              />
              <ChildContainer parentState={this.props.state} />
            </View>
          )
        }
      }

      const childCalls = []
      @connect((state, parentProps) => {
        childMapStateInvokes++
        childCalls.push([state, parentProps.parentState])
        // The state from parent props should always be consistent with the current state
        expect(state).toEqual(parentProps.parentState)
        return {}
      })
      class ChildContainer extends Component {
        render() {
          return <Passthrough {...this.props} />
        }
      }

      const tester = rtl.render(
        <ProviderMock store={store}>
          <Container />
        </ProviderMock>
      )

      expect(childMapStateInvokes).toBe(1)
      expect(childCalls).toEqual([['a', 'a']])

      rtl.act(() => {
        store.dispatch({ type: 'APPEND', body: 'c' })
      })
      expect(childMapStateInvokes).toBe(2)
      expect(childCalls).toEqual([
        ['a', 'a'],
        ['ac', 'ac'],
      ])

      // setState calls DOM handlers are batched
      const button = tester.getByTestId('change-button')
      rtl.fireEvent.press(button)
      expect(childMapStateInvokes).toBe(3)

      rtl.act(() => {
        store.dispatch({ type: 'APPEND', body: 'd' })
      })

      expect(childMapStateInvokes).toBe(4)
      expect(childCalls).toEqual([
        ['a', 'a'],
        ['ac', 'ac'],
        ['acb', 'acb'],
        ['acbd', 'acbd'],
      ])
    })

    it('should invoke mapState always with latest props', () => {
      // Explicitly silence "not wrapped in act()" messages for this test
      const spy = jest.spyOn(console, 'error')
      spy.mockImplementation(() => {})

      const store = createStore((state = 0) => state + 1)

      let propsPassedIn

      @connect((reduxCount) => {
        return { reduxCount }
      })
      class InnerComponent extends Component {
        render() {
          propsPassedIn = this.props
          return <Passthrough {...this.props} />
        }
      }

      class OuterComponent extends Component {
        constructor() {
          super()
          this.state = { count: 0 }
        }

        render() {
          return <InnerComponent {...this.state} />
        }
      }

      let outerComponent
      rtl.render(
        <ProviderMock store={store}>
          <OuterComponent ref={(c) => (outerComponent = c)} />
        </ProviderMock>
      )
      outerComponent.setState(({ count }) => ({ count: count + 1 }))
      store.dispatch({ type: '' })

      expect(propsPassedIn.count).toEqual(1)
      expect(propsPassedIn.reduxCount).toEqual(2)

      spy.mockRestore()
    })

    it('should use the latest props when updated between actions', () => {
      // Explicitly silence "not wrapped in act()" messages for this test
      const spy = jest.spyOn(console, 'error')
      spy.mockImplementation(() => {})

      const reactCallbackMiddleware = (store) => {
        let callback

        return (next) => (action) => {
          if (action.type === 'SET_COMPONENT_CALLBACK') {
            callback = action.payload
          }

          if (callback && action.type === 'INC1') {
            // Deliberately create multiple updates of different types in a row:
            // 1) Store update causes subscriber notifications
            next(action)
            // 2) React setState outside batching causes a sync re-render.
            //    Because we're not using `act()`, this won't flush pending passive effects,
            //    simulating
            callback()
            // 3) Second dispatch causes subscriber notifications again. If `connect` is working
            //    correctly, nested subscriptions won't execute until the parents have rendered,
            //    to ensure that the subscriptions have access to the latest wrapper props.
            store.dispatch({ type: 'INC2' })
            return
          }

          next(action)
        }
      }

      const counter = (state = 0, action) => {
        if (action.type === 'INC1') {
          return state + 1
        } else if (action.type === 'INC2') {
          return state + 2
        }
        return state
      }

      const store = createStore(
        counter,
        applyMiddleware(reactCallbackMiddleware)
      )

      const Child = connect((count) => ({ count }))(function (props) {
        return (
          <View>
            <Text testID="child-prop">{props.prop}</Text>
            <Text testID="child-count">{props.count}</Text>
          </View>
        )
      })
      class Parent extends Component {
        constructor() {
          super()
          this.state = {
            prop: 'a',
          }
          this.inc1 = () => store.dispatch({ type: 'INC1' })
          store.dispatch({
            type: 'SET_COMPONENT_CALLBACK',
            payload: () => this.setState({ prop: 'b' }),
          })
        }

        render() {
          return (
            <ProviderMock store={store}>
              <Child prop={this.state.prop} />
            </ProviderMock>
          )
        }
      }

      let parent
      const rendered = rtl.render(<Parent ref={(ref) => (parent = ref)} />)
      expect(rendered.getByTestId('child-count').children).toEqual(['0'])
      expect(rendered.getByTestId('child-prop').children).toEqual(['a'])

      // Force the multi-update sequence by running this bound action creator
      parent.inc1()

      // The connected child component _should_ have rendered with the latest Redux
      // store value (3) _and_ the latest wrapper prop ('b').
      expect(rendered.getByTestId('child-count')).toHaveTextContent('3')
      expect(rendered.getByTestId('child-prop')).toHaveTextContent('b')

      spy.mockRestore()
    })

    it('should invoke mapState always with latest store state', () => {
      // Explicitly silence "not wrapped in act()" messages for this test
      const spy = jest.spyOn(console, 'error')
      spy.mockImplementation(() => {})
      const store = createStore((state = 0) => state + 1)

      let reduxCountPassedToMapState

      @connect((reduxCount) => {
        reduxCountPassedToMapState = reduxCount
        return reduxCount < 2 ? { a: 'a' } : { a: 'b' }
      })
      class InnerComponent extends Component {
        render() {
          return <Passthrough {...this.props} />
        }
      }

      class OuterComponent extends Component {
        constructor() {
          super()
          this.state = { count: 0 }
        }

        render() {
          return <InnerComponent {...this.state} />
        }
      }

      let outerComponent
      rtl.render(
        <ProviderMock store={store}>
          <OuterComponent ref={(c) => (outerComponent = c)} />
        </ProviderMock>
      )

      store.dispatch({ type: '' })
      store.dispatch({ type: '' })
      outerComponent.setState(({ count }) => ({ count: count + 1 }))

      expect(reduxCountPassedToMapState).toEqual(3)

      spy.mockRestore()
    })

    it('should ensure top-down updates for consecutive batched updates', () => {
      const INC = 'INC'
      const reducer = (c = 0, { type }) => (type === INC ? c + 1 : c)
      const store = createStore(reducer)

      let executionOrder = []
      let expectedExecutionOrder = [
        'parent map',
        'parent render',
        'child map',
        'child render',
      ]

      const ChildImpl = () => {
        executionOrder.push('child render')
        return <View>child</View>
      }

      const Child = connect((state) => {
        executionOrder.push('child map')
        return { state }
      })(ChildImpl)

      const ParentImpl = () => {
        executionOrder.push('parent render')
        return <Child />
      }

      const Parent = connect((state) => {
        executionOrder.push('parent map')
        return { state }
      })(ParentImpl)

      rtl.render(
        <ProviderMock store={store}>
          <Parent />
        </ProviderMock>
      )

      executionOrder = []
      rtl.act(() => {
        store.dispatch({ type: INC })
        store.dispatch({ type: '' })
      })

      expect(executionOrder).toEqual(expectedExecutionOrder)
    })
  })

  describe('useSelector', () => {
    it('should stay in sync with the store', () => {
      // https://github.com/reduxjs/react-redux/issues/1437

      jest.useFakeTimers()

      // Explicitly silence "not wrapped in act()" messages for this test
      const spy = jest.spyOn(console, 'error')
      spy.mockImplementation(() => {})

      const INIT_STATE = { bool: false }

      const reducer = (state = INIT_STATE, action) => {
        switch (action.type) {
          case 'TOGGLE':
            return { bool: !state.bool }
          default:
            return state
        }
      }

      const store = createStore(reducer, INIT_STATE)

      const selector = (state) => ({
        bool: state.bool,
      })

      const ReduxBugParent = () => {
        const dispatch = useDispatch()
        const { bool } = useSelector(selector)
        const boolFromStore = store.getState().bool

        expect(boolFromStore).toBe(bool)

        return (
          <>
            <Button
              title="Click Me"
              testID="standardBatching"
              onPress={() => {
                dispatch({ type: 'NOOP' })
                dispatch({ type: 'TOGGLE' })
              }}
            />
            <Button
              title="[BUG] Click Me (setTimeout)"
              testID="setTimeout"
              onPress={() => {
                setTimeout(() => {
                  dispatch({ type: 'NOOP' })
                  dispatch({ type: 'TOGGLE' })
                }, 0)
              }}
            />
            <Button
              title="Click Me (setTimeout & batched from react-native)"
              testID="unstableBatched"
              onPress={() => {
                setTimeout(() => {
                  unstable_batchedUpdates(() => {
                    dispatch({ type: 'NOOP' })
                    dispatch({ type: 'TOGGLE' })
                  })
                }, 0)
              }}
            />
            <Button
              title="Click Me (setTimeout & batched from react-redux)"
              testID="reactReduxBatch"
              onPress={() => {
                setTimeout(() => {
                  batch(() => {
                    dispatch({ type: 'NOOP' })
                    dispatch({ type: 'TOGGLE' })
                  })
                }, 0)
              }}
            />
            <Text testID="boolFromSelector">
              bool from useSelector is {JSON.stringify(bool)}
            </Text>
            <Text testID="boolFromStore">
              bool from store.getState is {JSON.stringify(boolFromStore)}
            </Text>

            {bool !== boolFromStore && <Text>They are not same!</Text>}
          </>
        )
      }

      const ReduxBugDemo = () => {
        return (
          <ProviderMock store={store}>
            <ReduxBugParent />
          </ProviderMock>
        )
      }

      const rendered = rtl.render(<ReduxBugDemo />)

      const assertValuesMatch = (rendered) => {
        const [, boolFromSelector] =
          rendered.getByTestId('boolFromSelector').children
        const [, boolFromStore] = rendered.getByTestId('boolFromStore').children
        expect(boolFromSelector).toBe(boolFromStore)
      }

      const clickButton = (rendered, testID) => {
        const button = rendered.getByTestId(testID)
        rtl.fireEvent.press(button)
      }

      const clickAndRender = (rendered, testID) => {
        // Note: Normally we'd wrap this all in act(), but that automatically
        // wraps your code in batchedUpdates(). The point of this bug is that it
        // specifically occurs when you are _not_ batching updates!
        clickButton(rendered, 'setTimeout')
        jest.advanceTimersByTime(100)
        assertValuesMatch(rendered, testID)
      }

      assertValuesMatch(rendered)

      clickAndRender(rendered, 'setTimeout')
      clickAndRender(rendered, 'standardBatching')
      clickAndRender(rendered, 'unstableBatched')
      clickAndRender(rendered, 'reactReduxBatch')

      spy.mockRestore()
    })
  })
})