Skip to content

Commit

Permalink
Fix RN batching and effect behavior (#1444)
Browse files Browse the repository at this point in the history
* Add React Native deps for testing

* Add a Jest config to run tests in an RN environment

* Add initial RN tests

* Add test for RN batch export

* Add jest-native testing assertions

* Extract useIsomorphicLayoutEffect utility

* Add additional RN batching-related tests

* 7.1.2-alpha.0
  • Loading branch information
markerikson committed Nov 6, 2019
1 parent b5034a6 commit b832f83
Show file tree
Hide file tree
Showing 8 changed files with 3,699 additions and 100 deletions.
29 changes: 29 additions & 0 deletions jest.config.js
@@ -0,0 +1,29 @@
const defaults = {
coverageDirectory: './coverage/',
collectCoverage: true,
testURL: 'http://localhost'
}

const testFolderPath = folderName => `<rootDir>/test/${folderName}/**/*.js`

const NORMAL_TEST_FOLDERS = ['components', 'hooks', 'integration', 'utils']

const standardConfig = {
...defaults,
displayName: 'ReactDOM',
testMatch: NORMAL_TEST_FOLDERS.map(testFolderPath)
}

const rnConfig = {
...defaults,
displayName: 'React Native',
testMatch: [testFolderPath('react-native')],
preset: 'react-native',
transform: {
'^.+\\.js$': '<rootDir>/node_modules/react-native/jest/preprocessor.js'
}
}

module.exports = {
projects: [standardConfig, rnConfig]
}
3,190 changes: 3,139 additions & 51 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 4 additions & 6 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "react-redux",
"version": "7.1.1",
"version": "7.1.2-alpha.0",
"description": "Official React bindings for Redux",
"keywords": [
"react",
Expand Down Expand Up @@ -65,8 +65,10 @@
"@babel/plugin-transform-runtime": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@testing-library/jest-dom": "^4.1.0",
"@testing-library/jest-native": "^3.0.2",
"@testing-library/react": "^8.0.8",
"@testing-library/react-hooks": "^1.1.0",
"@testing-library/react-native": "^4.2.0",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"codecov": "^3.5.0",
Expand All @@ -83,6 +85,7 @@
"prettier": "^1.18.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-native": "^0.61.4",
"react-test-renderer": "^16.8.6",
"redux": "^4.0.4",
"rimraf": "^3.0.0",
Expand All @@ -97,10 +100,5 @@
"transform": [
"loose-envify"
]
},
"jest": {
"coverageDirectory": "./coverage/",
"collectCoverage": true,
"testURL": "http://localhost"
}
}
22 changes: 2 additions & 20 deletions src/components/connectAdvanced.js
@@ -1,15 +1,9 @@
import hoistStatics from 'hoist-non-react-statics'
import invariant from 'invariant'
import React, {
useContext,
useMemo,
useEffect,
useLayoutEffect,
useRef,
useReducer
} from 'react'
import React, { useContext, useMemo, useRef, useReducer } from 'react'
import { isValidElementType, isContextConsumer } from 'react-is'
import Subscription from '../utils/Subscription'
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'

import { ReactReduxContext } from './Context'

Expand All @@ -32,18 +26,6 @@ function storeStateUpdatesReducer(state, action) {

const initStateUpdates = () => [null, 0]

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect because we want
// `connect` to perform sync updates to a ref to save the latest props after
// a render is actually committed to the DOM.
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' &&
typeof window.document !== 'undefined' &&
typeof window.document.createElement !== 'undefined'
? useLayoutEffect
: useEffect

export default function connectAdvanced(
/*
selectorFactory is a func that is responsible for returning the selector function used to
Expand Down
25 changes: 2 additions & 23 deletions src/hooks/useSelector.js
@@ -1,31 +1,10 @@
import {
useReducer,
useRef,
useEffect,
useMemo,
useLayoutEffect,
useContext
} from 'react'
import { useReducer, useRef, useMemo, useContext } from 'react'
import invariant from 'invariant'
import { useReduxContext as useDefaultReduxContext } from './useReduxContext'
import Subscription from '../utils/Subscription'
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
import { ReactReduxContext } from '../components/Context'

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store
// subscription callback always has the selector from the latest render commit
// available, otherwise a store update may happen between render and the effect,
// which may cause missed updates; we also must ensure the store subscription
// is created synchronously, otherwise a store update may occur before the
// subscription is created and an inconsistent state may be observed
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' &&
typeof window.document !== 'undefined' &&
typeof window.document.createElement !== 'undefined'
? useLayoutEffect
: useEffect

const refEquality = (a, b) => a === b

function useSelectorWithStoreAndSubscription(
Expand Down
19 changes: 19 additions & 0 deletions src/utils/useIsomorphicLayoutEffect.js
@@ -0,0 +1,19 @@
import { useEffect, useLayoutEffect } from 'react'

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store
// subscription callback always has the selector from the latest render commit
// available, otherwise a store update may happen between render and the effect,
// which may cause missed updates; we also must ensure the store subscription
// is created synchronously, otherwise a store update may occur before the
// subscription is created and an inconsistent state may be observed

const isHopefullyDomEnvironment =
typeof window !== 'undefined' &&
typeof window.document !== 'undefined' &&
typeof window.document.createElement !== 'undefined'

export const useIsomorphicLayoutEffect = isHopefullyDomEnvironment
? useLayoutEffect
: useEffect
5 changes: 5 additions & 0 deletions src/utils/useIsomorphicLayoutEffect.native.js
@@ -0,0 +1,5 @@
import { useLayoutEffect } from 'react'

// Under React Native, we know that we always want to use useLayoutEffect

export const useIsomorphicLayoutEffect = useLayoutEffect

0 comments on commit b832f83

Please sign in to comment.