Compare commits

...

18 Commits

Author SHA1 Message Date
Dan Abramov
f9e41e3a51 Changelog 2019-03-22 14:49:12 +00:00
Jared Palmer
fb572afc14 Add more info to invalid hook call error message (#15139)
* Add more info to invalid hook call error message

* Update other renderers + change call to action

* Update related tests for new hooks error message

* Fix lint errors
2019-03-22 14:47:58 +00:00
Dan Abramov
a0a2e846ce Update CHANGELOG.md 2019-03-22 14:37:19 +00:00
Dan Abramov
1fc13e4b35 Add 16.8.5 changelog 2019-03-22 14:34:33 +00:00
Dan Abramov
b5cb9d345c Link to useLayoutEffect gist in a warning (#15158) 2019-03-22 14:14:46 +00:00
Dan Abramov
342fa78ed4 [Shallow] Implement setState for Hooks and remount on type change (#15120)
* Throw away old shallow renderer state on type change

This worked in function components but was broken for classes. It incorrectly retained the old instance even if the type was different.

* Remove _previousComponentIdentity

We only needed this because we didn't correctly reset based on type. Now we do so this can go away.

* Use _reset when unmounting

* Use arbitrary componentIdentity

There was no particular reason it was set to element.type. We just wanted to check if something is a render phase update.

* Support Hook state updates in shallow renderer
2019-03-22 14:13:28 +00:00
Brandon Dail
62f5d4a057 Support React.memo in ReactShallowRenderer (#14816)
* Support React.memo in ReactShallowRenderer

ReactShallowRenderer uses element.type frequently, but with React.memo
elements the actual type is element.type.type. This updates
ReactShallowRenderer so it uses the correct element type for Memo
components and also validates the inner props for the wrapped
components.

* Allow Rect.memo to prevent re-renders

* Support memo(forwardRef())

* Dont call memo comparison function on initial render

* Fix test

* Small tweaks
2019-03-22 14:13:15 +00:00
Dan Abramov
6b86a6e039 Use same example code for async effect warning (#15118) 2019-03-22 14:13:05 +00:00
Sebastian Silbermann
8f7335875c Fix shallow renderer not allowing hooks in forwardRef render functions (#15100)
* test: Add test for shallow + forwardRef + hook

* fix(react-test-renderer): shallow forwardRef hooks
2019-03-22 14:12:24 +00:00
Mateusz
d822d4bbe7 Don't set the first option as selected in select tag with size attribute (#14242)
* Set 'size' attribute to select tag if it occurs before appending options

* Add comment about why size is assigned on select create. Tests

I added some more clarification for why size must be set on select
element creation:

- In the source code
- In the DOM test fixture
- In a unit test

* Use let, not const in select tag stub assignment
2019-03-22 14:12:11 +00:00
Dan Abramov
13a3788c54 Improve async useEffect warning (#15104) 2019-03-22 14:11:22 +00:00
Brian Vaughn
d8a73b5eb6 16.8.4 and changelog 2019-03-05 15:16:01 -08:00
Brian Vaughn
741aa17a33 Turning off eslint-plugin-react-hooks for the upcoming patch release 2019-03-05 14:35:53 -08:00
Brian Vaughn
95c2b49543 Cherry picked 4186952 from master, but with manually resolved conflicts.
Original commit: Fixed incompatibility between react-debug-tools and useContext() (#14940)
* Refactor hook ordering check to use DEV-only data structure. This enables us to warn about more cases (e.g. useContext, useDebugValue) withou the need to add any overhead to production bundles.
2019-03-05 14:29:04 -08:00
Dan Abramov
55cf14f98e Release 16.8.3 2019-03-05 14:09:10 -08:00
Dan Abramov
29b7b775f2 Fix UMD builds by re-exporting the scheduler priorities (#14914) 2019-02-21 17:21:38 +00:00
overlookmotel
b668168d4d Fix react-dom/server context leaks when render stream destroyed early (#14706)
* Fix react-dom/server context memory retention

* Test for pollution of later renders

* Inline loop

* More tests
2019-02-20 11:25:02 -08:00
Dan Abramov
619cdfc624 Don't discard render phase state updates with the eager reducer optimization (#14852)
* Add test cases for setState(fn) + render phase updates

* Update eager state and reducer for render phase updates

* Fix a newly firing warning
2019-02-20 11:24:49 -08:00
37 changed files with 3099 additions and 333 deletions

View File

@@ -6,6 +6,40 @@
</summary>
</details>
## 16.8.5 (March 22, 2019)
### React DOM
* Don't set the first option as selected in select tag with `size` attribute. ([@kulek1](https://github.com/kulek1) in [#14242](https://github.com/facebook/react/pull/14242))
* Improve the `useEffect(async () => ...)` warning message. ([@gaearon](https://github.com/gaearon) in [#15118](https://github.com/facebook/react/pull/15118))
* Improve the error message sometimes caused by duplicate React. ([@jaredpalmer](https://github.com/jaredpalmer) in [#15139](https://github.com/facebook/react/pull/15139))
### React DOM Server
* Improve the `useLayoutEffect` warning message when server rendering. ([@gaearon](https://github.com/gaearon) in [#15158](https://github.com/facebook/react/pull/15158))
### React Shallow Renderer
* Fix `setState` in shallow renderer to work with Hooks. ([@gaearon](https://github.com/gaearon) in [#15120](https://github.com/facebook/react/pull/15120))
* Fix shallow renderer to support `React.memo`. ([@aweary](https://github.com/aweary) in [#14816](https://github.com/facebook/react/pull/14816))
* Fix shallow renderer to support Hooks inside `forwardRef`. ([@eps1lon](https://github.com/eps1lon) in [#15100](https://github.com/facebook/react/pull/15100))
## 16.8.4 (March 5, 2019)
### React DOM and other renderers
- Fix a bug where DevTools caused a runtime error when inspecting a component that used a `useContext` hook. ([@bvaughn](https://github.com/bvaughn) in [#14940](https://github.com/facebook/react/pull/14940))
## 16.8.3 (February 21, 2019)
### React DOM
- Fix a bug that caused inputs to behave incorrectly in UMD builds. ([@gaearon](https://github.com/gaearon) in [#14914](https://github.com/facebook/react/pull/14914))
- Fix a bug that caused render phase updates to be discarded. ([@gaearon](https://github.com/gaearon) in [#14852](https://github.com/facebook/react/pull/14852))
### React DOM Server
- Unwind the context stack when a stream is destroyed without completing, to prevent incorrect values during a subsequent render. ([@overlookmotel](https://github.com/overlookmotel) in [#14706](https://github.com/facebook/react/pull/14706/))
## 16.8.2 (February 14, 2019)
### React DOM

View File

@@ -202,6 +202,34 @@ class SelectFixture extends React.Component {
</select>
</div>
</TestCase>
<TestCase
title="A select with the size attribute should not set first option as selected"
relatedIssues="14239"
introducedIn="16.0.0">
<TestCase.ExpectedResult>
No options should be selected.
</TestCase.ExpectedResult>
<div className="test-fixture">
<select size="3">
<option>0</option>
<option>1</option>
<option>2</option>
</select>
</div>
<p className="footnote">
<b>Notes:</b> This happens if <code>size</code> is assigned after
options are selected. The select element picks the first item by
default, then it is expanded to show more options when{' '}
<code>size</code> is assigned, preserving the default selection.
</p>
<p className="footnote">
This was introduced in React 16.0.0 when options were added before
select attribute assignment.
</p>
</TestCase>
</FixtureSet>
);
}

View File

@@ -1,7 +1,7 @@
{
"name": "create-subscription",
"description": "utility for subscribing to external data sources inside React components",
"version": "16.8.2",
"version": "16.8.4",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",

View File

@@ -1,7 +1,8 @@
{
"private": true,
"name": "eslint-plugin-react-hooks",
"description": "ESLint rules for React Hooks",
"version": "1.0.2",
"version": "1.1.0",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",

View File

@@ -1,6 +1,6 @@
{
"name": "jest-react",
"version": "0.6.2",
"version": "0.6.4",
"description": "Jest matchers and utilities for testing React components.",
"main": "index.js",
"repository": {

View File

@@ -1,7 +1,7 @@
{
"name": "react-art",
"description": "React ART is a JavaScript library for drawing vector graphics using React. It provides declarative and reactive bindings to the ART library. Using the same declarative API you can render the output to either Canvas, SVG or VML (IE8).",
"version": "16.8.2",
"version": "16.8.4",
"main": "index.js",
"repository": {
"type": "git",
@@ -27,7 +27,7 @@
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.13.2"
"scheduler": "^0.13.4"
},
"peerDependencies": {
"react": "^16.0.0"

View File

@@ -239,7 +239,12 @@ describe('ReactHooksInspection', () => {
expect(() => {
ReactDebugTools.inspectHooks(Foo, {}, FakeDispatcherRef);
}).toThrow(
'Hooks can only be called inside the body of a function component.',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
expect(getterCalls).toBe(1);

View File

@@ -419,7 +419,12 @@ describe('ReactHooksInspectionIntegration', () => {
expect(() => {
ReactDebugTools.inspectHooksOfFiber(childFiber, FakeDispatcherRef);
}).toThrow(
'Hooks can only be called inside the body of a function component.',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
expect(getterCalls).toBe(1);
@@ -427,4 +432,42 @@ describe('ReactHooksInspectionIntegration', () => {
expect(setterCalls[0]).not.toBe(initial);
expect(setterCalls[1]).toBe(initial);
});
// This test case is based on an open source bug report:
// facebookincubator/redux-react-hook/issues/34#issuecomment-466693787
it('should properly advance the current hook for useContext', () => {
const MyContext = React.createContext(1);
let incrementCount;
function Foo(props) {
const context = React.useContext(MyContext);
const [data, setData] = React.useState({count: context});
incrementCount = () => setData(({count}) => ({count: count + 1}));
return <div>count: {data.count}</div>;
}
const renderer = ReactTestRenderer.create(<Foo />);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['count: ', '1'],
});
act(incrementCount);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['count: ', '2'],
});
const childFiber = renderer.root._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{name: 'Context', value: 1, subHooks: []},
{name: 'State', value: {count: 2}, subHooks: []},
]);
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "react-dom",
"version": "16.8.2",
"version": "16.8.4",
"description": "React package for working with the DOM.",
"main": "index.js",
"repository": {
@@ -20,7 +20,7 @@
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.13.2"
"scheduler": "^0.13.4"
},
"peerDependencies": {
"react": "^16.0.0"

View File

@@ -362,6 +362,32 @@ describe('ReactDOMSelect', () => {
expect(node.options[2].selected).toBe(true); // gorilla
});
it('does not select an item when size is initially set to greater than 1', () => {
const stub = (
<select size="2">
<option value="monkey">A monkey!</option>
<option value="giraffe">A giraffe!</option>
<option value="gorilla">A gorilla!</option>
</select>
);
const container = document.createElement('div');
const select = ReactDOM.render(stub, container);
expect(select.options[0].selected).toBe(false);
expect(select.options[1].selected).toBe(false);
expect(select.options[2].selected).toBe(false);
// Note: There is an inconsistency between JSDOM and Chrome where
// Chrome reports an empty string when no value is selected for a
// single-select with a size greater than 0. JSDOM reports the first
// value
//
// This assertion exists only for clarity of JSDOM behavior:
expect(select.value).toBe('monkey'); // "" in Chrome
// Despite this, the selection index is correct:
expect(select.selectedIndex).toBe(-1);
});
it('should remember value when switching to uncontrolled', () => {
let stub = (
<select value={'giraffe'} onChange={noop}>

View File

@@ -144,7 +144,12 @@ describe('ReactDOMServerHooks', () => {
return render(<Counter />);
},
'Hooks can only be called inside the body of a function component.',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
itRenders('multiple times when an updater is called', async render => {
@@ -626,7 +631,12 @@ describe('ReactDOMServerHooks', () => {
return render(<Counter />);
},
'Hooks can only be called inside the body of a function component.',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
});

View File

@@ -482,5 +482,98 @@ describe('ReactDOMServerIntegration', () => {
);
}
});
// Regression test for https://github.com/facebook/react/issues/14705
it('does not pollute later renders when stream destroyed', () => {
const LoggedInUser = React.createContext('default');
const AppWithUser = user => (
<LoggedInUser.Provider value={user}>
<header>
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
</header>
</LoggedInUser.Provider>
);
const stream = ReactDOMServer.renderToNodeStream(
AppWithUser('Amy'),
).setEncoding('utf8');
// This is an implementation detail because we test a memory leak
const {threadID} = stream.partialRenderer;
// Read enough to render Provider but not enough for it to be exited
stream._read(10);
expect(LoggedInUser[threadID]).toBe('Amy');
stream.destroy();
const AppWithUserNoProvider = () => (
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
);
const stream2 = ReactDOMServer.renderToNodeStream(
AppWithUserNoProvider(),
).setEncoding('utf8');
// Sanity check to ensure 2nd render has same threadID as 1st render,
// otherwise this test is not testing what it's meant to
expect(stream2.partialRenderer.threadID).toBe(threadID);
const markup = stream2.read(Infinity);
expect(markup).toBe('default');
});
// Regression test for https://github.com/facebook/react/issues/14705
it('frees context value reference when stream destroyed', () => {
const LoggedInUser = React.createContext('default');
const AppWithUser = user => (
<LoggedInUser.Provider value={user}>
<header>
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
</header>
</LoggedInUser.Provider>
);
const stream = ReactDOMServer.renderToNodeStream(
AppWithUser('Amy'),
).setEncoding('utf8');
// This is an implementation detail because we test a memory leak
const {threadID} = stream.partialRenderer;
// Read enough to render Provider but not enough for it to be exited
stream._read(10);
expect(LoggedInUser[threadID]).toBe('Amy');
stream.destroy();
expect(LoggedInUser[threadID]).toBe('default');
});
it('does not pollute sync renders after an error', () => {
const LoggedInUser = React.createContext('default');
const Crash = () => {
throw new Error('Boo!');
};
const AppWithUser = user => (
<LoggedInUser.Provider value={user}>
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
<Crash />
</LoggedInUser.Provider>
);
expect(() => {
ReactDOMServer.renderToString(AppWithUser('Casper'));
}).toThrow('Boo');
// Should not report a value from failed render
expect(
ReactDOMServer.renderToString(
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>,
),
).toBe('default');
});
});
});

View File

@@ -419,14 +419,25 @@ export function createElement(
// See discussion in https://github.com/facebook/react/pull/6896
// and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
domElement = ownerDocument.createElement(type);
// Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple`
// attribute on `select`s needs to be added before `option`s are inserted. This prevents
// a bug where the `select` does not scroll to the correct option because singular
// `select` elements automatically pick the first item.
// Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` and `size`
// attributes on `select`s needs to be added before `option`s are inserted.
// This prevents:
// - a bug where the `select` does not scroll to the correct option because singular
// `select` elements automatically pick the first item #13222
// - a bug where the `select` set the first item as selected despite the `size` attribute #14239
// See https://github.com/facebook/react/issues/13222
if (type === 'select' && props.multiple) {
// and https://github.com/facebook/react/issues/14239
if (type === 'select') {
const node = ((domElement: any): HTMLSelectElement);
node.multiple = true;
if (props.multiple) {
node.multiple = true;
} else if (props.size) {
// Setting a size greater than 1 causes a select to behave like `multiple=true`, where
// it is possible that no option is selected.
//
// This is only necessary when a select in "single selection mode".
node.size = props.size;
}
}
}
} else {

View File

@@ -715,6 +715,7 @@ class ReactDOMServerRenderer {
destroy() {
if (!this.exhausted) {
this.exhausted = true;
this.clearProviders();
freeThreadID(this.threadID);
}
}
@@ -776,6 +777,15 @@ class ReactDOMServerRenderer {
context[this.threadID] = previousValue;
}
clearProviders(): void {
// Restore any remaining providers on the stack to previous values
for (let index = this.contextIndex; index >= 0; index--) {
const context: ReactContext<any> = this.contextStack[index];
const previousValue = this.contextValueStack[index];
context[this.threadID] = previousValue;
}
}
read(bytes: number): string | null {
if (this.exhausted) {
return null;

View File

@@ -57,8 +57,12 @@ let currentHookNameInDev: ?string;
function resolveCurrentlyRenderingComponent(): Object {
invariant(
currentlyRenderingComponent !== null,
'Hooks can only be called inside the body of a function component. ' +
'(https://fb.me/react-invalid-hook-call)',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
if (__DEV__) {
warning(
@@ -389,7 +393,8 @@ export function useLayoutEffect(
"be encoded into the server renderer's output format. This will lead " +
'to a mismatch between the initial, non-hydrated UI and the intended ' +
'UI. To avoid this, useLayoutEffect should only be used in ' +
'components that render exclusively on the client.',
'components that render exclusively on the client. ' +
'See https://fb.me/react-uselayouteffect-ssr for common fixes.',
);
}

View File

@@ -1,6 +1,6 @@
{
"name": "react-is",
"version": "16.8.2",
"version": "16.8.4",
"description": "Brand checking of React Elements.",
"main": "index.js",
"repository": {

View File

@@ -1,7 +1,7 @@
{
"name": "react-reconciler",
"description": "React package for creating custom renderers.",
"version": "0.20.0",
"version": "0.20.2",
"keywords": [
"react"
],
@@ -33,7 +33,7 @@
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.13.2"
"scheduler": "^0.13.4"
},
"browserify": {
"transform": [

View File

@@ -15,6 +15,7 @@ import type {SideEffectTag} from 'shared/ReactSideEffectTags';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {UpdateQueue} from './ReactUpdateQueue';
import type {ContextDependencyList} from './ReactFiberNewContext';
import type {HookType} from './ReactFiberHooks';
import invariant from 'shared/invariant';
import warningWithoutStack from 'shared/warningWithoutStack';
@@ -204,6 +205,9 @@ export type Fiber = {|
_debugSource?: Source | null,
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,
// Used to verify that the order of hooks does not change between renders.
_debugHookTypes?: Array<HookType> | null,
|};
let debugCounter;
@@ -285,6 +289,7 @@ function FiberNode(
this._debugSource = null;
this._debugOwner = null;
this._debugIsCurrentlyTiming = false;
this._debugHookTypes = null;
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
Object.preventExtensions(this);
}
@@ -370,6 +375,7 @@ export function createWorkInProgress(
workInProgress._debugID = current._debugID;
workInProgress._debugSource = current._debugSource;
workInProgress._debugOwner = current._debugOwner;
workInProgress._debugHookTypes = current._debugHookTypes;
}
workInProgress.alternate = current;
@@ -723,5 +729,6 @@ export function assignFiberPropertiesInDEV(
target._debugSource = source._debugSource;
target._debugOwner = source._debugOwner;
target._debugIsCurrentlyTiming = source._debugIsCurrentlyTiming;
target._debugHookTypes = source._debugHookTypes;
return target;
}

View File

@@ -346,22 +346,23 @@ function commitHookEffectList(
} else if (typeof destroy.then === 'function') {
addendum =
'\n\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' +
'Instead, you may write an async function separately ' +
'and then call it from inside the effect:\n\n' +
'async function fetchComment(commentId) {\n' +
' // You can await here\n' +
'}\n\n' +
'Instead, write the async function inside your effect ' +
'and call it immediately:\n\n' +
'useEffect(() => {\n' +
' fetchComment(commentId);\n' +
'}, [commentId]);\n\n' +
'In the future, React will provide a more idiomatic solution for data fetching ' +
"that doesn't involve writing effects manually.";
' async function fetchData() {\n' +
' // You can await here\n' +
' const response = await MyAPI.getData(someId);\n' +
' // ...\n' +
' }\n' +
' fetchData();\n' +
`}, [someId]); // Or [] if effect doesn't need props or state\n\n` +
'Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching';
} else {
addendum = ' You returned: ' + destroy;
}
warningWithoutStack(
false,
'An Effect function must not return anything besides a function, ' +
'An effect function must not return anything besides a function, ' +
'which is used for clean-up.%s%s',
addendum,
getStackByFiberInDevAndProd(finishedWork),

View File

@@ -93,7 +93,7 @@ type UpdateQueue<S, A> = {
eagerState: S | null,
};
type HookType =
export type HookType =
| 'useState'
| 'useReducer'
| 'useContext'
@@ -120,10 +120,6 @@ export type Hook = {
next: Hook | null,
};
type HookDev = Hook & {
_debugType: HookType,
};
type Effect = {
tag: HookEffectTag,
create: () => (() => void) | void,
@@ -150,7 +146,6 @@ let currentlyRenderingFiber: Fiber | null = null;
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let firstCurrentHook: Hook | null = null;
let currentHook: Hook | null = null;
let nextCurrentHook: Hook | null = null;
let firstWorkInProgressHook: Hook | null = null;
@@ -183,7 +178,38 @@ const RE_RENDER_LIMIT = 25;
// In DEV, this is the name of the currently executing primitive hook
let currentHookNameInDev: ?HookType = null;
function warnOnHookMismatchInDev() {
// In DEV, this list ensures that hooks are called in the same order between renders.
// The list stores the order of hooks used during the initial render (mount).
// Subsequent renders (updates) reference this list.
let hookTypesDev: Array<HookType> | null = null;
let hookTypesUpdateIndexDev: number = -1;
function mountHookTypesDev() {
if (__DEV__) {
const hookName = ((currentHookNameInDev: any): HookType);
if (hookTypesDev === null) {
hookTypesDev = [hookName];
} else {
hookTypesDev.push(hookName);
}
}
}
function updateHookTypesDev() {
if (__DEV__) {
const hookName = ((currentHookNameInDev: any): HookType);
if (hookTypesDev !== null) {
hookTypesUpdateIndexDev++;
if (hookTypesDev[hookTypesUpdateIndexDev] !== hookName) {
warnOnHookMismatchInDev(hookName);
}
}
}
}
function warnOnHookMismatchInDev(currentHookName: HookType) {
if (__DEV__) {
const componentName = getComponentName(
((currentlyRenderingFiber: any): Fiber).type,
@@ -191,44 +217,44 @@ function warnOnHookMismatchInDev() {
if (!didWarnAboutMismatchedHooksForComponent.has(componentName)) {
didWarnAboutMismatchedHooksForComponent.add(componentName);
const secondColumnStart = 22;
if (hookTypesDev !== null) {
let table = '';
let table = '';
let prevHook: HookDev | null = (firstCurrentHook: any);
let nextHook: HookDev | null = (firstWorkInProgressHook: any);
let n = 1;
while (prevHook !== null && nextHook !== null) {
const oldHookName = prevHook._debugType;
const newHookName = nextHook._debugType;
const secondColumnStart = 30;
let row = `${n}. ${oldHookName}`;
for (let i = 0; i <= ((hookTypesUpdateIndexDev: any): number); i++) {
const oldHookName = hookTypesDev[i];
const newHookName =
i === ((hookTypesUpdateIndexDev: any): number)
? currentHookName
: oldHookName;
// Extra space so second column lines up
// lol @ IE not supporting String#repeat
while (row.length < secondColumnStart) {
row += ' ';
let row = `${i + 1}. ${oldHookName}`;
// Extra space so second column lines up
// lol @ IE not supporting String#repeat
while (row.length < secondColumnStart) {
row += ' ';
}
row += newHookName + '\n';
table += row;
}
row += newHookName + '\n';
table += row;
prevHook = (prevHook.next: any);
nextHook = (nextHook.next: any);
n++;
warning(
false,
'React has detected a change in the order of Hooks called by %s. ' +
'This will lead to bugs and errors if not fixed. ' +
'For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
' Previous render Next render\n' +
' ------------------------------------------------------\n' +
'%s' +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n',
componentName,
table,
);
}
warning(
false,
'React has detected a change in the order of Hooks called by %s. ' +
'This will lead to bugs and errors if not fixed. ' +
'For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
' Previous render Next render\n' +
' -------------------------------\n' +
'%s' +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n',
componentName,
table,
);
}
}
}
@@ -236,8 +262,12 @@ function warnOnHookMismatchInDev() {
function throwInvalidHookError() {
invariant(
false,
'Hooks can only be called inside the body of a function component. ' +
'(https://fb.me/react-invalid-hook-call)',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
}
@@ -293,8 +323,15 @@ export function renderWithHooks(
): any {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
firstCurrentHook = nextCurrentHook =
current !== null ? current.memoizedState : null;
nextCurrentHook = current !== null ? current.memoizedState : null;
if (__DEV__) {
hookTypesDev =
current !== null
? ((current._debugHookTypes: any): Array<HookType>)
: null;
hookTypesUpdateIndexDev = -1;
}
// The following should have already been reset
// currentHook = null;
@@ -308,11 +345,26 @@ export function renderWithHooks(
// numberOfReRenders = 0;
// sideEffectTag = 0;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because nextCurrentHook === null.
// This is tricky because it's valid for certain types of components (e.g. React.lazy)
// Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.
// Non-stateful hooks (e.g. context) don't get added to memoizedState,
// so nextCurrentHook would be null during updates and mounts.
if (__DEV__) {
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMountInDEV
: HooksDispatcherOnUpdateInDEV;
if (nextCurrentHook !== null) {
ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
} else if (hookTypesDev !== null) {
// This dispatcher handles an edge case where a component is updating,
// but no stateful hooks have been used.
// We want to match the production code behavior (which will use HooksDispatcherOnMount),
// but with the extra DEV validation to ensure hooks ordering hasn't changed.
// This dispatcher does that.
ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
} else {
ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
}
} else {
ReactCurrentDispatcher.current =
nextCurrentHook === null
@@ -328,14 +380,18 @@ export function renderWithHooks(
numberOfReRenders += 1;
// Start over from the beginning of the list
firstCurrentHook = nextCurrentHook =
current !== null ? current.memoizedState : null;
nextCurrentHook = current !== null ? current.memoizedState : null;
nextWorkInProgressHook = firstWorkInProgressHook;
currentHook = null;
workInProgressHook = null;
componentUpdateQueue = null;
if (__DEV__) {
// Also validate hook order for cascading updates.
hookTypesUpdateIndexDev = -1;
}
ReactCurrentDispatcher.current = __DEV__
? HooksDispatcherOnUpdateInDEV
: HooksDispatcherOnUpdate;
@@ -347,10 +403,6 @@ export function renderWithHooks(
numberOfReRenders = 0;
}
if (__DEV__) {
currentHookNameInDev = null;
}
// We can assume the previous dispatcher is always this one, since we set it
// at the beginning of the render phase and there's no re-entrancy.
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
@@ -362,19 +414,30 @@ export function renderWithHooks(
renderedWork.updateQueue = (componentUpdateQueue: any);
renderedWork.effectTag |= sideEffectTag;
if (__DEV__) {
renderedWork._debugHookTypes = hookTypesDev;
}
// This check uses currentHook so that it works the same in DEV and prod bundles.
// hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
firstCurrentHook = null;
currentHook = null;
nextCurrentHook = null;
firstWorkInProgressHook = null;
workInProgressHook = null;
nextWorkInProgressHook = null;
if (__DEV__) {
currentHookNameInDev = null;
hookTypesDev = null;
hookTypesUpdateIndexDev = -1;
}
remainingExpirationTime = NoWork;
componentUpdateQueue = null;
sideEffectTag = 0;
@@ -416,21 +479,23 @@ export function resetHooks(): void {
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
firstCurrentHook = null;
currentHook = null;
nextCurrentHook = null;
firstWorkInProgressHook = null;
workInProgressHook = null;
nextWorkInProgressHook = null;
if (__DEV__) {
hookTypesDev = null;
hookTypesUpdateIndexDev = -1;
currentHookNameInDev = null;
}
remainingExpirationTime = NoWork;
componentUpdateQueue = null;
sideEffectTag = 0;
if (__DEV__) {
currentHookNameInDev = null;
}
didScheduleRenderPhaseUpdate = false;
renderPhaseUpdates = null;
numberOfReRenders = 0;
@@ -447,9 +512,6 @@ function mountWorkInProgressHook(): Hook {
next: null,
};
if (__DEV__) {
(hook: any)._debugType = (currentHookNameInDev: any);
}
if (workInProgressHook === null) {
// This is the first hook in the list
firstWorkInProgressHook = workInProgressHook = hook;
@@ -499,13 +561,6 @@ function updateWorkInProgressHook(): Hook {
workInProgressHook = workInProgressHook.next = newHook;
}
nextCurrentHook = currentHook.next;
if (__DEV__) {
(newHook: any)._debugType = (currentHookNameInDev: any);
if (currentHookNameInDev !== ((currentHook: any): HookDev)._debugType) {
warnOnHookMismatchInDev();
}
}
}
return workInProgressHook;
}
@@ -520,26 +575,6 @@ function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
function mountContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
): T {
if (__DEV__) {
mountWorkInProgressHook();
}
return readContext(context, observedBits);
}
function updateContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
): T {
if (__DEV__) {
updateWorkInProgressHook();
}
return readContext(context, observedBits);
}
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
@@ -607,7 +642,6 @@ function updateReducer<S, I, A>(
}
hook.memoizedState = newState;
// Don't persist the state accumlated from the render phase updates to
// the base state unless the queue is empty.
// TODO: Not sure if this is the desired semantics, but it's what we
@@ -616,6 +650,9 @@ function updateReducer<S, I, A>(
hook.baseState = newState;
}
queue.eagerReducer = reducer;
queue.eagerState = newState;
return [newState, dispatch];
}
}
@@ -1179,6 +1216,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
};
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
let HooksDispatcherOnUpdateInDEV: Dispatcher | null = null;
let InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher | null = null;
let InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher | null = null;
@@ -1214,6 +1252,7 @@ if (__DEV__) {
useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useCallback';
mountHookTypesDev();
return mountCallback(callback, deps);
},
useContext<T>(
@@ -1221,13 +1260,15 @@ if (__DEV__) {
observedBits: void | number | boolean,
): T {
currentHookNameInDev = 'useContext';
return mountContext(context, observedBits);
mountHookTypesDev();
return readContext(context, observedBits);
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useEffect';
mountHookTypesDev();
return mountEffect(create, deps);
},
useImperativeHandle<T>(
@@ -1236,6 +1277,7 @@ if (__DEV__) {
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useImperativeHandle';
mountHookTypesDev();
return mountImperativeHandle(ref, create, deps);
},
useLayoutEffect(
@@ -1243,10 +1285,12 @@ if (__DEV__) {
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useLayoutEffect';
mountHookTypesDev();
return mountLayoutEffect(create, deps);
},
useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useMemo';
mountHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
@@ -1261,6 +1305,7 @@ if (__DEV__) {
init?: I => S,
): [S, Dispatch<A>] {
currentHookNameInDev = 'useReducer';
mountHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
@@ -1271,12 +1316,14 @@ if (__DEV__) {
},
useRef<T>(initialValue: T): {current: T} {
currentHookNameInDev = 'useRef';
mountHookTypesDev();
return mountRef(initialValue);
},
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
mountHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
@@ -1287,6 +1334,104 @@ if (__DEV__) {
},
useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
currentHookNameInDev = 'useDebugValue';
mountHookTypesDev();
return mountDebugValue(value, formatterFn);
},
};
HooksDispatcherOnMountWithHookTypesInDEV = {
readContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
): T {
return readContext(context, observedBits);
},
useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useCallback';
updateHookTypesDev();
return mountCallback(callback, deps);
},
useContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
): T {
currentHookNameInDev = 'useContext';
updateHookTypesDev();
return readContext(context, observedBits);
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useEffect';
updateHookTypesDev();
return mountEffect(create, deps);
},
useImperativeHandle<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useImperativeHandle';
updateHookTypesDev();
return mountImperativeHandle(ref, create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useLayoutEffect';
updateHookTypesDev();
return mountLayoutEffect(create, deps);
},
useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useMemo';
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountMemo(create, deps);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
currentHookNameInDev = 'useReducer';
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountReducer(reducer, initialArg, init);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
useRef<T>(initialValue: T): {current: T} {
currentHookNameInDev = 'useRef';
updateHookTypesDev();
return mountRef(initialValue);
},
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountState(initialState);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
currentHookNameInDev = 'useDebugValue';
updateHookTypesDev();
return mountDebugValue(value, formatterFn);
},
};
@@ -1301,6 +1446,7 @@ if (__DEV__) {
useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useCallback';
updateHookTypesDev();
return updateCallback(callback, deps);
},
useContext<T>(
@@ -1308,13 +1454,15 @@ if (__DEV__) {
observedBits: void | number | boolean,
): T {
currentHookNameInDev = 'useContext';
return updateContext(context, observedBits);
updateHookTypesDev();
return readContext(context, observedBits);
},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useEffect';
updateHookTypesDev();
return updateEffect(create, deps);
},
useImperativeHandle<T>(
@@ -1323,6 +1471,7 @@ if (__DEV__) {
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useImperativeHandle';
updateHookTypesDev();
return updateImperativeHandle(ref, create, deps);
},
useLayoutEffect(
@@ -1330,10 +1479,12 @@ if (__DEV__) {
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useLayoutEffect';
updateHookTypesDev();
return updateLayoutEffect(create, deps);
},
useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useMemo';
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
@@ -1348,6 +1499,7 @@ if (__DEV__) {
init?: I => S,
): [S, Dispatch<A>] {
currentHookNameInDev = 'useReducer';
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
@@ -1358,12 +1510,14 @@ if (__DEV__) {
},
useRef<T>(initialValue: T): {current: T} {
currentHookNameInDev = 'useRef';
updateHookTypesDev();
return updateRef(initialValue);
},
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
@@ -1374,6 +1528,7 @@ if (__DEV__) {
},
useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
currentHookNameInDev = 'useDebugValue';
updateHookTypesDev();
return updateDebugValue(value, formatterFn);
},
};
@@ -1390,6 +1545,7 @@ if (__DEV__) {
useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useCallback';
warnInvalidHookAccess();
mountHookTypesDev();
return mountCallback(callback, deps);
},
useContext<T>(
@@ -1398,7 +1554,8 @@ if (__DEV__) {
): T {
currentHookNameInDev = 'useContext';
warnInvalidHookAccess();
return mountContext(context, observedBits);
mountHookTypesDev();
return readContext(context, observedBits);
},
useEffect(
create: () => (() => void) | void,
@@ -1406,6 +1563,7 @@ if (__DEV__) {
): void {
currentHookNameInDev = 'useEffect';
warnInvalidHookAccess();
mountHookTypesDev();
return mountEffect(create, deps);
},
useImperativeHandle<T>(
@@ -1415,6 +1573,7 @@ if (__DEV__) {
): void {
currentHookNameInDev = 'useImperativeHandle';
warnInvalidHookAccess();
mountHookTypesDev();
return mountImperativeHandle(ref, create, deps);
},
useLayoutEffect(
@@ -1423,11 +1582,13 @@ if (__DEV__) {
): void {
currentHookNameInDev = 'useLayoutEffect';
warnInvalidHookAccess();
mountHookTypesDev();
return mountLayoutEffect(create, deps);
},
useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useMemo';
warnInvalidHookAccess();
mountHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
@@ -1443,6 +1604,7 @@ if (__DEV__) {
): [S, Dispatch<A>] {
currentHookNameInDev = 'useReducer';
warnInvalidHookAccess();
mountHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
@@ -1454,6 +1616,7 @@ if (__DEV__) {
useRef<T>(initialValue: T): {current: T} {
currentHookNameInDev = 'useRef';
warnInvalidHookAccess();
mountHookTypesDev();
return mountRef(initialValue);
},
useState<S>(
@@ -1461,6 +1624,7 @@ if (__DEV__) {
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
warnInvalidHookAccess();
mountHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
@@ -1472,6 +1636,7 @@ if (__DEV__) {
useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
currentHookNameInDev = 'useDebugValue';
warnInvalidHookAccess();
mountHookTypesDev();
return mountDebugValue(value, formatterFn);
},
};
@@ -1488,6 +1653,7 @@ if (__DEV__) {
useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useCallback';
warnInvalidHookAccess();
updateHookTypesDev();
return updateCallback(callback, deps);
},
useContext<T>(
@@ -1496,7 +1662,8 @@ if (__DEV__) {
): T {
currentHookNameInDev = 'useContext';
warnInvalidHookAccess();
return updateContext(context, observedBits);
updateHookTypesDev();
return readContext(context, observedBits);
},
useEffect(
create: () => (() => void) | void,
@@ -1504,6 +1671,7 @@ if (__DEV__) {
): void {
currentHookNameInDev = 'useEffect';
warnInvalidHookAccess();
updateHookTypesDev();
return updateEffect(create, deps);
},
useImperativeHandle<T>(
@@ -1513,6 +1681,7 @@ if (__DEV__) {
): void {
currentHookNameInDev = 'useImperativeHandle';
warnInvalidHookAccess();
updateHookTypesDev();
return updateImperativeHandle(ref, create, deps);
},
useLayoutEffect(
@@ -1521,11 +1690,13 @@ if (__DEV__) {
): void {
currentHookNameInDev = 'useLayoutEffect';
warnInvalidHookAccess();
updateHookTypesDev();
return updateLayoutEffect(create, deps);
},
useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
currentHookNameInDev = 'useMemo';
warnInvalidHookAccess();
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
@@ -1541,6 +1712,7 @@ if (__DEV__) {
): [S, Dispatch<A>] {
currentHookNameInDev = 'useReducer';
warnInvalidHookAccess();
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
@@ -1552,6 +1724,7 @@ if (__DEV__) {
useRef<T>(initialValue: T): {current: T} {
currentHookNameInDev = 'useRef';
warnInvalidHookAccess();
updateHookTypesDev();
return updateRef(initialValue);
},
useState<S>(
@@ -1559,6 +1732,7 @@ if (__DEV__) {
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
warnInvalidHookAccess();
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
@@ -1570,6 +1744,7 @@ if (__DEV__) {
useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
currentHookNameInDev = 'useDebugValue';
warnInvalidHookAccess();
updateHookTypesDev();
return updateDebugValue(value, formatterFn);
},
};

View File

@@ -44,7 +44,12 @@ describe('ReactHooks', () => {
expect(() => {
ReactTestRenderer.create(<Example />);
}).toThrow(
'Hooks can only be called inside the body of a function component.',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen' +
' for one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
});
}
@@ -645,20 +650,20 @@ describe('ReactHooks', () => {
const root1 = ReactTestRenderer.create(null);
expect(() => root1.update(<App return={17} />)).toWarnDev([
'Warning: An Effect function must not return anything besides a ' +
'Warning: An effect function must not return anything besides a ' +
'function, which is used for clean-up. You returned: 17',
]);
const root2 = ReactTestRenderer.create(null);
expect(() => root2.update(<App return={null} />)).toWarnDev([
'Warning: An Effect function must not return anything besides a ' +
'Warning: An effect function must not return anything besides a ' +
'function, which is used for clean-up. You returned null. If your ' +
'effect does not require clean up, return undefined (or nothing).',
]);
const root3 = ReactTestRenderer.create(null);
expect(() => root3.update(<App return={Promise.resolve()} />)).toWarnDev([
'Warning: An Effect function must not return anything besides a ' +
'Warning: An effect function must not return anything besides a ' +
'function, which is used for clean-up.\n\n' +
'It looks like you wrote useEffect(async () => ...) or returned a Promise.',
]);
@@ -669,6 +674,76 @@ describe('ReactHooks', () => {
}).toThrow('is not a function');
});
it('does not forget render phase useState updates inside an effect', () => {
const {useState, useEffect} = React;
function Counter() {
const [counter, setCounter] = useState(0);
if (counter === 0) {
setCounter(x => x + 1);
setCounter(x => x + 1);
}
useEffect(() => {
setCounter(x => x + 1);
setCounter(x => x + 1);
}, []);
return counter;
}
const root = ReactTestRenderer.create(null);
ReactTestRenderer.act(() => {
root.update(<Counter />);
});
expect(root).toMatchRenderedOutput('4');
});
it('does not forget render phase useReducer updates inside an effect with hoisted reducer', () => {
const {useReducer, useEffect} = React;
const reducer = x => x + 1;
function Counter() {
const [counter, increment] = useReducer(reducer, 0);
if (counter === 0) {
increment();
increment();
}
useEffect(() => {
increment();
increment();
}, []);
return counter;
}
const root = ReactTestRenderer.create(null);
ReactTestRenderer.act(() => {
root.update(<Counter />);
});
expect(root).toMatchRenderedOutput('4');
});
it('does not forget render phase useReducer updates inside an effect with inline reducer', () => {
const {useReducer, useEffect} = React;
function Counter() {
const [counter, increment] = useReducer(x => x + 1, 0);
if (counter === 0) {
increment();
increment();
}
useEffect(() => {
increment();
increment();
}, []);
return counter;
}
const root = ReactTestRenderer.create(null);
ReactTestRenderer.act(() => {
root.update(<Counter />);
});
expect(root).toMatchRenderedOutput('4');
});
it('warns for bad useImperativeHandle first arg', () => {
const {useImperativeHandle} = React;
function App() {
@@ -735,15 +810,30 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(<MemoApp />);
// trying to render again should trigger comparison and throw
expect(() => root.update(<MemoApp />)).toThrow(
'Hooks can only be called inside the body of a function component',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
// the next round, it does a fresh mount, so should render
expect(() => root.update(<MemoApp />)).not.toThrow(
'Hooks can only be called inside the body of a function component',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
// and then again, fail
expect(() => root.update(<MemoApp />)).toThrow(
'Hooks can only be called inside the body of a function component',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
});
@@ -914,8 +1004,6 @@ describe('ReactHooks', () => {
it('warns when calling hooks inside useReducer', () => {
const {useReducer, useState, useRef} = React;
spyOnDev(console, 'error');
function App() {
const [value, dispatch] = useReducer((state, action) => {
useRef(0);
@@ -927,16 +1015,23 @@ describe('ReactHooks', () => {
useState();
return value;
}
expect(() => {
ReactTestRenderer.create(<App />);
}).toThrow('Rendered more hooks than during the previous render.');
if (__DEV__) {
expect(console.error).toHaveBeenCalledTimes(3);
expect(console.error.calls.argsFor(0)[0]).toContain(
'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
);
}
expect(() => {
expect(() => {
ReactTestRenderer.create(<App />);
}).toThrow('Rendered more hooks than during the previous render.');
}).toWarnDev([
'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
'Warning: React has detected a change in the order of Hooks called by App. ' +
'This will lead to bugs and errors if not fixed. For more information, ' +
'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
' Previous render Next render\n' +
' ------------------------------------------------------\n' +
'1. useReducer useReducer\n' +
'2. useState useRef\n' +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
]);
});
it("warns when calling hooks inside useState's initialize function", () => {
@@ -1267,74 +1362,261 @@ describe('ReactHooks', () => {
expect(useMemoCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
});
it('warns on using differently ordered hooks on subsequent renders', () => {
const {useState, useReducer, useRef} = React;
function useCustomHook() {
return useState(0);
}
function App(props) {
/* eslint-disable no-unused-vars */
if (props.flip) {
useCustomHook(0);
useReducer((s, a) => a, 0);
} else {
useReducer((s, a) => a, 0);
useCustomHook(0);
}
// This should not appear in the warning message because it occurs after
// the first mismatch
const ref = useRef(null);
return null;
/* eslint-enable no-unused-vars */
}
let root = ReactTestRenderer.create(<App flip={false} />);
expect(() => {
root.update(<App flip={true} />);
}).toWarnDev([
'Warning: React has detected a change in the order of Hooks called by App. ' +
'This will lead to bugs and errors if not fixed. For more information, ' +
'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
' Previous render Next render\n' +
' -------------------------------\n' +
'1. useReducer useState\n' +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
' in App (at **)',
]);
describe('hook ordering', () => {
const useCallbackHelper = () => React.useCallback(() => {}, []);
const useContextHelper = () => React.useContext(React.createContext());
const useDebugValueHelper = () => React.useDebugValue('abc');
const useEffectHelper = () => React.useEffect(() => () => {}, []);
const useImperativeHandleHelper = () => {
React.useImperativeHandle({current: null}, () => ({}), []);
};
const useLayoutEffectHelper = () =>
React.useLayoutEffect(() => () => {}, []);
const useMemoHelper = () => React.useMemo(() => 123, []);
const useReducerHelper = () => React.useReducer((s, a) => a, 0);
const useRefHelper = () => React.useRef(null);
const useStateHelper = () => React.useState(0);
// further warnings for this component are silenced
root.update(<App flip={false} />);
});
// We don't include useImperativeHandleHelper in this set,
// because it generates an additional warning about the inputs length changing.
// We test it below with its own test.
let orderedHooks = [
useCallbackHelper,
useContextHelper,
useDebugValueHelper,
useEffectHelper,
useLayoutEffectHelper,
useMemoHelper,
useReducerHelper,
useRefHelper,
useStateHelper,
];
it('detects a bad hook order even if the component throws', () => {
const {useState, useReducer} = React;
function useCustomHook() {
useState(0);
}
function App(props) {
/* eslint-disable no-unused-vars */
if (props.flip) {
useCustomHook();
useReducer((s, a) => a, 0);
throw new Error('custom error');
} else {
useReducer((s, a) => a, 0);
useCustomHook();
const formatHookNamesToMatchErrorMessage = (hookNameA, hookNameB) => {
return `use${hookNameA}${' '.repeat(24 - hookNameA.length)}${
hookNameB ? `use${hookNameB}` : undefined
}`;
};
orderedHooks.forEach((firstHelper, index) => {
const secondHelper =
index > 0
? orderedHooks[index - 1]
: orderedHooks[orderedHooks.length - 1];
const hookNameA = firstHelper.name
.replace('use', '')
.replace('Helper', '');
const hookNameB = secondHelper.name
.replace('use', '')
.replace('Helper', '');
it(`warns on using differently ordered hooks (${hookNameA}, ${hookNameB}) on subsequent renders`, () => {
function App(props) {
/* eslint-disable no-unused-vars */
if (props.update) {
secondHelper();
firstHelper();
} else {
firstHelper();
secondHelper();
}
// This should not appear in the warning message because it occurs after the first mismatch
useRefHelper();
return null;
/* eslint-enable no-unused-vars */
}
let root = ReactTestRenderer.create(<App update={false} />);
expect(() => {
try {
root.update(<App update={true} />);
} catch (error) {
// Swapping certain types of hooks will cause runtime errors.
// This is okay as far as this test is concerned.
// We just want to verify that warnings are always logged.
}
}).toWarnDev([
'Warning: React has detected a change in the order of Hooks called by App. ' +
'This will lead to bugs and errors if not fixed. For more information, ' +
'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
' Previous render Next render\n' +
' ------------------------------------------------------\n' +
`1. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameB)}\n` +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
' in App (at **)',
]);
// further warnings for this component are silenced
try {
root.update(<App update={false} />);
} catch (error) {
// Swapping certain types of hooks will cause runtime errors.
// This is okay as far as this test is concerned.
// We just want to verify that warnings are always logged.
}
});
it(`warns when more hooks (${(hookNameA,
hookNameB)}) are used during update than mount`, () => {
function App(props) {
/* eslint-disable no-unused-vars */
if (props.update) {
firstHelper();
secondHelper();
} else {
firstHelper();
}
return null;
/* eslint-enable no-unused-vars */
}
let root = ReactTestRenderer.create(<App update={false} />);
expect(() => {
try {
root.update(<App update={true} />);
} catch (error) {
// Swapping certain types of hooks will cause runtime errors.
// This is okay as far as this test is concerned.
// We just want to verify that warnings are always logged.
}
}).toWarnDev([
'Warning: React has detected a change in the order of Hooks called by App. ' +
'This will lead to bugs and errors if not fixed. For more information, ' +
'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
' Previous render Next render\n' +
' ------------------------------------------------------\n' +
`1. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameA)}\n` +
`2. undefined use${hookNameB}\n` +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
' in App (at **)',
]);
});
});
// We don't include useContext or useDebugValue in this set,
// because they aren't added to the hooks list and so won't throw.
let hooksInList = [
useCallbackHelper,
useEffectHelper,
useImperativeHandleHelper,
useLayoutEffectHelper,
useMemoHelper,
useReducerHelper,
useRefHelper,
useStateHelper,
];
hooksInList.forEach((firstHelper, index) => {
const secondHelper =
index > 0
? hooksInList[index - 1]
: hooksInList[hooksInList.length - 1];
const hookNameA = firstHelper.name
.replace('use', '')
.replace('Helper', '');
const hookNameB = secondHelper.name
.replace('use', '')
.replace('Helper', '');
it(`warns when fewer hooks (${(hookNameA,
hookNameB)}) are used during update than mount`, () => {
function App(props) {
/* eslint-disable no-unused-vars */
if (props.update) {
firstHelper();
} else {
firstHelper();
secondHelper();
}
return null;
/* eslint-enable no-unused-vars */
}
let root = ReactTestRenderer.create(<App update={false} />);
expect(() => {
root.update(<App update={true} />);
}).toThrow('Rendered fewer hooks than expected.');
});
});
it(
'warns on using differently ordered hooks ' +
'(useImperativeHandleHelper, useMemoHelper) on subsequent renders',
() => {
function App(props) {
/* eslint-disable no-unused-vars */
if (props.update) {
useMemoHelper();
useImperativeHandleHelper();
} else {
useImperativeHandleHelper();
useMemoHelper();
}
// This should not appear in the warning message because it occurs after the first mismatch
useRefHelper();
return null;
/* eslint-enable no-unused-vars */
}
let root = ReactTestRenderer.create(<App update={false} />);
expect(() => {
try {
root.update(<App update={true} />);
} catch (error) {
// Swapping certain types of hooks will cause runtime errors.
// This is okay as far as this test is concerned.
// We just want to verify that warnings are always logged.
}
}).toWarnDev([
'Warning: React has detected a change in the order of Hooks called by App. ' +
'This will lead to bugs and errors if not fixed. For more information, ' +
'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
' Previous render Next render\n' +
' ------------------------------------------------------\n' +
`1. ${formatHookNamesToMatchErrorMessage(
'ImperativeHandle',
'Memo',
)}\n` +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
' in App (at **)',
]);
// further warnings for this component are silenced
root.update(<App update={false} />);
},
);
it('detects a bad hook order even if the component throws', () => {
const {useState, useReducer} = React;
function useCustomHook() {
useState(0);
}
return null;
/* eslint-enable no-unused-vars */
}
let root = ReactTestRenderer.create(<App flip={false} />);
expect(() => {
expect(() => root.update(<App flip={true} />)).toThrow('custom error');
}).toWarnDev([
'Warning: React has detected a change in the order of Hooks called by App. ' +
'This will lead to bugs and errors if not fixed. For more information, ' +
'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
' Previous render Next render\n' +
' -------------------------------\n' +
'1. useReducer useState\n' +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^',
]);
function App(props) {
/* eslint-disable no-unused-vars */
if (props.update) {
useCustomHook();
useReducer((s, a) => a, 0);
throw new Error('custom error');
} else {
useReducer((s, a) => a, 0);
useCustomHook();
}
return null;
/* eslint-enable no-unused-vars */
}
let root = ReactTestRenderer.create(<App update={false} />);
expect(() => {
expect(() => root.update(<App update={true} />)).toThrow(
'custom error',
);
}).toWarnDev([
'Warning: React has detected a change in the order of Hooks called by App. ' +
'This will lead to bugs and errors if not fixed. For more information, ' +
'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
' Previous render Next render\n' +
' ------------------------------------------------------\n' +
'1. useReducer useState\n' +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
]);
});
});
// Regression test for #14674

View File

@@ -108,7 +108,12 @@ describe('ReactHooksWithNoopRenderer', () => {
ReactNoop.render(<BadCounter />);
expect(() => ReactNoop.flush()).toThrow(
'Hooks can only be called inside the body of a function component.',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
// Confirm that a subsequent hook works properly.
@@ -131,7 +136,12 @@ describe('ReactHooksWithNoopRenderer', () => {
}
ReactNoop.render(<Counter />);
expect(() => ReactNoop.flush()).toThrow(
'Hooks can only be called inside the body of a function component.',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
// Confirm that a subsequent hook works properly.
@@ -145,7 +155,12 @@ describe('ReactHooksWithNoopRenderer', () => {
it('throws when called outside the render phase', () => {
expect(() => useState(0)).toThrow(
'Hooks can only be called inside the body of a function component.',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
});
@@ -454,7 +469,9 @@ describe('ReactHooksWithNoopRenderer', () => {
// Test that it works on update, too. This time the log is a bit different
// because we started with reducerB instead of reducerA.
counter.current.dispatch('reset');
ReactNoop.act(() => {
counter.current.dispatch('reset');
});
ReactNoop.render(<Counter ref={counter} />);
expect(ReactNoop.flush()).toEqual([
'Render: 0',
@@ -1738,8 +1755,20 @@ describe('ReactHooksWithNoopRenderer', () => {
ReactNoop.render(<App loadC={true} />);
expect(() => {
expect(ReactNoop.flush()).toEqual(['A: 2, B: 3, C: 0']);
}).toThrow('Rendered more hooks than during the previous render');
expect(() => {
expect(ReactNoop.flush()).toEqual(['A: 2, B: 3, C: 0']);
}).toThrow('Rendered more hooks than during the previous render');
}).toWarnDev([
'Warning: React has detected a change in the order of Hooks called by App. ' +
'This will lead to bugs and errors if not fixed. For more information, ' +
'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
' Previous render Next render\n' +
' ------------------------------------------------------\n' +
'1. useState useState\n' +
'2. useState useState\n' +
'3. undefined useState\n' +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
]);
// Uncomment if/when we support this again
// expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 0')]);
@@ -1817,8 +1846,19 @@ describe('ReactHooksWithNoopRenderer', () => {
ReactNoop.render(<App showMore={true} />);
expect(() => {
expect(ReactNoop.flush()).toEqual([]);
}).toThrow('Rendered more hooks than during the previous render');
expect(() => {
expect(ReactNoop.flush()).toEqual([]);
}).toThrow('Rendered more hooks than during the previous render');
}).toWarnDev([
'Warning: React has detected a change in the order of Hooks called by App. ' +
'This will lead to bugs and errors if not fixed. For more information, ' +
'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
' Previous render Next render\n' +
' ------------------------------------------------------\n' +
'1. useEffect useEffect\n' +
'2. undefined useEffect\n' +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
]);
// Uncomment if/when we support this again
// ReactNoop.flushPassiveEffects();

View File

@@ -1514,7 +1514,12 @@ describe('ReactNewContext', () => {
}
ReactNoop.render(<Foo />);
expect(ReactNoop.flush).toThrow(
'Hooks can only be called inside the body of a function component.',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen' +
' for one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
});

View File

@@ -1,6 +1,6 @@
{
"name": "react-test-renderer",
"version": "16.8.2",
"version": "16.8.4",
"description": "React package for snapshot testing.",
"main": "index.js",
"repository": {
@@ -21,8 +21,8 @@
"dependencies": {
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"react-is": "^16.8.2",
"scheduler": "^0.13.2"
"react-is": "^16.8.4",
"scheduler": "^0.13.4"
},
"peerDependencies": {
"react": "^16.0.0"

View File

@@ -8,7 +8,7 @@
*/
import React from 'react';
import {isForwardRef} from 'react-is';
import {isForwardRef, isMemo, ForwardRef} from 'react-is';
import describeComponentFrame from 'shared/describeComponentFrame';
import getComponentName from 'shared/getComponentName';
import shallowEqual from 'shared/shallowEqual';
@@ -31,7 +31,7 @@ type Update<A> = {
};
type UpdateQueue<A> = {
last: Update<A> | null,
first: Update<A> | null,
dispatch: any,
};
@@ -178,6 +178,10 @@ class ReactShallowRenderer {
};
constructor() {
this._reset();
}
_reset() {
this._context = null;
this._element = null;
this._instance = null;
@@ -192,9 +196,7 @@ class ReactShallowRenderer {
this._isReRender = false;
this._didScheduleRenderPhaseUpdate = false;
this._renderPhaseUpdates = null;
this._currentlyRenderingComponent = null;
this._numberOfReRenders = 0;
this._previousComponentIdentity = null;
}
_context: null | Object;
@@ -208,8 +210,6 @@ class ReactShallowRenderer {
_dispatcher: DispatcherType;
_workInProgressHook: null | Hook;
_firstWorkInProgressHook: null | Hook;
_currentlyRenderingComponent: null | Object;
_previousComponentIdentity: null | Object;
_renderPhaseUpdates: Map<UpdateQueue<any>, Update<any>> | null;
_isReRender: boolean;
_didScheduleRenderPhaseUpdate: boolean;
@@ -217,9 +217,13 @@ class ReactShallowRenderer {
_validateCurrentlyRenderingComponent() {
invariant(
this._currentlyRenderingComponent !== null,
'Hooks can only be called inside the body of a function component. ' +
'(https://fb.me/react-invalid-hook-call)',
this._rendering && !this._instance,
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
}
@@ -232,33 +236,44 @@ class ReactShallowRenderer {
this._validateCurrentlyRenderingComponent();
this._createWorkInProgressHook();
const workInProgressHook: Hook = (this._workInProgressHook: any);
if (this._isReRender) {
// This is a re-render. Apply the new render phase updates to the previous
// current hook.
// This is a re-render.
const queue: UpdateQueue<A> = (workInProgressHook.queue: any);
const dispatch: Dispatch<A> = (queue.dispatch: any);
if (this._renderPhaseUpdates !== null) {
// Render phase updates are stored in a map of queue -> linked list
const firstRenderPhaseUpdate = this._renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
(this._renderPhaseUpdates: any).delete(queue);
let newState = workInProgressHook.memoizedState;
let update = firstRenderPhaseUpdate;
do {
// Process this render phase update. We don't have to check the
// priority because it will always be the same as the current
// render's.
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null);
workInProgressHook.memoizedState = newState;
return [newState, dispatch];
if (this._numberOfReRenders > 0) {
// Apply the new render phase updates to the previous current hook.
if (this._renderPhaseUpdates !== null) {
// Render phase updates are stored in a map of queue -> linked list
const firstRenderPhaseUpdate = this._renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
(this._renderPhaseUpdates: any).delete(queue);
let newState = workInProgressHook.memoizedState;
let update = firstRenderPhaseUpdate;
do {
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null);
workInProgressHook.memoizedState = newState;
return [newState, dispatch];
}
}
return [workInProgressHook.memoizedState, dispatch];
}
return [workInProgressHook.memoizedState, dispatch];
// Process updates outside of render
let newState = workInProgressHook.memoizedState;
let update = queue.first;
if (update !== null) {
do {
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null);
queue.first = null;
workInProgressHook.memoizedState = newState;
}
return [newState, dispatch];
} else {
let initialState;
if (reducer === basicStateReducer) {
@@ -273,16 +288,12 @@ class ReactShallowRenderer {
}
workInProgressHook.memoizedState = initialState;
const queue: UpdateQueue<A> = (workInProgressHook.queue = {
last: null,
first: null,
dispatch: null,
});
const dispatch: Dispatch<
A,
> = (queue.dispatch = (this._dispatchAction.bind(
this,
(this._currentlyRenderingComponent: any),
queue,
): any));
> = (queue.dispatch = (this._dispatchAction.bind(this, queue): any));
return [workInProgressHook.memoizedState, dispatch];
}
};
@@ -373,18 +384,14 @@ class ReactShallowRenderer {
};
}
_dispatchAction<A>(
componentIdentity: Object,
queue: UpdateQueue<A>,
action: A,
) {
_dispatchAction<A>(queue: UpdateQueue<A>, action: A) {
invariant(
this._numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);
if (componentIdentity === this._currentlyRenderingComponent) {
if (this._rendering) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
@@ -409,9 +416,24 @@ class ReactShallowRenderer {
lastRenderPhaseUpdate.next = update;
}
} else {
// This means an update has happened after the function component has
// returned. On the server this is a no-op. In React Fiber, the update
// would be scheduled for a future render.
const update: Update<A> = {
action,
next: null,
};
// Append the update to the end of the list.
let last = queue.first;
if (last === null) {
queue.first = update;
} else {
while (last.next !== null) {
last = last.next;
}
last.next = update;
}
// Re-render now.
this.render(this._element, this._context);
}
}
@@ -441,17 +463,6 @@ class ReactShallowRenderer {
return this._workInProgressHook;
}
_prepareToUseHooks(componentIdentity: Object): void {
if (
this._previousComponentIdentity !== null &&
this._previousComponentIdentity !== componentIdentity
) {
this._firstWorkInProgressHook = null;
}
this._currentlyRenderingComponent = componentIdentity;
this._previousComponentIdentity = componentIdentity;
}
_finishHooks(element: ReactElement, context: null | Object) {
if (this._didScheduleRenderPhaseUpdate) {
// Updates were scheduled during the render phase. They are stored in
@@ -466,7 +477,6 @@ class ReactShallowRenderer {
this._rendering = false;
this.render(element, context);
} else {
this._currentlyRenderingComponent = null;
this._workInProgressHook = null;
this._renderPhaseUpdates = null;
this._numberOfReRenders = 0;
@@ -500,7 +510,8 @@ class ReactShallowRenderer {
element.type,
);
invariant(
isForwardRef(element) || typeof element.type === 'function',
isForwardRef(element) ||
(typeof element.type === 'function' || isMemo(element.type)),
'ReactShallowRenderer render(): Shallow rendering works only with custom ' +
'components, but the provided element type was `%s`.',
Array.isArray(element.type)
@@ -513,25 +524,40 @@ class ReactShallowRenderer {
if (this._rendering) {
return;
}
if (this._element != null && this._element.type !== element.type) {
this._reset();
}
const elementType = isMemo(element.type) ? element.type.type : element.type;
const previousElement = this._element;
this._rendering = true;
this._element = element;
this._context = getMaskedContext(element.type.contextTypes, context);
this._context = getMaskedContext(elementType.contextTypes, context);
// Inner memo component props aren't currently validated in createElement.
if (isMemo(element.type) && elementType.propTypes) {
currentlyValidatingElement = element;
checkPropTypes(
elementType.propTypes,
element.props,
'prop',
getComponentName(elementType),
getStackAddendum,
);
}
if (this._instance) {
this._updateClassComponent(element, this._context);
this._updateClassComponent(elementType, element, this._context);
} else {
if (isForwardRef(element)) {
this._rendered = element.type.render(element.props, element.ref);
} else if (shouldConstruct(element.type)) {
this._instance = new element.type(
if (shouldConstruct(elementType)) {
this._instance = new elementType(
element.props,
this._context,
this._updater,
);
if (typeof element.type.getDerivedStateFromProps === 'function') {
const partialState = element.type.getDerivedStateFromProps.call(
if (typeof elementType.getDerivedStateFromProps === 'function') {
const partialState = elementType.getDerivedStateFromProps.call(
null,
element.props,
this._instance.state,
@@ -545,35 +571,54 @@ class ReactShallowRenderer {
}
}
if (element.type.hasOwnProperty('contextTypes')) {
if (elementType.contextTypes) {
currentlyValidatingElement = element;
checkPropTypes(
element.type.contextTypes,
elementType.contextTypes,
this._context,
'context',
getName(element.type, this._instance),
getName(elementType, this._instance),
getStackAddendum,
);
currentlyValidatingElement = null;
}
this._mountClassComponent(element, this._context);
this._mountClassComponent(elementType, element, this._context);
} else {
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = this._dispatcher;
this._prepareToUseHooks(element.type);
try {
this._rendered = element.type.call(
undefined,
element.props,
this._context,
);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
let shouldRender = true;
if (isMemo(element.type) && previousElement !== null) {
// This is a Memo component that is being re-rendered.
const compare = element.type.compare || shallowEqual;
if (compare(previousElement.props, element.props)) {
shouldRender = false;
}
}
if (shouldRender) {
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = this._dispatcher;
try {
// elementType could still be a ForwardRef if it was
// nested inside Memo.
if (elementType.$$typeof === ForwardRef) {
invariant(
typeof elementType.render === 'function',
'forwardRef requires a render function but was given %s.',
typeof elementType.render,
);
this._rendered = elementType.render.call(
undefined,
element.props,
element.ref,
);
} else {
this._rendered = elementType(element.props, this._context);
}
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
this._finishHooks(element, context);
}
this._finishHooks(element, context);
}
}
@@ -589,17 +634,14 @@ class ReactShallowRenderer {
this._instance.componentWillUnmount();
}
}
this._firstWorkInProgressHook = null;
this._previousComponentIdentity = null;
this._context = null;
this._element = null;
this._newState = null;
this._rendered = null;
this._instance = null;
this._reset();
}
_mountClassComponent(element: ReactElement, context: null | Object) {
_mountClassComponent(
elementType: Function,
element: ReactElement,
context: null | Object,
) {
this._instance.context = context;
this._instance.props = element.props;
this._instance.state = this._instance.state || null;
@@ -614,7 +656,7 @@ class ReactShallowRenderer {
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for components using the new APIs.
if (
typeof element.type.getDerivedStateFromProps !== 'function' &&
typeof elementType.getDerivedStateFromProps !== 'function' &&
typeof this._instance.getSnapshotBeforeUpdate !== 'function'
) {
if (typeof this._instance.componentWillMount === 'function') {
@@ -636,8 +678,12 @@ class ReactShallowRenderer {
// because DOM refs are not available.
}
_updateClassComponent(element: ReactElement, context: null | Object) {
const {props, type} = element;
_updateClassComponent(
elementType: Function,
element: ReactElement,
context: null | Object,
) {
const {props} = element;
const oldState = this._instance.state || emptyObject;
const oldProps = this._instance.props;
@@ -646,7 +692,7 @@ class ReactShallowRenderer {
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for components using the new APIs.
if (
typeof element.type.getDerivedStateFromProps !== 'function' &&
typeof elementType.getDerivedStateFromProps !== 'function' &&
typeof this._instance.getSnapshotBeforeUpdate !== 'function'
) {
if (typeof this._instance.componentWillReceiveProps === 'function') {
@@ -662,8 +708,8 @@ class ReactShallowRenderer {
// Read state after cWRP in case it calls setState
let state = this._newState || oldState;
if (typeof type.getDerivedStateFromProps === 'function') {
const partialState = type.getDerivedStateFromProps.call(
if (typeof elementType.getDerivedStateFromProps === 'function') {
const partialState = elementType.getDerivedStateFromProps.call(
null,
props,
state,
@@ -683,7 +729,10 @@ class ReactShallowRenderer {
state,
context,
);
} else if (type.prototype && type.prototype.isPureReactComponent) {
} else if (
elementType.prototype &&
elementType.prototype.isPureReactComponent
) {
shouldUpdate =
!shallowEqual(oldProps, props) || !shallowEqual(oldState, state);
}
@@ -692,7 +741,7 @@ class ReactShallowRenderer {
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for components using the new APIs.
if (
typeof element.type.getDerivedStateFromProps !== 'function' &&
typeof elementType.getDerivedStateFromProps !== 'function' &&
typeof this._instance.getSnapshotBeforeUpdate !== 'function'
) {
if (typeof this._instance.componentWillUpdate === 'function') {
@@ -727,7 +776,8 @@ function getDisplayName(element) {
} else if (typeof element.type === 'string') {
return element.type;
} else {
return element.type.displayName || element.type.name || 'Unknown';
const elementType = isMemo(element.type) ? element.type.type : element.type;
return elementType.displayName || elementType.name || 'Unknown';
}
}

View File

@@ -1454,4 +1454,157 @@ describe('ReactShallowRenderer', () => {
shallowRenderer.render(<Foo foo="bar" />);
expect(logs).toEqual([undefined]);
});
it('should handle memo', () => {
function Foo() {
return <div>foo</div>;
}
const MemoFoo = React.memo(Foo);
const shallowRenderer = createRenderer();
shallowRenderer.render(<MemoFoo />);
});
it('should enable React.memo to prevent a re-render', () => {
const logs = [];
const Foo = React.memo(({count}) => {
logs.push(`Foo: ${count}`);
return <div>{count}</div>;
});
const Bar = React.memo(({count}) => {
logs.push(`Bar: ${count}`);
return <div>{count}</div>;
});
const shallowRenderer = createRenderer();
shallowRenderer.render(<Foo count={1} />);
expect(logs).toEqual(['Foo: 1']);
logs.length = 0;
// Rendering the same element with the same props should be prevented
shallowRenderer.render(<Foo count={1} />);
expect(logs).toEqual([]);
// A different element with the same props should cause a re-render
shallowRenderer.render(<Bar count={1} />);
expect(logs).toEqual(['Bar: 1']);
});
it('should respect a custom comparison function with React.memo', () => {
let renderCount = 0;
function areEqual(props, nextProps) {
return props.foo === nextProps.foo;
}
const Foo = React.memo(({foo, bar}) => {
renderCount++;
return (
<div>
{foo} {bar}
</div>
);
}, areEqual);
const shallowRenderer = createRenderer();
shallowRenderer.render(<Foo foo={1} bar={1} />);
expect(renderCount).toBe(1);
// Change a prop that the comparison funciton ignores
shallowRenderer.render(<Foo foo={1} bar={2} />);
expect(renderCount).toBe(1);
shallowRenderer.render(<Foo foo={2} bar={2} />);
expect(renderCount).toBe(2);
});
it('should not call the comparison function with React.memo on the initial render', () => {
const areEqual = jest.fn(() => false);
const SomeComponent = React.memo(({foo}) => {
return <div>{foo}</div>;
}, areEqual);
const shallowRenderer = createRenderer();
shallowRenderer.render(<SomeComponent foo={1} />);
expect(areEqual).not.toHaveBeenCalled();
expect(shallowRenderer.getRenderOutput()).toEqual(<div>{1}</div>);
});
it('should handle memo(forwardRef())', () => {
const testRef = React.createRef();
const SomeComponent = React.forwardRef((props, ref) => {
expect(ref).toEqual(testRef);
return (
<div>
<span className="child1" />
<span className="child2" />
</div>
);
});
const SomeMemoComponent = React.memo(SomeComponent);
const shallowRenderer = createRenderer();
const result = shallowRenderer.render(<SomeMemoComponent ref={testRef} />);
expect(result.type).toBe('div');
expect(result.props.children).toEqual([
<span className="child1" />,
<span className="child2" />,
]);
});
it('should warn for forwardRef(memo())', () => {
const testRef = React.createRef();
const SomeMemoComponent = React.memo(({foo}) => {
return <div>{foo}</div>;
});
const shallowRenderer = createRenderer();
expect(() => {
expect(() => {
const SomeComponent = React.forwardRef(SomeMemoComponent);
shallowRenderer.render(<SomeComponent ref={testRef} />);
}).toWarnDev(
'Warning: forwardRef requires a render function but received ' +
'a `memo` component. Instead of forwardRef(memo(...)), use ' +
'memo(forwardRef(...))',
{withoutStack: true},
);
}).toThrowError(
'forwardRef requires a render function but was given object.',
);
});
it('should let you change type', () => {
function Foo({prop}) {
return <div>Foo {prop}</div>;
}
function Bar({prop}) {
return <div>Bar {prop}</div>;
}
const shallowRenderer = createRenderer();
shallowRenderer.render(<Foo prop="foo1" />);
expect(shallowRenderer.getRenderOutput()).toEqual(<div>Foo {'foo1'}</div>);
shallowRenderer.render(<Foo prop="foo2" />);
expect(shallowRenderer.getRenderOutput()).toEqual(<div>Foo {'foo2'}</div>);
shallowRenderer.render(<Bar prop="bar1" />);
expect(shallowRenderer.getRenderOutput()).toEqual(<div>Bar {'bar1'}</div>);
shallowRenderer.render(<Bar prop="bar2" />);
expect(shallowRenderer.getRenderOutput()).toEqual(<div>Bar {'bar2'}</div>);
});
it('should let you change class type', () => {
class Foo extends React.Component {
render() {
return <div>Foo {this.props.prop}</div>;
}
}
class Bar extends React.Component {
render() {
return <div>Bar {this.props.prop}</div>;
}
}
const shallowRenderer = createRenderer();
shallowRenderer.render(<Foo prop="foo1" />);
expect(shallowRenderer.getRenderOutput()).toEqual(<div>Foo {'foo1'}</div>);
shallowRenderer.render(<Foo prop="foo2" />);
expect(shallowRenderer.getRenderOutput()).toEqual(<div>Foo {'foo2'}</div>);
shallowRenderer.render(<Bar prop="bar1" />);
expect(shallowRenderer.getRenderOutput()).toEqual(<div>Bar {'bar1'}</div>);
shallowRenderer.render(<Bar prop="bar2" />);
expect(shallowRenderer.getRenderOutput()).toEqual(<div>Bar {'bar2'}</div>);
});
});

View File

@@ -90,6 +90,61 @@ describe('ReactShallowRenderer with hooks', () => {
);
});
it('should work with updating a derived value from useState', () => {
let _updateName;
function SomeComponent({defaultName}) {
const [name, updateName] = React.useState(defaultName);
const [prevName, updatePrevName] = React.useState(defaultName);
const [letter, updateLetter] = React.useState(name[0]);
_updateName = updateName;
if (name !== prevName) {
updatePrevName(name);
updateLetter(name[0]);
}
return (
<div>
<p>
Your name is: <span>{name + ' (' + letter + ')'}</span>
</p>
</div>
);
}
const shallowRenderer = createRenderer();
let result = shallowRenderer.render(
<SomeComponent defaultName={'Sophie'} />,
);
expect(result).toEqual(
<div>
<p>
Your name is: <span>Sophie (S)</span>
</p>
</div>,
);
result = shallowRenderer.render(<SomeComponent defaultName={'Dan'} />);
expect(result).toEqual(
<div>
<p>
Your name is: <span>Sophie (S)</span>
</p>
</div>,
);
_updateName('Dan');
expect(shallowRenderer.getRenderOutput()).toEqual(
<div>
<p>
Your name is: <span>Dan (D)</span>
</p>
</div>,
);
});
it('should work with useReducer', () => {
function reducer(state, action) {
switch (action.type) {
@@ -304,4 +359,133 @@ describe('ReactShallowRenderer with hooks', () => {
</div>,
);
});
it('should work with with forwardRef + any hook', () => {
const SomeComponent = React.forwardRef((props, ref) => {
const randomNumberRef = React.useRef({number: Math.random()});
return (
<div ref={ref}>
<p>The random number is: {randomNumberRef.current.number}</p>
</div>
);
});
const shallowRenderer = createRenderer();
let firstResult = shallowRenderer.render(<SomeComponent />);
let secondResult = shallowRenderer.render(<SomeComponent />);
expect(firstResult).toEqual(secondResult);
});
it('should update a value from useState outside the render', () => {
let _dispatch;
function SomeComponent({defaultName}) {
const [count, dispatch] = React.useReducer(
(s, a) => (a === 'inc' ? s + 1 : s),
0,
);
const [name, updateName] = React.useState(defaultName);
_dispatch = () => dispatch('inc');
return (
<div onClick={() => updateName('Dan')}>
<p>
Your name is: <span>{name}</span> ({count})
</p>
</div>
);
}
const shallowRenderer = createRenderer();
const element = <SomeComponent defaultName={'Dominic'} />;
const result = shallowRenderer.render(element);
expect(result.props.children).toEqual(
<p>
Your name is: <span>Dominic</span> ({0})
</p>,
);
result.props.onClick();
let updated = shallowRenderer.render(element);
expect(updated.props.children).toEqual(
<p>
Your name is: <span>Dan</span> ({0})
</p>,
);
_dispatch('foo');
updated = shallowRenderer.render(element);
expect(updated.props.children).toEqual(
<p>
Your name is: <span>Dan</span> ({1})
</p>,
);
_dispatch('inc');
updated = shallowRenderer.render(element);
expect(updated.props.children).toEqual(
<p>
Your name is: <span>Dan</span> ({2})
</p>,
);
});
it('should ignore a foreign update outside the render', () => {
let _updateCountForFirstRender;
function SomeComponent() {
const [count, updateCount] = React.useState(0);
if (!_updateCountForFirstRender) {
_updateCountForFirstRender = updateCount;
}
return count;
}
const shallowRenderer = createRenderer();
const element = <SomeComponent />;
let result = shallowRenderer.render(element);
expect(result).toEqual(0);
_updateCountForFirstRender(1);
result = shallowRenderer.render(element);
expect(result).toEqual(1);
shallowRenderer.unmount();
result = shallowRenderer.render(element);
expect(result).toEqual(0);
_updateCountForFirstRender(1); // Should be ignored.
result = shallowRenderer.render(element);
expect(result).toEqual(0);
});
it('should not forget render phase updates', () => {
let _updateCount;
function SomeComponent() {
const [count, updateCount] = React.useState(0);
_updateCount = updateCount;
if (count < 5) {
updateCount(x => x + 1);
}
return count;
}
const shallowRenderer = createRenderer();
const element = <SomeComponent />;
let result = shallowRenderer.render(element);
expect(result).toEqual(5);
_updateCount(10);
result = shallowRenderer.render(element);
expect(result).toEqual(10);
_updateCount(x => x + 1);
result = shallowRenderer.render(element);
expect(result).toEqual(11);
_updateCount(x => x - 10);
result = shallowRenderer.render(element);
expect(result).toEqual(5);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"keywords": [
"react"
],
"version": "16.8.2",
"version": "16.8.4",
"homepage": "https://reactjs.org/",
"bugs": "https://github.com/facebook/react/issues",
"license": "MIT",
@@ -29,7 +29,7 @@
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.13.2"
"scheduler": "^0.13.4"
},
"browserify": {
"transform": [

View File

@@ -17,8 +17,12 @@ function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
invariant(
dispatcher !== null,
'Hooks can only be called inside the body of a function component. ' +
'(https://fb.me/react-invalid-hook-call)',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
return dispatcher;
}

View File

@@ -18,6 +18,11 @@ import {
unstable_continueExecution,
unstable_wrapCallback,
unstable_getCurrentPriorityLevel,
unstable_IdlePriority,
unstable_ImmediatePriority,
unstable_LowPriority,
unstable_NormalPriority,
unstable_UserBlockingPriority,
} from 'scheduler';
import {
__interactionsRef,
@@ -60,6 +65,11 @@ if (__UMD__) {
unstable_pauseExecution,
unstable_continueExecution,
unstable_getCurrentPriorityLevel,
unstable_IdlePriority,
unstable_ImmediatePriority,
unstable_LowPriority,
unstable_NormalPriority,
unstable_UserBlockingPriority,
},
SchedulerTracing: {
__interactionsRef,

View File

@@ -108,5 +108,25 @@
unstable_continueExecution: unstable_continueExecution,
unstable_pauseExecution: unstable_pauseExecution,
unstable_getFirstCallbackNode: unstable_getFirstCallbackNode,
get unstable_IdlePriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_IdlePriority;
},
get unstable_ImmediatePriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_ImmediatePriority;
},
get unstable_LowPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_LowPriority;
},
get unstable_NormalPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_NormalPriority;
},
get unstable_UserBlockingPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_UserBlockingPriority;
},
});
});

View File

@@ -102,5 +102,25 @@
unstable_continueExecution: unstable_continueExecution,
unstable_pauseExecution: unstable_pauseExecution,
unstable_getFirstCallbackNode: unstable_getFirstCallbackNode,
get unstable_IdlePriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_IdlePriority;
},
get unstable_ImmediatePriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_ImmediatePriority;
},
get unstable_LowPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_LowPriority;
},
get unstable_NormalPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_NormalPriority;
},
get unstable_UserBlockingPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_UserBlockingPriority;
},
});
});

View File

@@ -102,5 +102,25 @@
unstable_continueExecution: unstable_continueExecution,
unstable_pauseExecution: unstable_pauseExecution,
unstable_getFirstCallbackNode: unstable_getFirstCallbackNode,
get unstable_IdlePriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_IdlePriority;
},
get unstable_ImmediatePriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_ImmediatePriority;
},
get unstable_LowPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_LowPriority;
},
get unstable_NormalPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_NormalPriority;
},
get unstable_UserBlockingPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_UserBlockingPriority;
},
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "scheduler",
"version": "0.13.2",
"version": "0.13.4",
"description": "Cooperative scheduler for the browser environment.",
"main": "index.js",
"repository": {

View File

@@ -17,8 +17,17 @@ describe('Scheduling UMD bundle', () => {
});
function filterPrivateKeys(name) {
// TODO: Figure out how to forward priority levels.
return !name.startsWith('_') && !name.endsWith('Priority');
// Be very careful adding things to this whitelist!
// It's easy to introduce bugs by doing it:
// https://github.com/facebook/react/issues/14904
switch (name) {
case '__interactionsRef':
case '__subscriberRef':
// Don't forward these. (TODO: why?)
return false;
default:
return true;
}
}
function validateForwardedAPIs(api, forwardedAPIs) {

View File

@@ -8,4 +8,4 @@
'use strict';
// TODO: this is special because it gets imported during build.
module.exports = '16.8.2';
module.exports = '16.8.4';