Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9e41e3a51 | ||
|
|
fb572afc14 | ||
|
|
a0a2e846ce | ||
|
|
1fc13e4b35 | ||
|
|
b5cb9d345c | ||
|
|
342fa78ed4 | ||
|
|
62f5d4a057 | ||
|
|
6b86a6e039 | ||
|
|
8f7335875c | ||
|
|
d822d4bbe7 | ||
|
|
13a3788c54 | ||
|
|
d8a73b5eb6 | ||
|
|
741aa17a33 | ||
|
|
95c2b49543 | ||
|
|
55cf14f98e | ||
|
|
29b7b775f2 | ||
|
|
b668168d4d | ||
|
|
619cdfc624 |
34
CHANGELOG.md
34
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: []},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": [
|
||||
|
||||
7
packages/react-reconciler/src/ReactFiber.js
vendored
7
packages/react-reconciler/src/ReactFiber.js
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
363
packages/react-reconciler/src/ReactFiberHooks.js
vendored
363
packages/react-reconciler/src/ReactFiberHooks.js
vendored
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
1520
packages/react-test-renderer/src/__tests__/ReactShallowRendererMemo-test.js
vendored
Normal file
1520
packages/react-test-renderer/src/__tests__/ReactShallowRendererMemo-test.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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": [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user