Compare commits

..

12 Commits

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

* Update other renderers + change call to action

* Update related tests for new hooks error message

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

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

* Remove _previousComponentIdentity

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

* Use _reset when unmounting

* Use arbitrary componentIdentity

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

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

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

* Allow Rect.memo to prevent re-renders

* Support memo(forwardRef())

* Dont call memo comparison function on initial render

* Fix test

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

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

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

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

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

* Use let, not const in select tag stub assignment
2019-03-22 14:12:11 +00:00
Dan Abramov
13a3788c54 Improve async useEffect warning (#15104) 2019-03-22 14:11:22 +00:00
29 changed files with 2224 additions and 158 deletions

View File

@@ -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

View File

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

View File

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

View File

@@ -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": {

View File

@@ -1,7 +1,7 @@
{
"name": "react-art",
"description": "React ART is a JavaScript library for drawing vector graphics using React. It provides declarative and reactive bindings to the ART library. Using the same declarative API you can render the output to either Canvas, SVG or VML (IE8).",
"version": "16.8.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"

View File

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

View File

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

View File

@@ -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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "react-reconciler",
"description": "React package for creating custom renderers.",
"version": "0.20.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": [

View File

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

View File

@@ -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.',
);
}

View File

@@ -44,7 +44,12 @@ describe('ReactHooks', () => {
expect(() => {
ReactTestRenderer.create(<Example />);
}).toThrow(
'Hooks can only be called inside the body of a function component.',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen' +
' for one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
});
}
@@ -645,20 +650,20 @@ describe('ReactHooks', () => {
const root1 = ReactTestRenderer.create(null);
expect(() => root1.update(<App return={17} />)).toWarnDev([
'Warning: An Effect function must not return anything besides a ' +
'Warning: An effect function must not return anything besides a ' +
'function, which is used for clean-up. You returned: 17',
]);
const root2 = ReactTestRenderer.create(null);
expect(() => root2.update(<App return={null} />)).toWarnDev([
'Warning: An Effect function must not return anything besides a ' +
'Warning: An effect function must not return anything besides a ' +
'function, which is used for clean-up. You returned null. If your ' +
'effect does not require clean up, return undefined (or nothing).',
]);
const root3 = ReactTestRenderer.create(null);
expect(() => root3.update(<App return={Promise.resolve()} />)).toWarnDev([
'Warning: An Effect function must not return anything besides a ' +
'Warning: An effect function must not return anything besides a ' +
'function, which is used for clean-up.\n\n' +
'It looks like you wrote useEffect(async () => ...) or returned a Promise.',
]);
@@ -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.',
);
});

View File

@@ -108,7 +108,12 @@ describe('ReactHooksWithNoopRenderer', () => {
ReactNoop.render(<BadCounter />);
expect(() => ReactNoop.flush()).toThrow(
'Hooks can only be called inside the body of a function component.',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
// Confirm that a subsequent hook works properly.
@@ -131,7 +136,12 @@ describe('ReactHooksWithNoopRenderer', () => {
}
ReactNoop.render(<Counter />);
expect(() => ReactNoop.flush()).toThrow(
'Hooks can only be called inside the body of a function component.',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
// Confirm that a subsequent hook works properly.
@@ -145,7 +155,12 @@ describe('ReactHooksWithNoopRenderer', () => {
it('throws when called outside the render phase', () => {
expect(() => useState(0)).toThrow(
'Hooks can only be called inside the body of a function component.',
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
});

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "react-test-renderer",
"version": "16.8.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"

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"keywords": [
"react"
],
"version": "16.8.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": [

View File

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

View File

@@ -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": {

View File

@@ -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';

View File

@@ -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."
}