diff --git a/debug/src/debug.js b/debug/src/debug.js index c09a0863fd..c4ed11c4e4 100644 --- a/debug/src/debug.js +++ b/debug/src/debug.js @@ -74,6 +74,22 @@ export function initDebug() { // Check prop-types if available if (typeof vnode.type==='function' && vnode.type.propTypes) { + if (vnode.type.name === 'Lazy') { + const m = 'PropTypes are not supported on lazy(). Use propTypes on the wrapped component itself. '; + try { + const lazied = vnode.type(); + console.warn(m + 'Component wrapped in lazy() is ' + lazied.then.displayName || lazied.then.name); + } + catch (promise) { + console.warn(m + 'We will log the wrapped component\'s name once it is loaded.'); + if (promise.then) { + promise.then((exports) => { + console.warn('Component wrapped in lazy() is ' + (exports.default.displayName || exports.default.name)); + }); + } + + } + } checkPropTypes(vnode.type.propTypes, vnode.props, getDisplayName(vnode), serializeVNode(vnode)); } diff --git a/debug/test/browser/debug.test.js b/debug/test/browser/debug.test.js index 1e5b6fd65f..151b667df7 100644 --- a/debug/test/browser/debug.test.js +++ b/debug/test/browser/debug.test.js @@ -1,6 +1,6 @@ -import { createElement as h, options, render, createRef, Component, Fragment } from 'preact'; +import { createElement as h, options, render, createRef, Component, Fragment, lazy, Suspense } from 'preact'; import { useState, useEffect, useLayoutEffect, useMemo, useCallback } from 'preact/hooks'; -import { act } from 'preact/test-utils'; +import { act, setupRerender } from 'preact/test-utils'; import { setupScratch, teardown, clearOptions, serializeHtml } from '../../../test/_util/helpers'; import { serializeVNode, initDebug } from '../../src/debug'; import * as PropTypes from 'prop-types'; @@ -392,5 +392,53 @@ describe('debug', () => { render(, scratch); expect(console.error).to.not.be.called; }); + + it('should validate propTypes inside lazy()', () => { + const rerender = setupRerender(); + + function Baz(props) { + return

{props.unhappy}

; + } + + Baz.propTypes = { + unhappy: function alwaysThrows(obj, key) { if (obj[key] === 'signal') throw Error('got prop inside lazy()'); } + }; + + + const loader = Promise.resolve({ default: Baz }); + const LazyBaz = lazy(() => loader); + + render( + fallback...}> + + , + scratch + ); + + expect(console.error).to.not.be.called; + + return loader.then(() => { + rerender(); + expect(errors.length).to.equal(1); + expect(errors[0].includes('got prop')).to.equal(true); + expect(serializeHtml(scratch)).to.equal('

signal

'); + }); + }); + + it('should warn for PropTypes on lazy()', () => { + const loader = Promise.resolve({ default: function MyLazyLoadedComponent() { return
Hi there
} }); + const FakeLazy = lazy(() => loader); + FakeLazy.propTypes = {}; + render( + fallback...} > + + , + scratch + ); + return loader.then(() => { + expect(console.warn).to.be.calledTwice; + expect(warnings[1].includes('MyLazyLoadedComponent')).to.equal(true); + }); + }); }); });