Skip to content

Commit

Permalink
warn if passive effects get queued outside of an act() call.
Browse files Browse the repository at this point in the history
based on facebook#15591. of note, we don't modify our own tests to satisfy this warning, instead using a feature flag to disable the warning for some tests. this is because we're testing the actual sequencing of work in these tests, and don't want to flush everything with act().
  • Loading branch information
Sunil Pai committed May 12, 2019
1 parent 6920e9f commit 9f7a346
Show file tree
Hide file tree
Showing 20 changed files with 81 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ let React;
let ReactTestRenderer;
let Scheduler;
let ReactDebugTools;
let ReactFeatureFlags;
let act;

describe('ReactHooksInspectionIntegration', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.warnAboutUnactedEffectsinDEV = false;
React = require('react');
ReactTestRenderer = require('react-test-renderer');
Scheduler = require('scheduler');
Expand Down
3 changes: 3 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMHooks-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

let React;
let ReactDOM;
let ReactFeatureFlags;
let Scheduler;

describe('ReactDOMHooks', () => {
Expand All @@ -22,6 +23,8 @@ describe('ReactDOMHooks', () => {
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.warnAboutUnactedEffectsinDEV = false;

container = document.createElement('div');
document.body.appendChild(container);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function initModules() {

ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.warnAboutUnactedEffectsinDEV = false;
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('ReactErrorBoundaries', () => {
PropTypes = require('prop-types');
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
ReactFeatureFlags.warnAboutUnactedEffectsinDEV = false;
ReactDOM = require('react-dom');
React = require('react');
Scheduler = require('scheduler');
Expand Down
3 changes: 3 additions & 0 deletions packages/react-dom/src/__tests__/ReactUpdates-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
let React;
let ReactDOM;
let ReactTestUtils;
let ReactFeatureFlags;
let Scheduler;

describe('ReactUpdates', () => {
Expand All @@ -20,6 +21,8 @@ describe('ReactUpdates', () => {
React = require('react');
ReactDOM = require('react-dom');
ReactTestUtils = require('react-dom/test-utils');
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.warnAboutUnactedEffectsinDEV = false;
Scheduler = require('scheduler');
});

Expand Down
25 changes: 23 additions & 2 deletions packages/react-reconciler/src/ReactFiberHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ import {
computeExpirationForFiber,
flushPassiveEffects,
requestCurrentTime,
warnIfNotCurrentlyActingUpdatesInDev,
warnIfNotCurrentlyActingEffectsInDEV,
warnIfNotCurrentlyActingUpdatesInDEV,
markRenderEventTime,
} from './ReactFiberScheduler';

Expand Down Expand Up @@ -869,6 +870,16 @@ function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
if (__DEV__) {
// jest isn't a 'global', it's just exposed to tests via a wrapped function
// further, this isn't a test file, so flow doesn't recognize the symbol. So...
// $FlowExpectedError - because requirements don't give a damn about your type sigs.
if ('undefined' !== typeof jest) {
warnIfNotCurrentlyActingEffectsInDEV(
((currentlyRenderingFiber: any): Fiber),
);
}
}
return mountEffectImpl(
UpdateEffect | PassiveEffect,
UnmountPassive | MountPassive,
Expand All @@ -881,6 +892,16 @@ function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
if (__DEV__) {
// jest isn't a 'global', it's just exposed to tests via a wrapped function
// further, this isn't a test file, so flow doesn't recognize the symbol. So...
// $FlowExpectedError - because requirements don't give a damn about your type sigs.
if ('undefined' !== typeof jest) {
warnIfNotCurrentlyActingEffectsInDEV(
((currentlyRenderingFiber: any): Fiber),
);
}
}
return updateEffectImpl(
UpdateEffect | PassiveEffect,
UnmountPassive | MountPassive,
Expand Down Expand Up @@ -1180,7 +1201,7 @@ function dispatchAction<S, A>(
// further, this isn't a test file, so flow doesn't recognize the symbol. So...
// $FlowExpectedError - because requirements don't give a damn about your type sigs.
if ('undefined' !== typeof jest) {
warnIfNotCurrentlyActingUpdatesInDev(fiber);
warnIfNotCurrentlyActingUpdatesInDEV(fiber);
}
}
scheduleWork(fiber, expirationTime);
Expand Down
30 changes: 27 additions & 3 deletions packages/react-reconciler/src/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
enableProfilerTimer,
disableYielding,
enableSchedulerTracing,
warnAboutUnactedEffectsinDEV,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import invariant from 'shared/invariant';
Expand Down Expand Up @@ -2088,7 +2089,32 @@ function warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber) {
}
}

function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
if (__DEV__) {
if (warnAboutUnactedEffectsinDEV) {
if (ReactShouldWarnActingUpdates.current === false) {
warningWithoutStack(
false,
'An effect from %s inside a test was not wrapped in act(...).\n\n' +
'When testing, code that causes React state updates should be ' +
'wrapped into act(...):\n\n' +
'act(() => {\n' +
' /* fire events that update state */\n' +
'});\n' +
'/* assert on the output */\n\n' +
"This ensures that you're testing the behavior the user would see " +
'in the browser.' +
' Learn more at https://fb.me/react-wrap-tests-with-act' +
'%s',
getComponentName(fiber.type),
getStackByFiberInDevAndProd(fiber),
);
}
}
}
}

export function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
if (__DEV__) {
if (
workPhase === NotWorking &&
Expand All @@ -2114,8 +2140,6 @@ function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
}
}

export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV;

let componentsWithSuspendedDiscreteUpdates = null;
export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) {
if (__DEV__) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe('ReactHooks', () => {

ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.warnAboutUnactedEffectsinDEV = false;
React = require('react');
ReactTestRenderer = require('react-test-renderer');
Scheduler = require('scheduler');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe('ReactHooksWithNoopRenderer', () => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.enableSchedulerTracing = true;
ReactFeatureFlags.warnAboutUnactedEffectsinDEV = false;
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('ReactIncrementalScheduling', () => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.warnAboutUnactedEffectsinDEV = false;
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('ReactIncrementalUpdates', () => {
jest.resetModuleRegistry();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.warnAboutUnactedEffectsinDEV = false;
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('ReactSchedulerIntegration', () => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.warnAboutUnactedEffectsinDEV = false;
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
Expand Down
8 changes: 3 additions & 5 deletions packages/react/src/__tests__/withComponentStack-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ describe('withComponentStack', () => {
let React = null;
let ReactTestRenderer = null;
let error = null;
let scheduler = null;
let warn = null;

beforeEach(() => {
Expand All @@ -53,7 +52,6 @@ describe('withComponentStack', () => {

React = require('react');
ReactTestRenderer = require('react-test-renderer');
scheduler = require('scheduler');

error = React.error;
warn = React.warn;
Expand Down Expand Up @@ -179,9 +177,9 @@ describe('withComponentStack', () => {
return null;
}

ReactTestRenderer.create(<Parent />);

scheduler.flushAll(); // Flush passive effects
ReactTestRenderer.act(() => {
ReactTestRenderer.create(<Parent />);
});

expectMessageAndStack(
'logged in child render method',
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/ReactFeatureFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,8 @@ export const enableJSXTransformAPI = false;
// We will enforce mocking scheduler with scheduler/unstable_mock at some point. (v17?)
// Till then, we warn about the missing mock, but still fallback to a sync mode compatible version
export const warnAboutMissingMockScheduler = false;

// We warn if effects are queued in tests without a surround act()
// However, this interferes withour internal tests that test the actual
// sequencing work. So we use a feature flag to disable the warning *only* for ourselves
export const warnAboutUnactedEffectsinDEV = true;
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.native-fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const warnAboutDeprecatedSetNativeProps = true;
export const enableEventAPI = false;
export const enableJSXTransformAPI = false;
export const warnAboutMissingMockScheduler = true;
export const warnAboutUnactedEffectsinDEV = true;

// Only used in www builds.
export function addUserTimingListener() {
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.native-oss.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const warnAboutDeprecatedSetNativeProps = false;
export const enableEventAPI = false;
export const enableJSXTransformAPI = false;
export const warnAboutMissingMockScheduler = false;
export const warnAboutUnactedEffectsinDEV = true;

// Only used in www builds.
export function addUserTimingListener() {
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.persistent.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const warnAboutDeprecatedSetNativeProps = false;
export const enableEventAPI = false;
export const enableJSXTransformAPI = false;
export const warnAboutMissingMockScheduler = true;
export const warnAboutUnactedEffectsinDEV = true;

// Only used in www builds.
export function addUserTimingListener() {
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.test-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const warnAboutDeprecatedSetNativeProps = false;
export const enableEventAPI = false;
export const enableJSXTransformAPI = false;
export const warnAboutMissingMockScheduler = false;
export const warnAboutUnactedEffectsinDEV = true;

// Only used in www builds.
export function addUserTimingListener() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const disableYielding = false;
export const enableEventAPI = true;
export const enableJSXTransformAPI = true;
export const warnAboutMissingMockScheduler = true;
export const warnAboutUnactedEffectsinDEV = true;

// Only used in www builds.
export function addUserTimingListener() {
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/forks/ReactFeatureFlags.www.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export const enableJSXTransformAPI = true;

export const warnAboutMissingMockScheduler = true;

export const warnAboutUnactedEffectsinDEV = true;

// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;
Expand Down

0 comments on commit 9f7a346

Please sign in to comment.