A new prop type for React Components
Since we can use props to pass React Components, we should have a better way of validating these props than just `object`. Having a prop type `component` will allow us to safely assume that the passed prop IS a component.
Mouad Debbar committed
Dec 20, 2013 at 18:07 UTC
d914522ae4111512c7e0a604a6c5027600f5e966
2 files changed
+133
-1
src/core/ReactPropTypes.js
+46
-1
@@ -18,6 +18,7 @@
18
19
"use strict";
20
21
+var ReactComponent = require('ReactComponent');
22
var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
23
24
var createObjectFrom = require('createObjectFrom');
@@ -83,12 +84,37 @@ var Props = {
84
oneOf: createEnumTypeChecker,
85
oneOfType: createUnionTypeChecker,
86
86
- instanceOf: createInstanceTypeChecker
87
+ instanceOf: createInstanceTypeChecker,
88
+
89
+ renderable: createRenderableTypeChecker()
90
91
};
92
93
var ANONYMOUS = '<<anonymous>>';
94
95
+function isRenderable(propValue) {
96
+ switch(typeof propValue) {
97
+ case 'number':
98
+ case 'string':
99
+ return true;
100
+ case 'object':
101
+ if (Array.isArray(propValue)) {
102
+ return propValue.every(isRenderable);
103
+ }
104
+ if (ReactComponent.isValidComponent(propValue)) {
105
+ return true;
106
+ }
107
+ for (var k in propValue) {
108
+ if (!isRenderable(propValue[k])) {
109
+ return false;
110
+ }
111
+ }
112
+ return true;
113
+ default:
114
+ return false;
115
+ }
116
+}
117
+
118
function createPrimitiveTypeChecker(expectedType) {
119
function validatePrimitiveType(
120
shouldThrow, propValue, propName, componentName, location
@@ -155,6 +181,25 @@ function createInstanceTypeChecker(expectedClass) {
181
return createChainableTypeChecker(validateInstanceType);
182
}
183
184
+function createRenderableTypeChecker() {
185
+ function validateRenderableType(
186
+ shouldThrow, propValue, propName, componentName, location
187
+ ) {
188
+ var isValid = isRenderable(propValue);
189
+ if (!shouldThrow) {
190
+ return isValid;
191
+ }
192
+ invariant(
193
+ isValid,
194
+ 'Invalid %s `%s` supplied to `%s`, expected a renderable prop.',
195
+ ReactPropTypeLocationNames[location],
196
+ propName,
197
+ componentName
198
+ );
199
+ }
200
+ return createChainableTypeChecker(validateRenderableType);
201
+}
202
+
203
function createChainableTypeChecker(validate) {
204
function checkType(
205
isRequired, shouldThrow, props, propName, componentName, location
src/core/__tests__/ReactPropTypes-test.js
+87
@@ -20,6 +20,7 @@
20
"use strict";
21
22
var Props = require('ReactPropTypes');
23
+var React = require('React');
24
var ReactPropTypeLocations = require('ReactPropTypeLocations');
25
26
function typeCheck(declaration, value) {
@@ -36,6 +37,12 @@ function typeCheck(declaration, value) {
37
);
38
}
39
40
+var MyComponent = React.createClass({
41
+ render: function() {
42
+ return <div />;
43
+ }
44
+});
45
+
46
describe('Primitive Types', function() {
47
beforeEach(function() {
48
require('mock-modules').dumpCache();
@@ -206,4 +213,84 @@ describe('Union Types', function() {
213
expect(typeCheck(checker, 123)).not.toThrow();
214
});
215
216
+ describe('React Component Types', function() {
217
+ beforeEach(function() {
218
+ require('mock-modules').dumpCache();
219
+ });
220
+
221
+ var myFunc = function() {};
222
+
223
+ it('should throw for invalid values', function() {
224
+ expect(typeCheck(Props.renderable, false)).toThrow(
225
+ 'Invariant Violation: Invalid prop `testProp` supplied to ' +
226
+ '`testComponent`, expected a renderable prop.'
227
+ );
228
+ expect(typeCheck(Props.renderable, myFunc)).toThrow(
229
+ 'Invariant Violation: Invalid prop `testProp` supplied to ' +
230
+ '`testComponent`, expected a renderable prop.'
231
+ );
232
+ expect(typeCheck(Props.renderable, {key: myFunc})).toThrow(
233
+ 'Invariant Violation: Invalid prop `testProp` supplied to ' +
234
+ '`testComponent`, expected a renderable prop.'
235
+ );
236
+ });
237
+
238
+ it('should not throw for valid values', function() {
239
+ // DOM component
240
+ expect(typeCheck(Props.renderable, <div />)).not.toThrow();
241
+ // Custom component
242
+ expect(typeCheck(Props.renderable, <MyComponent />)).not.toThrow();
243
+ // String
244
+ expect(typeCheck(Props.renderable, 'Some string')).not.toThrow();
245
+ // Empty array
246
+ expect(typeCheck(Props.renderable, [])).not.toThrow();
247
+ // Empty object
248
+ expect(typeCheck(Props.renderable, {})).not.toThrow();
249
+ // Array of renderable things
250
+ expect(
251
+ typeCheck(Props.renderable, [
252
+ 123,
253
+ 'Some string',
254
+ <div />,
255
+ ['Another string', [456], <span />, <MyComponent />],
256
+ <MyComponent />
257
+ ])
258
+ ).not.toThrow();
259
+ // Object of rendereable things
260
+ expect(
261
+ typeCheck(Props.renderable, {
262
+ k0: 123,
263
+ k1: 'Some string',
264
+ k2: <div />,
265
+ k3: {
266
+ k30: <MyComponent />,
267
+ k31: {k310: <a />},
268
+ k32: 'Another string'
269
+ }
270
+ })
271
+ ).not.toThrow();
272
+ });
273
+
274
+ it('should not throw for null/undefined if not required', function() {
275
+ expect(typeCheck(Props.renderable, null)).not.toThrow();
276
+ expect(typeCheck(Props.renderable, undefined)).not.toThrow();
277
+ });
278
+
279
+ it('should throw for missing required values', function() {
280
+ expect(typeCheck(Props.renderable.isRequired, null)).toThrow(
281
+ 'Invariant Violation: Required prop `testProp` was not specified in ' +
282
+ '`testComponent`.'
283
+ );
284
+ expect(typeCheck(Props.renderable.isRequired, undefined)).toThrow(
285
+ 'Invariant Violation: Required prop `testProp` was not specified in ' +
286
+ '`testComponent`.'
287
+ );
288
+ });
289
+
290
+ it('should accept empty array & object for required props', function() {
291
+ expect(typeCheck(Props.renderable.isRequired, [])).not.toThrow();
292
+ expect(typeCheck(Props.renderable.isRequired, {})).not.toThrow();
293
+ });
294
+ });
295
+
296
});