Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84cc8a31fa | ||
|
|
f9e41e3a51 | ||
|
|
fb572afc14 | ||
|
|
a0a2e846ce | ||
|
|
1fc13e4b35 | ||
|
|
b5cb9d345c | ||
|
|
342fa78ed4 | ||
|
|
62f5d4a057 | ||
|
|
6b86a6e039 | ||
|
|
8f7335875c | ||
|
|
d822d4bbe7 | ||
|
|
13a3788c54 |
18
CHANGELOG.md
18
CHANGELOG.md
@@ -6,6 +6,24 @@
|
||||
</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
|
||||
|
||||
@@ -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.4",
|
||||
"version": "16.8.5",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/react.git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jest-react",
|
||||
"version": "0.6.4",
|
||||
"version": "0.6.5",
|
||||
"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.4",
|
||||
"version": "16.8.5",
|
||||
"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.4"
|
||||
"scheduler": "^0.13.5"
|
||||
},
|
||||
"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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-dom",
|
||||
"version": "16.8.4",
|
||||
"version": "16.8.5",
|
||||
"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.4"
|
||||
"scheduler": "^0.13.5"
|
||||
},
|
||||
"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.',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.4",
|
||||
"version": "16.8.5",
|
||||
"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.2",
|
||||
"version": "0.20.3",
|
||||
"keywords": [
|
||||
"react"
|
||||
],
|
||||
@@ -33,7 +33,7 @@
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.13.4"
|
||||
"scheduler": "^0.13.5"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -262,8 +262,12 @@ function warnOnHookMismatchInDev(currentHookName: HookType) {
|
||||
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.',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.',
|
||||
]);
|
||||
@@ -805,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.',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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.4",
|
||||
"version": "16.8.5",
|
||||
"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.4",
|
||||
"scheduler": "^0.13.4"
|
||||
"react-is": "^16.8.5",
|
||||
"scheduler": "^0.13.5"
|
||||
},
|
||||
"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.4",
|
||||
"version": "16.8.5",
|
||||
"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.4"
|
||||
"scheduler": "^0.13.5"
|
||||
},
|
||||
"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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scheduler",
|
||||
"version": "0.13.4",
|
||||
"version": "0.13.5",
|
||||
"description": "Cooperative scheduler for the browser environment.",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
'use strict';
|
||||
|
||||
// TODO: this is special because it gets imported during build.
|
||||
module.exports = '16.8.4';
|
||||
module.exports = '16.8.5';
|
||||
|
||||
@@ -319,5 +319,7 @@
|
||||
"317": "Expected to have a hydrated suspense instance. This error is likely caused by a bug in React. Please file an issue.",
|
||||
"318": "A dehydrated suspense component was completed without a hydrated node. This is probably a bug in React.",
|
||||
"319": "A dehydrated suspense boundary must commit before trying to render. This is probably a bug in React.",
|
||||
"320": "Expected ReactFiberErrorDialog.showErrorDialog to be a function."
|
||||
"320": "Expected ReactFiberErrorDialog.showErrorDialog to be a function.",
|
||||
"321": "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:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.",
|
||||
"322": "forwardRef requires a render function but was given %s."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user