main
js 376 lines 11.6 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and 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 {ATTRIBUTE_NAME_CHAR} from './isAttributeNameSafe';
9 import isCustomElement from './isCustomElement';
10 import possibleStandardNames from './possibleStandardNames';
11 import hasOwnProperty from 'shared/hasOwnProperty';
12
13 const warnedProperties = {};
14 const EVENT_NAME_REGEX = /^on./;
15 const INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/;
16 const rARIA = __DEV__
17 ? new RegExp('^(aria)-[' + ATTRIBUTE_NAME_CHAR + ']*$')
18 : null;
19 const rARIACamel = __DEV__
20 ? new RegExp('^(aria)[A-Z][' + ATTRIBUTE_NAME_CHAR + ']*$')
21 : null;
22
23 function validateProperty(tagName, name, value, eventRegistry) {
24 if (__DEV__) {
25 if (hasOwnProperty.call(warnedProperties, name) && warnedProperties[name]) {
26 return true;
27 }
28
29 const lowerCasedName = name.toLowerCase();
30 if (lowerCasedName === 'onfocusin' || lowerCasedName === 'onfocusout') {
31 console.error(
32 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
33 'All React events are normalized to bubble, so onFocusIn and onFocusOut ' +
34 'are not needed/supported by React.',
35 );
36 warnedProperties[name] = true;
37 return true;
38 }
39
40 // Actions are special because unlike events they can have other value types.
41 if (typeof value === 'function') {
42 if (tagName === 'form' && name === 'action') {
43 return true;
44 }
45 if (tagName === 'input' && name === 'formAction') {
46 return true;
47 }
48 if (tagName === 'button' && name === 'formAction') {
49 return true;
50 }
51 }
52 // We can't rely on the event system being injected on the server.
53 if (eventRegistry != null) {
54 const {registrationNameDependencies, possibleRegistrationNames} =
55 eventRegistry;
56 if (registrationNameDependencies.hasOwnProperty(name)) {
57 return true;
58 }
59 const registrationName = possibleRegistrationNames.hasOwnProperty(
60 lowerCasedName,
61 )
62 ? possibleRegistrationNames[lowerCasedName]
63 : null;
64 if (registrationName != null) {
65 console.error(
66 'Invalid event handler property `%s`. Did you mean `%s`?',
67 name,
68 registrationName,
69 );
70 warnedProperties[name] = true;
71 return true;
72 }
73 if (EVENT_NAME_REGEX.test(name)) {
74 console.error(
75 'Unknown event handler property `%s`. It will be ignored.',
76 name,
77 );
78 warnedProperties[name] = true;
79 return true;
80 }
81 } else if (EVENT_NAME_REGEX.test(name)) {
82 // If no event plugins have been injected, we are in a server environment.
83 // So we can't tell if the event name is correct for sure, but we can filter
84 // out known bad ones like `onclick`. We can't suggest a specific replacement though.
85 if (INVALID_EVENT_NAME_REGEX.test(name)) {
86 console.error(
87 'Invalid event handler property `%s`. ' +
88 'React events use the camelCase naming convention, for example `onClick`.',
89 name,
90 );
91 }
92 warnedProperties[name] = true;
93 return true;
94 }
95
96 // Let the ARIA attribute hook validate ARIA attributes
97 if (rARIA.test(name) || rARIACamel.test(name)) {
98 return true;
99 }
100
101 if (lowerCasedName === 'innerhtml') {
102 console.error(
103 'Directly setting property `innerHTML` is not permitted. ' +
104 'For more information, lookup documentation on `dangerouslySetInnerHTML`.',
105 );
106 warnedProperties[name] = true;
107 return true;
108 }
109
110 if (lowerCasedName === 'aria') {
111 console.error(
112 'The `aria` attribute is reserved for future use in React. ' +
113 'Pass individual `aria-` attributes instead.',
114 );
115 warnedProperties[name] = true;
116 return true;
117 }
118
119 if (
120 lowerCasedName === 'is' &&
121 value !== null &&
122 value !== undefined &&
123 typeof value !== 'string'
124 ) {
125 console.error(
126 'Received a `%s` for a string attribute `is`. If this is expected, cast ' +
127 'the value to a string.',
128 typeof value,
129 );
130 warnedProperties[name] = true;
131 return true;
132 }
133
134 if (typeof value === 'number' && isNaN(value)) {
135 console.error(
136 'Received NaN for the `%s` attribute. If this is expected, cast ' +
137 'the value to a string.',
138 name,
139 );
140 warnedProperties[name] = true;
141 return true;
142 }
143
144 // Known attributes should match the casing specified in the property config.
145 if (possibleStandardNames.hasOwnProperty(lowerCasedName)) {
146 const standardName = possibleStandardNames[lowerCasedName];
147 if (standardName !== name) {
148 console.error(
149 'Invalid DOM property `%s`. Did you mean `%s`?',
150 name,
151 standardName,
152 );
153 warnedProperties[name] = true;
154 return true;
155 }
156 } else if (name !== lowerCasedName) {
157 // Unknown attributes should have lowercase casing since that's how they
158 // will be cased anyway with server rendering.
159 console.error(
160 'React does not recognize the `%s` prop on a DOM element. If you ' +
161 'intentionally want it to appear in the DOM as a custom ' +
162 'attribute, spell it as lowercase `%s` instead. ' +
163 'If you accidentally passed it from a parent component, remove ' +
164 'it from the DOM element.',
165 name,
166 lowerCasedName,
167 );
168 warnedProperties[name] = true;
169 return true;
170 }
171
172 // Now that we've validated casing, do not validate
173 // data types for reserved props
174 switch (name) {
175 case 'dangerouslySetInnerHTML':
176 case 'children':
177 case 'style':
178 case 'suppressContentEditableWarning':
179 case 'suppressHydrationWarning':
180 case 'defaultValue': // Reserved
181 case 'defaultChecked':
182 case 'innerHTML':
183 case 'ref': {
184 return true;
185 }
186 case 'innerText': // Properties
187 case 'textContent':
188 return true;
189 }
190
191 switch (typeof value) {
192 case 'boolean': {
193 switch (name) {
194 case 'autoFocus':
195 case 'checked':
196 case 'multiple':
197 case 'muted':
198 case 'selected':
199 case 'contentEditable':
200 case 'spellCheck':
201 case 'draggable':
202 case 'value':
203 case 'autoReverse':
204 case 'externalResourcesRequired':
205 case 'focusable':
206 case 'preserveAlpha':
207 case 'allowFullScreen':
208 case 'async':
209 case 'autoPlay':
210 case 'controls':
211 case 'credentialless':
212 case 'default':
213 case 'defer':
214 case 'disabled':
215 case 'disablePictureInPicture':
216 case 'disableRemotePlayback':
217 case 'formNoValidate':
218 case 'hidden':
219 case 'loop':
220 case 'noModule':
221 case 'noValidate':
222 case 'open':
223 case 'playsInline':
224 case 'readOnly':
225 case 'required':
226 case 'reversed':
227 case 'scoped':
228 case 'seamless':
229 case 'itemScope':
230 case 'capture':
231 case 'download':
232 case 'inert': {
233 // Boolean properties can accept boolean values
234 return true;
235 }
236 // fallthrough
237 default: {
238 const prefix = name.toLowerCase().slice(0, 5);
239 if (prefix === 'data-' || prefix === 'aria-') {
240 return true;
241 }
242 if (value) {
243 console.error(
244 'Received `%s` for a non-boolean attribute `%s`.\n\n' +
245 'If you want to write it to the DOM, pass a string instead: ' +
246 '%s="%s" or %s={value.toString()}.',
247 value,
248 name,
249 name,
250 value,
251 name,
252 );
253 } else {
254 console.error(
255 'Received `%s` for a non-boolean attribute `%s`.\n\n' +
256 'If you want to write it to the DOM, pass a string instead: ' +
257 '%s="%s" or %s={value.toString()}.\n\n' +
258 'If you used to conditionally omit it with %s={condition && value}, ' +
259 'pass %s={condition ? value : undefined} instead.',
260 value,
261 name,
262 name,
263 value,
264 name,
265 name,
266 name,
267 );
268 }
269 warnedProperties[name] = true;
270 return true;
271 }
272 }
273 }
274 case 'function':
275 case 'symbol':
276 // Warn when a known attribute is a bad type
277 warnedProperties[name] = true;
278 return false;
279 case 'string': {
280 // Warn when passing the strings 'false' or 'true' into a boolean prop
281 if (value === 'false' || value === 'true') {
282 switch (name) {
283 case 'checked':
284 case 'selected':
285 case 'multiple':
286 case 'muted':
287 case 'allowFullScreen':
288 case 'async':
289 case 'autoPlay':
290 case 'controls':
291 case 'credentialless':
292 case 'default':
293 case 'defer':
294 case 'disabled':
295 case 'disablePictureInPicture':
296 case 'disableRemotePlayback':
297 case 'formNoValidate':
298 case 'hidden':
299 case 'loop':
300 case 'noModule':
301 case 'noValidate':
302 case 'open':
303 case 'playsInline':
304 case 'readOnly':
305 case 'required':
306 case 'reversed':
307 case 'scoped':
308 case 'seamless':
309 case 'itemScope':
310 case 'inert': {
311 break;
312 }
313 default: {
314 return true;
315 }
316 }
317 console.error(
318 'Received the string `%s` for the boolean attribute `%s`. ' +
319 '%s ' +
320 'Did you mean %s={%s}?',
321 value,
322 name,
323 value === 'false'
324 ? 'The browser will interpret it as a truthy value.'
325 : 'Although this works, it will not work as expected if you pass the string "false".',
326 name,
327 value,
328 );
329 warnedProperties[name] = true;
330 return true;
331 }
332 }
333 }
334 return true;
335 }
336 }
337
338 function warnUnknownProperties(type, props, eventRegistry) {
339 if (__DEV__) {
340 const unknownProps = [];
341 for (const key in props) {
342 const isValid = validateProperty(type, key, props[key], eventRegistry);
343 if (!isValid) {
344 unknownProps.push(key);
345 }
346 }
347
348 const unknownPropString = unknownProps
349 .map(prop => '`' + prop + '`')
350 .join(', ');
351 if (unknownProps.length === 1) {
352 console.error(
353 'Invalid value for prop %s on <%s> tag. Either remove it from the element, ' +
354 'or pass a string or number value to keep it in the DOM. ' +
355 'For details, see https://react.dev/link/attribute-behavior ',
356 unknownPropString,
357 type,
358 );
359 } else if (unknownProps.length > 1) {
360 console.error(
361 'Invalid values for props %s on <%s> tag. Either remove them from the element, ' +
362 'or pass a string or number value to keep them in the DOM. ' +
363 'For details, see https://react.dev/link/attribute-behavior ',
364 unknownPropString,
365 type,
366 );
367 }
368 }
369 }
370
371 export function validateProperties(type, props, eventRegistry) {
372 if (isCustomElement(type, props) || typeof props.is === 'string') {
373 return;
374 }
375 warnUnknownProperties(type, props, eventRegistry);
376 }