Skip to content

Commit f161ee2

Browse files
authored
React.warn() and React.error() (#15170)
1 parent 78968bb commit f161ee2

File tree

3 files changed

+244
-0
lines changed

3 files changed

+244
-0
lines changed

packages/react/src/React.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
cloneElementWithValidation,
4646
} from './ReactElementValidator';
4747
import ReactSharedInternals from './ReactSharedInternals';
48+
import {error, warn} from './withComponentStack';
4849
import {enableStableConcurrentModeAPIs} from 'shared/ReactFeatureFlags';
4950

5051
const React = {
@@ -65,6 +66,9 @@ const React = {
6566
lazy,
6667
memo,
6768

69+
error,
70+
warn,
71+
6872
useCallback,
6973
useContext,
7074
useEffect,
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
'use strict';
11+
12+
function normalizeCodeLocInfo(str) {
13+
return str && str.replace(/at .+?:\d+/g, 'at **');
14+
}
15+
16+
function expectHelper(spy, prefix, ...expectedArgs) {
17+
const expectedStack = expectedArgs.pop();
18+
19+
expect(spy).toHaveBeenCalledTimes(1);
20+
21+
const actualArgs = spy.calls.mostRecent().args;
22+
23+
let actualStack = undefined;
24+
if (expectedStack !== undefined) {
25+
actualStack = actualArgs.pop();
26+
expect(normalizeCodeLocInfo(actualStack)).toBe(expectedStack);
27+
}
28+
29+
expect(actualArgs).toHaveLength(expectedArgs.length);
30+
actualArgs.forEach((actualArg, index) => {
31+
const expectedArg = expectedArgs[index];
32+
expect(actualArg).toBe(
33+
index === 0 ? `${prefix}: ${expectedArg}` : expectedArg,
34+
);
35+
});
36+
}
37+
38+
function expectMessageAndStack(...expectedArgs) {
39+
expectHelper(console.error, 'error', ...expectedArgs);
40+
expectHelper(console.warn, 'warn', ...expectedArgs);
41+
}
42+
43+
describe('withComponentStack', () => {
44+
let React = null;
45+
let ReactTestRenderer = null;
46+
let error = null;
47+
let scheduler = null;
48+
let warn = null;
49+
50+
beforeEach(() => {
51+
jest.resetModules();
52+
jest.mock('scheduler', () => require('scheduler/unstable_mock'));
53+
54+
React = require('react');
55+
ReactTestRenderer = require('react-test-renderer');
56+
scheduler = require('scheduler');
57+
58+
error = React.error;
59+
warn = React.warn;
60+
61+
spyOnDevAndProd(console, 'error');
62+
spyOnDevAndProd(console, 'warn');
63+
});
64+
65+
if (!__DEV__) {
66+
it('does nothing in production mode', () => {
67+
error('error');
68+
warn('warning');
69+
70+
expect(console.error).toHaveBeenCalledTimes(0);
71+
expect(console.warn).toHaveBeenCalledTimes(0);
72+
});
73+
}
74+
75+
if (__DEV__) {
76+
it('does not include component stack when called outside of render', () => {
77+
error('error: logged outside of render');
78+
warn('warn: logged outside of render');
79+
expectMessageAndStack('logged outside of render', undefined);
80+
});
81+
82+
it('should support multiple args', () => {
83+
function Component() {
84+
error('error: number:', 123, 'boolean:', true);
85+
warn('warn: number:', 123, 'boolean:', true);
86+
return null;
87+
}
88+
89+
ReactTestRenderer.create(<Component />);
90+
91+
expectMessageAndStack(
92+
'number:',
93+
123,
94+
'boolean:',
95+
true,
96+
'\n in Component (at **)',
97+
);
98+
});
99+
100+
it('includes component stack when called from a render method', () => {
101+
class Parent extends React.Component {
102+
render() {
103+
return <Child />;
104+
}
105+
}
106+
107+
function Child() {
108+
error('error: logged in child render method');
109+
warn('warn: logged in child render method');
110+
return null;
111+
}
112+
113+
ReactTestRenderer.create(<Parent />);
114+
115+
expectMessageAndStack(
116+
'logged in child render method',
117+
'\n in Child (at **)' + '\n in Parent (at **)',
118+
);
119+
});
120+
121+
it('includes component stack when called from a render phase lifecycle method', () => {
122+
function Parent() {
123+
return <Child />;
124+
}
125+
126+
class Child extends React.Component {
127+
UNSAFE_componentWillMount() {
128+
error('error: logged in child cWM lifecycle');
129+
warn('warn: logged in child cWM lifecycle');
130+
}
131+
render() {
132+
return null;
133+
}
134+
}
135+
136+
ReactTestRenderer.create(<Parent />);
137+
138+
expectMessageAndStack(
139+
'logged in child cWM lifecycle',
140+
'\n in Child (at **)' + '\n in Parent (at **)',
141+
);
142+
});
143+
144+
it('includes component stack when called from a commit phase lifecycle method', () => {
145+
function Parent() {
146+
return <Child />;
147+
}
148+
149+
class Child extends React.Component {
150+
componentDidMount() {
151+
error('error: logged in child cDM lifecycle');
152+
warn('warn: logged in child cDM lifecycle');
153+
}
154+
render() {
155+
return null;
156+
}
157+
}
158+
159+
ReactTestRenderer.create(<Parent />);
160+
161+
expectMessageAndStack(
162+
'logged in child cDM lifecycle',
163+
'\n in Child (at **)' + '\n in Parent (at **)',
164+
);
165+
});
166+
167+
it('includes component stack when called from a passive effect handler', () => {
168+
class Parent extends React.Component {
169+
render() {
170+
return <Child />;
171+
}
172+
}
173+
174+
function Child() {
175+
React.useEffect(() => {
176+
error('error: logged in child render method');
177+
warn('warn: logged in child render method');
178+
});
179+
return null;
180+
}
181+
182+
ReactTestRenderer.create(<Parent />);
183+
184+
scheduler.flushAll(); // Flush passive effects
185+
186+
expectMessageAndStack(
187+
'logged in child render method',
188+
'\n in Child (at **)' + '\n in Parent (at **)',
189+
);
190+
});
191+
}
192+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import ReactSharedInternals from 'shared/ReactSharedInternals';
9+
10+
function noop() {}
11+
12+
let error = noop;
13+
let warn = noop;
14+
if (__DEV__) {
15+
const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
16+
17+
error = function() {
18+
const stack = ReactDebugCurrentFrame.getStackAddendum();
19+
if (stack !== '') {
20+
const length = arguments.length;
21+
const args = new Array(length + 1);
22+
for (let i = 0; i < length; i++) {
23+
args[i] = arguments[i];
24+
}
25+
args[length] = stack;
26+
console.error.apply(console, args);
27+
} else {
28+
console.error.apply(console, arguments);
29+
}
30+
};
31+
32+
warn = function() {
33+
const stack = ReactDebugCurrentFrame.getStackAddendum();
34+
if (stack !== '') {
35+
const length = arguments.length;
36+
const args = new Array(length + 1);
37+
for (let i = 0; i < length; i++) {
38+
args[i] = arguments[i];
39+
}
40+
args[length] = stack;
41+
console.warn.apply(console, args);
42+
} else {
43+
console.warn.apply(console, arguments);
44+
}
45+
};
46+
}
47+
48+
export {error, warn};

0 commit comments

Comments
 (0)