From 8b8de40304f98d6ebf56ae34bf913b4456043168 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Wed, 25 Dec 2019 22:53:52 +0100 Subject: [PATCH 01/20] fix compat hydration (#2206) --- compat/src/index.js | 4 ++-- compat/src/render.js | 4 ++++ compat/test/browser/hydrate.test.js | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 compat/test/browser/hydrate.test.js diff --git a/compat/src/index.js b/compat/src/index.js index bfa929deee..be948742e6 100644 --- a/compat/src/index.js +++ b/compat/src/index.js @@ -26,7 +26,7 @@ import { Children } from './Children'; import { Suspense, lazy } from './suspense'; import { SuspenseList } from './suspense-list'; import { createPortal } from './portals'; -import { render, REACT_ELEMENT_TYPE } from './render'; +import { hydrate, render, REACT_ELEMENT_TYPE } from './render'; const version = '16.8.0'; // trick libraries to think we are react @@ -100,7 +100,7 @@ export { version, Children, render, - render as hydrate, + hydrate, unmountComponentAtNode, createPortal, createElement, diff --git a/compat/src/render.js b/compat/src/render.js index e64ecbfdec..ab38419fe8 100644 --- a/compat/src/render.js +++ b/compat/src/render.js @@ -34,6 +34,10 @@ export function render(vnode, parent, callback) { } } + return hydrate(vnode, parent, callback); +} + +export function hydrate(vnode, parent, callback) { preactRender(vnode, parent); if (typeof callback === 'function') callback(); diff --git a/compat/test/browser/hydrate.test.js b/compat/test/browser/hydrate.test.js new file mode 100644 index 0000000000..c590339876 --- /dev/null +++ b/compat/test/browser/hydrate.test.js @@ -0,0 +1,25 @@ +import React, { hydrate } from 'preact/compat'; +import { setupScratch, teardown } from '../../../test/_util/helpers'; + +describe('compat hydrate', () => { + /** @type {HTMLDivElement} */ + let scratch; + + beforeEach(() => { + scratch = setupScratch(); + }); + + afterEach(() => { + teardown(scratch); + }); + + it('should render react-style jsx', () => { + const input = document.createElement('input'); + scratch.appendChild(input); + input.focus(); + expect(document.activeElement).to.equal(input); + + hydrate(, scratch); + expect(document.activeElement).to.equal(input); + }); +}); From 33b485b529f9a8f5d1c07246f7c07f38fdf954f0 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Mon, 23 Dec 2019 14:50:28 +0100 Subject: [PATCH 02/20] make proposal for functional error boundaries --- hooks/src/index.js | 19 +++++++++++++ hooks/test/browser/errorBoundary.test.js | 34 ++++++++++++++++++++++++ src/diff/catch-error.js | 4 ++- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 hooks/test/browser/errorBoundary.test.js diff --git a/hooks/src/index.js b/hooks/src/index.js index 8a62b7bfc3..f9b3f3b423 100644 --- a/hooks/src/index.js +++ b/hooks/src/index.js @@ -222,6 +222,25 @@ export function useDebugValue(value, formatter) { } } +export function errorBoundary(fn, callback) { + let setErr; + + function Boundary(props) { + const result = useState(); + setErr = result[1]; + return fn(props, result[0]); + } + + Boundary._catch = err => { + if (callback) callback(err); + setErr(err); + }; + + Boundary.displayName = 'ErrorBoundary(' + (fn.displayName || fn.name) + ')'; + + return Boundary; +} + /** * After paint effects consumer. */ diff --git a/hooks/test/browser/errorBoundary.test.js b/hooks/test/browser/errorBoundary.test.js new file mode 100644 index 0000000000..8a9be89c46 --- /dev/null +++ b/hooks/test/browser/errorBoundary.test.js @@ -0,0 +1,34 @@ +import { createElement, render } from 'preact'; +import { setupScratch, teardown } from '../../../test/_util/helpers'; +import { errorBoundary } from 'preact/hooks'; +import { setupRerender } from 'preact/test-utils'; + +/** @jsx createElement */ + +describe('errorBoundary', () => { + /** @type {HTMLDivElement} */ + let scratch, rerender; + + beforeEach(() => { + scratch = setupScratch(); + rerender = setupRerender(); + }); + + afterEach(() => { + teardown(scratch); + }); + + it('catches errors', () => { + const Throws = () => { + throw new Error('test'); + }; + + const App = errorBoundary((props, err) => { + return err ?

Error

: ; + }); + + render(, scratch); + rerender(); + expect(scratch.innerHTML).to.equal('

Error

'); + }); +}); diff --git a/src/diff/catch-error.js b/src/diff/catch-error.js index 30335ebad2..a47072055a 100644 --- a/src/diff/catch-error.js +++ b/src/diff/catch-error.js @@ -14,7 +14,9 @@ export function _catchError(error, vnode) { for (; (vnode = vnode._parent); ) { if ((component = vnode._component) && !component._processingException) { try { - if ( + if (vnode.type._catch) { + vnode.type._catch(error); + } else if ( component.constructor && component.constructor.getDerivedStateFromError != null ) { From 6ef51cda88d63abb20935be95c44acafc301e420 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Mon, 23 Dec 2019 17:29:07 +0100 Subject: [PATCH 03/20] add test asserting the callback to be called --- hooks/test/browser/errorBoundary.test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/hooks/test/browser/errorBoundary.test.js b/hooks/test/browser/errorBoundary.test.js index 8a9be89c46..e6cbb03e92 100644 --- a/hooks/test/browser/errorBoundary.test.js +++ b/hooks/test/browser/errorBoundary.test.js @@ -31,4 +31,22 @@ describe('errorBoundary', () => { rerender(); expect(scratch.innerHTML).to.equal('

Error

'); }); + + it('calls the errorBoundary callback', () => { + const spy = sinon.spy(); + const error = new Error('test'); + const Throws = () => { + throw error; + }; + + const App = errorBoundary((props, err) => { + return err ?

Error

: ; + }, spy); + + render(, scratch); + rerender(); + expect(scratch.innerHTML).to.equal('

Error

'); + expect(spy).to.be.calledOnce; + expect(spy).to.be.calledWith(error); + }); }); From e2f002042ab5a20c0efa9d241e1e0cc820e7436b Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Mon, 23 Dec 2019 17:31:11 +0100 Subject: [PATCH 04/20] add typings --- hooks/src/index.d.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/hooks/src/index.d.ts b/hooks/src/index.d.ts index f7e16d2acb..342c07e891 100644 --- a/hooks/src/index.d.ts +++ b/hooks/src/index.d.ts @@ -127,3 +127,13 @@ export function useDebugValue( value: T, formatter?: (value: T) => string | number ): void; + +export interface ComponentFunction

{ + (props: P, error?: Error): preact.ComponentChild; + displayName?: string; +} + +export function errorBoundary

( + fn: ComponentFunction

, + callback: () => Promise | void +): preact.FunctionalComponent

; From 4852f0bd993a663c37d26ab63d304fc4d25490b0 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Thu, 26 Dec 2019 22:06:25 +0100 Subject: [PATCH 05/20] refactor to use componentDidCatch for now --- hooks/src/index.js | 20 ++++++++++---------- src/diff/catch-error.js | 4 +--- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/hooks/src/index.js b/hooks/src/index.js index f9b3f3b423..cd272fdf84 100644 --- a/hooks/src/index.js +++ b/hooks/src/index.js @@ -223,18 +223,18 @@ export function useDebugValue(value, formatter) { } export function errorBoundary(fn, callback) { - let setErr; - function Boundary(props) { - const result = useState(); - setErr = result[1]; - return fn(props, result[0]); - } + const errState = useState(); - Boundary._catch = err => { - if (callback) callback(err); - setErr(err); - }; + if (!currentComponent.componentDidCatch) { + currentComponent.componentDidCatch = err => { + if (callback) callback(err); + errState[1](err); + }; + } + + return fn(props, errState[0]); + } Boundary.displayName = 'ErrorBoundary(' + (fn.displayName || fn.name) + ')'; diff --git a/src/diff/catch-error.js b/src/diff/catch-error.js index a47072055a..30335ebad2 100644 --- a/src/diff/catch-error.js +++ b/src/diff/catch-error.js @@ -14,9 +14,7 @@ export function _catchError(error, vnode) { for (; (vnode = vnode._parent); ) { if ((component = vnode._component) && !component._processingException) { try { - if (vnode.type._catch) { - vnode.type._catch(error); - } else if ( + if ( component.constructor && component.constructor.getDerivedStateFromError != null ) { From a6e33e64e8b88b54c75984bc7636f03293d56531 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Fri, 27 Dec 2019 19:29:19 +0100 Subject: [PATCH 06/20] convert to hook --- hooks/src/index.d.ts | 10 ++------ hooks/src/index.js | 29 +++++++++++------------- hooks/test/browser/errorBoundary.test.js | 12 ++++++---- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/hooks/src/index.d.ts b/hooks/src/index.d.ts index 342c07e891..405b362f8a 100644 --- a/hooks/src/index.d.ts +++ b/hooks/src/index.d.ts @@ -128,12 +128,6 @@ export function useDebugValue( formatter?: (value: T) => string | number ): void; -export interface ComponentFunction

{ - (props: P, error?: Error): preact.ComponentChild; - displayName?: string; -} - -export function errorBoundary

( - fn: ComponentFunction

, +export function useErrorBoundary( callback: () => Promise | void -): preact.FunctionalComponent

; +): [string | undefined, () => void]; diff --git a/hooks/src/index.js b/hooks/src/index.js index cd272fdf84..b69de3fb74 100644 --- a/hooks/src/index.js +++ b/hooks/src/index.js @@ -222,23 +222,20 @@ export function useDebugValue(value, formatter) { } } -export function errorBoundary(fn, callback) { - function Boundary(props) { - const errState = useState(); - - if (!currentComponent.componentDidCatch) { - currentComponent.componentDidCatch = err => { - if (callback) callback(err); - errState[1](err); - }; - } - - return fn(props, errState[0]); +export function useErrorBoundary(cb) { + const errState = useState(); + if (!currentComponent.componentDidCatch) { + currentComponent.componentDidCatch = err => { + if (cb) cb(err); + errState[1](err); + }; } - - Boundary.displayName = 'ErrorBoundary(' + (fn.displayName || fn.name) + ')'; - - return Boundary; + return [ + errState[0], + () => { + errState[1](undefined); + } + ]; } /** diff --git a/hooks/test/browser/errorBoundary.test.js b/hooks/test/browser/errorBoundary.test.js index e6cbb03e92..7548f0bed5 100644 --- a/hooks/test/browser/errorBoundary.test.js +++ b/hooks/test/browser/errorBoundary.test.js @@ -1,6 +1,6 @@ import { createElement, render } from 'preact'; import { setupScratch, teardown } from '../../../test/_util/helpers'; -import { errorBoundary } from 'preact/hooks'; +import { useErrorBoundary } from 'preact/hooks'; import { setupRerender } from 'preact/test-utils'; /** @jsx createElement */ @@ -23,9 +23,10 @@ describe('errorBoundary', () => { throw new Error('test'); }; - const App = errorBoundary((props, err) => { + const App = props => { + const [err] = useErrorBoundary(); return err ?

Error

: ; - }); + }; render(, scratch); rerender(); @@ -39,9 +40,10 @@ describe('errorBoundary', () => { throw error; }; - const App = errorBoundary((props, err) => { + const App = props => { + const [err] = useErrorBoundary(spy); return err ?

Error

: ; - }, spy); + }; render(, scratch); rerender(); From d7ce67f4c45783bfea691bca6a43321899b098b1 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Fri, 27 Dec 2019 19:33:23 +0100 Subject: [PATCH 07/20] add test for resetting the error --- hooks/test/browser/errorBoundary.test.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/hooks/test/browser/errorBoundary.test.js b/hooks/test/browser/errorBoundary.test.js index 7548f0bed5..d608bcab4c 100644 --- a/hooks/test/browser/errorBoundary.test.js +++ b/hooks/test/browser/errorBoundary.test.js @@ -19,18 +19,26 @@ describe('errorBoundary', () => { }); it('catches errors', () => { + let resetErr, + success = false; const Throws = () => { throw new Error('test'); }; const App = props => { - const [err] = useErrorBoundary(); - return err ?

Error

: ; + const [err, reset] = useErrorBoundary(); + resetErr = reset; + return err ?

Error

: success ?

Success

: ; }; render(, scratch); rerender(); expect(scratch.innerHTML).to.equal('

Error

'); + + success = true; + resetErr(); + rerender(); + expect(scratch.innerHTML).to.equal('

Success

'); }); it('calls the errorBoundary callback', () => { From 6de3446887c88e154bfbeee06ad000b4b1f8eb48 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sat, 28 Dec 2019 11:25:36 +0100 Subject: [PATCH 08/20] support combination of getDerived and compDidCatch (#2200) --- src/diff/catch-error.js | 14 ++++++--- test/browser/lifecycles/lifecycle.test.js | 37 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/diff/catch-error.js b/src/diff/catch-error.js index 30335ebad2..1994bb66d9 100644 --- a/src/diff/catch-error.js +++ b/src/diff/catch-error.js @@ -9,7 +9,7 @@ import { enqueueRender } from '../component'; */ export function _catchError(error, vnode) { /** @type {import('../internal').Component} */ - let component; + let component, hasCaught; for (; (vnode = vnode._parent); ) { if ((component = vnode._component) && !component._processingException) { @@ -18,15 +18,19 @@ export function _catchError(error, vnode) { component.constructor && component.constructor.getDerivedStateFromError != null ) { + hasCaught = true; component.setState( component.constructor.getDerivedStateFromError(error) ); - } else if (component.componentDidCatch != null) { + } + + if (component.componentDidCatch != null) { + hasCaught = true; component.componentDidCatch(error); - } else { - continue; } - return enqueueRender((component._pendingError = component)); + + if (hasCaught) + return enqueueRender((component._pendingError = component)); } catch (e) { error = e; } diff --git a/test/browser/lifecycles/lifecycle.test.js b/test/browser/lifecycles/lifecycle.test.js index db15d56c4a..2d0e6ca9be 100644 --- a/test/browser/lifecycles/lifecycle.test.js +++ b/test/browser/lifecycles/lifecycle.test.js @@ -580,6 +580,43 @@ describe('Lifecycle methods', () => { expect(proto.componentDidMount).to.have.been.called; }); + it('should be able to use getDerivedStateFromError and componentDidCatch together', () => { + let didCatch = sinon.spy(), + getDerived = sinon.spy(); + const error = new Error('hi'); + + class Boundary extends Component { + static getDerivedStateFromError(err) { + getDerived(err); + return { err }; + } + + componentDidCatch(err) { + didCatch(err); + } + + render() { + return this.state.err ?
: this.props.children; + } + } + + const ThrowErr = () => { + throw error; + }; + + render( + + + , + scratch + ); + rerender(); + + expect(didCatch).to.have.been.calledWith(error); + + expect(getDerived).to.have.been.calledWith(error); + }); + it('should remove this.base for HOC', () => { let createComponent = (name, fn) => { class C extends Component { From f64088284fbb19af52d39eda13bfa4cdea923388 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 29 Dec 2019 05:16:00 -0500 Subject: [PATCH 09/20] Allow lazy() usage with non-default imports (#2212) `lazy(() => import('./foo').foo)` Co-authored-by: Jovi De Croock --- compat/src/suspense.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compat/src/suspense.js b/compat/src/suspense.js index 974f6c67b4..9051d075f2 100644 --- a/compat/src/suspense.js +++ b/compat/src/suspense.js @@ -129,7 +129,7 @@ export function lazy(loader) { prom = loader(); prom.then( exports => { - component = exports.default; + component = exports.default || exports; }, e => { error = e; From 45328e9a0e734be9d3857c658a7146e297cb156e Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 29 Dec 2019 06:08:25 -0500 Subject: [PATCH 10/20] Shorten and correct renderToString dependency error (#2207) --- compat/server.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/compat/server.js b/compat/server.js index a20ac002cb..9eaeb7622f 100644 --- a/compat/server.js +++ b/compat/server.js @@ -3,9 +3,8 @@ var renderToString; try { renderToString = dep(require('preact-render-to-string')); } catch (e) { - throw new Error( - 'You seem to be missing the "preact-render-to-string" dependency.\n' + - 'You can add this by using "npm install --save preact-render-to-string@next".' + throw Error( + 'renderToString() error: missing "preact-render-to-string" dependency.' ); } From 47d46a00a4f23efa13c4bf97f003f249e48975e1 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 29 Dec 2019 17:07:07 +0100 Subject: [PATCH 11/20] don't assign null when the ref isn't assigned to the current vdom, this occurs when it has already been unmounted --- src/diff/index.js | 3 ++- test/browser/refs.test.js | 24 +++++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/diff/index.js b/src/diff/index.js index b9084ddc31..40a84b689c 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -406,7 +406,8 @@ export function unmount(vnode, parentVNode, skipRemove) { if (options.unmount) options.unmount(vnode); if ((r = vnode.ref)) { - applyRef(r, null, parentVNode); + if (!vnode.ref.current || vnode.ref.current === vnode._dom) + applyRef(r, null, parentVNode); } let dom; diff --git a/test/browser/refs.test.js b/test/browser/refs.test.js index acae7fb7e3..d95059d470 100644 --- a/test/browser/refs.test.js +++ b/test/browser/refs.test.js @@ -92,9 +92,7 @@ describe('refs', () => { const events = []; const App = () => (
events.push('called with ' + (r && r.tagName))}> -

events.push('called with ' + (r && r.tagName))}> - hi -

+

events.push('called with ' + (r && r.tagName))}>hi

); @@ -437,6 +435,26 @@ describe('refs', () => { expect(input.value).to.equal('foo'); }); + it('should correctly set nested child refs', () => { + const ref = createRef(); + const App = ({ open }) => + open ? ( +
+
+
+ ) : ( +
+
+
+ ); + + render(, scratch); + expect(ref.current).to.not.be.null; + + render(, scratch); + expect(ref.current).to.not.be.null; + }); + it('should correctly call child refs for un-keyed children on re-render', () => { let el = null; let ref = e => { From be7a4210d05eb7ea2884281e8743a36c7c8c9a2f Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Mon, 30 Dec 2019 12:07:03 -0500 Subject: [PATCH 12/20] Fix Text nodes being re-rendered unnecessarily (#2215) --- src/diff/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diff/index.js b/src/diff/index.js index b9084ddc31..742d5da15b 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -306,7 +306,7 @@ function diffElementNodes( excessDomChildren[excessDomChildren.indexOf(dom)] = null; } - if (oldProps !== newProps) { + if (oldProps !== newProps && !isHydrating && dom.data != newProps) { dom.data = newProps; } } else if (newVNode !== oldVNode) { From 585164719fe17f29a74055c699c7f59ce4abb0f5 Mon Sep 17 00:00:00 2001 From: thesmartwon Date: Mon, 30 Dec 2019 12:13:42 -0500 Subject: [PATCH 13/20] Add onReset/onFormData to Form Event types (#2209) Add missing events that are [a part of HTML 5.](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset_event) Co-authored-by: Jovi De Croock --- src/jsx.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/jsx.d.ts b/src/jsx.d.ts index dd11b514dc..3b14d6747a 100644 --- a/src/jsx.d.ts +++ b/src/jsx.d.ts @@ -437,6 +437,11 @@ export namespace JSXInternal { onSubmitCapture?: GenericEventHandler; onInvalid?: GenericEventHandler; onInvalidCapture?: GenericEventHandler; + onReset?: GenericEventHandler; + onResetCapture?: GenericEventHandler; + onFormData?: GenericEventHandler; + onFormDataCapture?: GenericEventHandler; + // Keyboard Events onKeyDown?: KeyboardEventHandler; From 54c0f02dd2452597becaeab3f986e7ba8951fe8e Mon Sep 17 00:00:00 2001 From: Bin <124781335@qq.com> Date: Thu, 2 Jan 2020 23:15:45 +0800 Subject: [PATCH 14/20] Update JSDoc comments (#2187) fix: update comments Co-authored-by: Jovi De Croock Co-authored-by: Marvin Hagemeister --- test/_util/dom.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/_util/dom.js b/test/_util/dom.js index 890583f72b..b24bd3edca 100644 --- a/test/_util/dom.js +++ b/test/_util/dom.js @@ -54,13 +54,13 @@ export const li = contents => `
  • ${serialize(contents)}
  • `; export const input = () => ``; /** - * A helper to generate innerHTML validation strings containing inputs + * A helper to generate innerHTML validation strings containing h1 * @param {Contents} contents The contents of the h1 */ export const h1 = contents => `

    ${serialize(contents)}

    `; /** - * A helper to generate innerHTML validation strings containing inputs + * A helper to generate innerHTML validation strings containing h2 * @param {Contents} contents The contents of the h2 */ export const h2 = contents => `

    ${serialize(contents)}

    `; From 6d2bd65f93b87679c721c7e980bae030eb04fa60 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Thu, 2 Jan 2020 21:23:55 +0100 Subject: [PATCH 15/20] use r instead of duplicate ref --- src/diff/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diff/index.js b/src/diff/index.js index 40a84b689c..fe96e0c607 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -406,7 +406,7 @@ export function unmount(vnode, parentVNode, skipRemove) { if (options.unmount) options.unmount(vnode); if ((r = vnode.ref)) { - if (!vnode.ref.current || vnode.ref.current === vnode._dom) + if (!r.current || r.current === vnode._dom) applyRef(r, null, parentVNode); } From 6fcf76b1da6bb28705a9d1513be550063ce09d50 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Thu, 2 Jan 2020 21:41:55 +0100 Subject: [PATCH 16/20] Fix failing tests on IE11 and add flag for flakey tests (#2220) * fix renderer tests by using sinon global * temp test IE11 (remove this when verified) * add polyfills * conditionally skip some tests in IE11 * refactor to skipped tests instead * correct typo * use DISABLE_FLAKEY * disable saucelabs again * rearrange browsers to see if this affects coverage or if cov takes lowest amount * assert correctly * use xit instead of it.skip * arrange correctly * try getting FLAKY from process.env * add flakey logging * revert some changes * add flakey to travis script * add one more flakey test * try some logging to see if we can derive the browser we're in * revert sauceLabs = true Co-authored-by: Marvin Hagemeister --- .travis.yml | 2 +- debug/test/browser/devtools/renderer.test.js | 24 +++++++++++--------- test/polyfills.js | 4 ++++ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4d58a329b6..d273ed6f01 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ addons: sauce_connect: true script: - - COVERAGE=true npm run test + - COVERAGE=true FLAKEY=false npm run test - ./node_modules/coveralls/bin/coveralls.js < ./coverage/lcov.info; after_success: sizereport --config diff --git a/debug/test/browser/devtools/renderer.test.js b/debug/test/browser/devtools/renderer.test.js index a3a9d61d6a..1cfd1d0635 100644 --- a/debug/test/browser/devtools/renderer.test.js +++ b/debug/test/browser/devtools/renderer.test.js @@ -7,7 +7,6 @@ import { createContext } from 'preact'; import { memo, forwardRef, Suspense } from 'preact/compat'; -import * as sinon from 'sinon'; import { createRenderer, getFilteredChildren @@ -28,6 +27,9 @@ import { SUSPENSE } from '../../../src/devtools/10/constants'; +/* global DISABLE_FLAKEY */ +const flakeyIt = DISABLE_FLAKEY ? xit : it; + /** @jsx createElement */ /** @@ -116,7 +118,7 @@ describe('Renderer 10', () => { ]); }); - it('should unmount nodes', () => { + flakeyIt('should unmount nodes', () => { render(
    foo @@ -151,7 +153,7 @@ describe('Renderer 10', () => { ]); }); - it('should mount after filtered update', () => { + flakeyIt('should mount after filtered update', () => { renderer.applyFilters({ regex: [], type: new Set(['dom']) @@ -479,7 +481,7 @@ describe('Renderer 10', () => { ).to.equal(-1); }); - it('should find filtered nodes', () => { + flakeyIt('should find filtered nodes', () => { renderer.applyFilters({ regex: [], type: new Set(['dom']) @@ -605,7 +607,7 @@ describe('Renderer 10', () => { ]); }); - it('should filter by dom type #1', () => { + flakeyIt('should filter by dom type #1', () => { renderer.applyFilters({ regex: [], type: new Set(['dom']) @@ -623,7 +625,7 @@ describe('Renderer 10', () => { ]); }); - it('should filter by dom type #2', () => { + flakeyIt('should filter by dom type #2', () => { renderer.applyFilters({ regex: [], type: new Set(['dom']) @@ -647,7 +649,7 @@ describe('Renderer 10', () => { ]); }); - it('should filter by fragment type', () => { + flakeyIt('should filter by fragment type', () => { renderer.applyFilters({ regex: [], type: new Set(['fragment']) @@ -672,7 +674,7 @@ describe('Renderer 10', () => { ]); }); - it('should filter on update', () => { + flakeyIt('should filter on update', () => { renderer.applyFilters({ regex: [], type: new Set(['dom']) @@ -713,7 +715,7 @@ describe('Renderer 10', () => { ]); }); - it('should update filters after 1st render', () => { + flakeyIt('should update filters after 1st render', () => { renderer.applyFilters({ regex: [], type: new Set(['dom']) @@ -757,7 +759,7 @@ describe('Renderer 10', () => { ]); }); - it('should update filters after 1st render with unmounts', () => { + flakeyIt('should update filters after 1st render with unmounts', () => { renderer.applyFilters({ regex: [], type: new Set(['dom']) @@ -830,7 +832,7 @@ describe('Renderer 10', () => { }); describe('getFilteredChildren', () => { - it('should get direct children', () => { + flakeyIt('should get direct children', () => { const Foo = () =>
    foo
    ; const Bar = () =>
    bar
    ; diff --git a/test/polyfills.js b/test/polyfills.js index 2ded413ad4..3a9141c105 100644 --- a/test/polyfills.js +++ b/test/polyfills.js @@ -7,6 +7,10 @@ import 'core-js/fn/array/find'; import 'core-js/fn/array/includes'; import 'core-js/fn/string/includes'; import 'core-js/fn/object/assign'; +import 'core-js/fn/string/starts-with'; +import 'core-js/fn/string/code-point-at'; +import 'core-js/fn/string/from-code-point'; +import 'core-js/fn/string/repeat'; // Fix Function#name on browsers that do not support it (IE). // Taken from: https://stackoverflow.com/a/17056530/755391 From c3239535f557d1192901f46a3317cc2b2b2fa865 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Fri, 3 Jan 2020 14:14:39 +0100 Subject: [PATCH 17/20] avoid stale closure in error boundary (#2225) --- hooks/src/index.js | 4 +++- hooks/test/browser/errorBoundary.test.js | 30 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/hooks/src/index.js b/hooks/src/index.js index b69de3fb74..98a0d84853 100644 --- a/hooks/src/index.js +++ b/hooks/src/index.js @@ -223,10 +223,12 @@ export function useDebugValue(value, formatter) { } export function useErrorBoundary(cb) { + const state = getHookState(currentIndex++); const errState = useState(); + state._value = cb; if (!currentComponent.componentDidCatch) { currentComponent.componentDidCatch = err => { - if (cb) cb(err); + if (state._value) state._value(err); errState[1](err); }; } diff --git a/hooks/test/browser/errorBoundary.test.js b/hooks/test/browser/errorBoundary.test.js index d608bcab4c..4c2f7f3927 100644 --- a/hooks/test/browser/errorBoundary.test.js +++ b/hooks/test/browser/errorBoundary.test.js @@ -59,4 +59,34 @@ describe('errorBoundary', () => { expect(spy).to.be.calledOnce; expect(spy).to.be.calledWith(error); }); + + it('does not leave a stale closure', () => { + const spy = sinon.spy(), + spy2 = sinon.spy(); + let resetErr; + const error = new Error('test'); + const Throws = () => { + throw error; + }; + + const App = props => { + const [err, reset] = useErrorBoundary(props.onError); + resetErr = reset; + return err ?

    Error

    : ; + }; + + render(, scratch); + rerender(); + expect(scratch.innerHTML).to.equal('

    Error

    '); + expect(spy).to.be.calledOnce; + expect(spy).to.be.calledWith(error); + + resetErr(); + render(, scratch); + rerender(); + expect(scratch.innerHTML).to.equal('

    Error

    '); + expect(spy).to.be.calledOnce; + expect(spy2).to.be.calledOnce; + expect(spy2).to.be.calledWith(error); + }); }); From 37e8483ab228f22fad2d7cd6bb05af5063744af6 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 5 Jan 2020 17:29:35 +0100 Subject: [PATCH 18/20] Add tests highlighting issues of Suspense (#2229) --- compat/test/browser/suspense.test.js | 103 +++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/compat/test/browser/suspense.test.js b/compat/test/browser/suspense.test.js index 3d7949f81c..5eb18c7144 100644 --- a/compat/test/browser/suspense.test.js +++ b/compat/test/browser/suspense.test.js @@ -1328,4 +1328,107 @@ describe('suspense', () => { expect(cWUSpy.getCall(0).thisValue).to.eql(suspender); expect(scratch.innerHTML).to.eql(`
    conditional hide
    `); }); + + xit('should support sCU=false when un-suspending', () => { + // See #2176 #2125 + const [Suspender, suspend] = createSuspender(() =>
    Hello
    ); + + render( + Suspended...
    }> + Text + {/* Adding a
    here will make things work... */} + + , + scratch + ); + + expect(scratch.innerHTML).to.eql(`Text
    Hello
    `); + + const [resolve] = suspend(); + rerender(); + + expect(scratch.innerHTML).to.eql(`
    Suspended...
    `); + + Suspender.prototype.shouldComponentUpdate = () => false; + + return resolve(() =>
    Hello 2
    ).then(() => { + rerender(); + expect(scratch.innerHTML).to.eql(`Text
    Hello 2
    `); + }); + }); + + xit('should allow suspended children to update', () => { + const log = []; + class Logger extends Component { + constructor(props) { + super(props); + log.push('construct'); + } + + render({ children }) { + log.push('render'); + return children; + } + } + + let suspender; + class Suspender extends Component { + constructor(props) { + super(props); + this.state = { promise: new Promise(() => {}) }; + suspender = this; + } + + unsuspend() { + this.setState({ promise: null }); + } + + render() { + if (this.state.promise) { + throw this.state.promise; + } + + return 'hello'; + } + } + + render( +
    + fallback
    }> + + + + , + scratch + ); + + expect(log).to.eql(['construct', 'render']); + expect(scratch.innerHTML).to.eql('
    '); + + // this rerender is needed because of Suspense issuing a forceUpdate itself + rerender(); + expect(scratch.innerHTML).to.eql('
    fallback
    '); + + suspender.unsuspend(); + + rerender(); + + /** + * These currently failing assertion shows the issue that we currently unmount + * the suspended tree (unlike react, which adds a display="none") and block any + * further processing on that tree. Thus updates below a suspended Suspense are + * getting lost. + */ + expect(log).to.eql(['construct', 'render', 'render']); + + /** + * When the above assertion will hold true we will certainly run into the second issue + * here. The problem is that we do not remove suspensions from an instance of Suspense + * when one of its suspending children no longer throws because of a state + * update. + */ + expect(scratch.innerHTML).to.eql( + '
    Suspender un-suspended
    ' + ); + }); }); From 398eb99fa28e62b19b2791c9ba1ae0b086e4724d Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 5 Jan 2020 17:01:17 -0500 Subject: [PATCH 19/20] Update package metadata (#2230) --- package.json | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 7b79cac69e..deb04efe3f 100644 --- a/package.json +++ b/package.json @@ -123,23 +123,19 @@ "keywords": [ "preact", "react", + "ui", + "user interface", "virtual dom", "vdom", "components", - "virtual", - "dom" + "dom diff" ], "authors": [ "Jason Miller " ], - "repository": { - "type": "git", - "url": "https://github.com/preactjs/preact.git" - }, - "bugs": { - "url": "https://github.com/preactjs/preact/issues" - }, - "homepage": "https://github.com/preactjs/preact", + "repository": "preactjs/preact", + "bugs": "https://github.com/preactjs/preact/issues", + "homepage": "https://preactjs.com", "devDependencies": { "@babel/core": "^7.7.0", "@babel/plugin-proposal-object-rest-spread": "^7.6.2", From 9566966728d412dfa5c9f7c8a2228acb21920352 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Tue, 7 Jan 2020 16:22:30 +0100 Subject: [PATCH 20/20] modernize our README (#2232) * modernize our README * remove preact-boilerplate * remove links * remove package size badge * Update README.md Co-Authored-By: Jason Miller * Update README.md Co-Authored-By: Jason Miller * apply wording suggestions * edit link to awesome-preact Co-authored-by: Jason Miller --- README.md | 479 ++++-------------------------------------------------- 1 file changed, 29 insertions(+), 450 deletions(-) diff --git a/README.md b/README.md index e07c8069ec..989f74fb88 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ **All the power of Virtual DOM components, without the overhead:** -- Familiar React API & patterns: [ES6 Class] and [Functional Components] +- Familiar React API & patterns: ES6 Class, hooks, and Functional Components - Extensive React compatibility via a simple [preact/compat] alias - Everything you need: JSX, VDOM, React DevTools, HMR, SSR.. - A highly optimized diff algorithm and seamless Server Side Rendering @@ -21,142 +21,25 @@ --- - - -- [Demos](#demos) -- [Libraries & Add-ons](#libraries--add-ons) -- [Getting Started](#getting-started) - - [Import what you need](#import-what-you-need) - - [Rendering JSX](#rendering-jsx) - - [Components](#components) - - [Props & State](#props--state) -- [Linked State](#linked-state) -- [Examples](#examples) -- [Extensions](#extensions) -- [Debug Mode](#debug-mode) -- [Backers](#backers) -- [Sponsors](#sponsors) -- [License](#license) - - - - # Preact [![npm](https://img.shields.io/npm/v/preact.svg)](http://npm.im/preact) -[![CDNJS](https://img.shields.io/cdnjs/v/preact.svg)](https://cdnjs.com/libraries/preact) [![Preact Slack Community](https://preact-slack.now.sh/badge.svg)](https://preact-slack.now.sh) [![OpenCollective Backers](https://opencollective.com/preact/backers/badge.svg)](#backers) [![OpenCollective Sponsors](https://opencollective.com/preact/sponsors/badge.svg)](#sponsors) [![travis](https://travis-ci.org/preactjs/preact.svg?branch=master)](https://travis-ci.org/preactjs/preact) [![coveralls](https://img.shields.io/coveralls/preactjs/preact/master.svg)](https://coveralls.io/github/preactjs/preact) [![gzip size](http://img.badgesize.io/https://unpkg.com/preact/dist/preact.min.js?compression=gzip)](https://unpkg.com/preact/dist/preact.min.js) -[![install size](https://packagephobia.now.sh/badge?p=preact)](https://packagephobia.now.sh/result?p=preact) +[![brotli size](http://img.badgesize.io/https://unpkg.com/preact/dist/preact.min.js?compression=brotli)](https://unpkg.com/preact/dist/preact.min.js) -Preact supports modern browsers and IE9+: +Preact supports modern browsers and IE11+: [![Browsers](https://saucelabs.com/browser-matrix/preact.svg)](https://saucelabs.com/u/preact) --- - -## Demos - -#### Real-World Apps - -- [**Preact Hacker News**](https://hn.kristoferbaxter.com) _([GitHub Project](https://github.com/kristoferbaxter/preact-hn))_ -- [**Play.cash**](https://play.cash) :notes: _([GitHub Project](https://github.com/feross/play.cash))_ -- [**Songsterr**](https://www.songsterr.com) 🎼 Using Preact X in production since 10.0 alpha -- [**BitMidi**](https://bitmidi.com/) 🎹 Wayback machine for free MIDI files _([GitHub Project](https://github.com/feross/bitmidi.com))_ -- [**Ultimate Guitar**](https://www.ultimate-guitar.com) 🎸speed boosted by Preact. -- [**ESBench**](http://esbench.com) is built using Preact. -- [**BigWebQuiz**](https://bigwebquiz.com) _([GitHub Project](https://github.com/jakearchibald/big-web-quiz))_ -- [**Nectarine.rocks**](http://nectarine.rocks) _([GitHub Project](https://github.com/developit/nectarine))_ :peach: -- [**TodoMVC**](https://preact-todomvc.surge.sh) _([GitHub Project](https://github.com/developit/preact-todomvc))_ -- [**OSS.Ninja**](https://oss.ninja) _([GitHub Project](https://github.com/developit/oss.ninja))_ -- [**GuriVR**](https://gurivr.com) _([GitHub Project](https://github.com/opennewslabs/guri-vr))_ -- [**Color Picker**](https://colors.now.sh) _([GitHub Project](https://github.com/lukeed/colors-app))_ :art: -- [**Offline Gallery**](https://use-the-platform.com/offline-gallery/) _([GitHub Project](https://github.com/vaneenige/offline-gallery/))_ :balloon: -- [**Periodic Weather**](https://use-the-platform.com/periodic-weather/) _([GitHub Project](https://github.com/vaneenige/periodic-weather/))_ :sunny: -- [**Rugby News Board**](http://nbrugby.com) _[(GitHub Project)](https://github.com/rugby-board/rugby-board-node)_ -- [**Preact Gallery**](https://preact.gallery/) an 8KB photo gallery PWA built using Preact. -- [**Rainbow Explorer**](https://use-the-platform.com/rainbow-explorer/) Preact app to translate real life color to digital color _([Github project](https://github.com/vaneenige/rainbow-explorer))_. -- [**YASCC**](https://carlosqsilva.github.io/YASCC/#/) Yet Another SoundCloud Client _([Github project](https://github.com/carlosqsilva/YASCC))_. -- [**Journalize**](https://preact-journal.herokuapp.com/) 14k offline-capable journaling PWA using preact. _([Github project](https://github.com/jpodwys/preact-journal))_. -- [**Proxx**](https://proxx.app) A game of proximity by GoogleChromeLabs using preact. _([Github project](https://github.com/GoogleChromeLabs/proxx))_. -- [**Web Maker**](https://webmaker.app) An offline and blazing fast frontend playground built using Preact. _([Github project](https://github.com/chinchang/web-maker))_. -- [**Intergram**](https://www.intergram.xyz) A live chat widget linked to your Telegram messenger built using Preact. _([Github project](https://github.com/idoco/intergram))_. - - -#### Runnable Examples - -- [**Flickr Browser**](http://codepen.io/developit/full/VvMZwK/) (@ CodePen) -- [**Animating Text**](http://codepen.io/developit/full/LpNOdm/) (@ CodePen) -- [**60FPS Rainbow Spiral**](http://codepen.io/developit/full/xGoagz/) (@ CodePen) -- [**Simple Clock**](http://jsfiddle.net/developit/u9m5x0L7/embedded/result,js/) (@ JSFiddle) -- [**3D + ThreeJS**](http://codepen.io/developit/pen/PPMNjd?editors=0010) (@ CodePen) -- [**Stock Ticker**](http://codepen.io/developit/pen/wMYoBb?editors=0010) (@ CodePen) -- [*Create your Own!*](https://jsfiddle.net/developit/rs6zrh5f/embedded/result/) (@ JSFiddle) - -### Starter Projects - -- [**Preact Boilerplate**](https://preact-boilerplate.surge.sh) _([GitHub Project](https://github.com/developit/preact-boilerplate))_ :zap: -- [**Preact Offline Starter**](https://preact-starter.now.sh) _([GitHub Project](https://github.com/lukeed/preact-starter))_ :100: -- [**Preact PWA**](https://preact-pwa-yfxiijbzit.now.sh/) _([GitHub Project](https://github.com/ezekielchentnik/preact-pwa))_ :hamburger: -- [**Parcel + Preact + Unistore Starter**](https://github.com/hwclass/parcel-preact-unistore-starter) -- [**Preact Mobx Starter**](https://awaw00.github.io/preact-mobx-starter/) _([GitHub Project](https://github.com/awaw00/preact-mobx-starter))_ :sunny: -- [**Preact Redux Example**](https://github.com/developit/preact-redux-example) :star: -- [**Preact Redux/RxJS/Reselect Example**](https://github.com/continuata/preact-seed) -- [**V2EX Preact**](https://github.com/yanni4night/v2ex-preact) -- [**Preact Coffeescript**](https://github.com/crisward/preact-coffee) -- [**Preact + TypeScript + Webpack**](https://github.com/k1r0s/bleeding-preact-starter) -- [**0 config => Preact + Poi**](https://github.com/k1r0s/preact-poi-starter) -- [**Zero configuration => Preact + Typescript + Parcel**](https://github.com/aalises/preact-typescript-parcel-starter) - ---- - -## Libraries & Add-ons - -- :raised_hands: [**preact-compat**](https://git.io/preact-compat): use any React library with Preact *([full example](http://git.io/preact-compat-example))* -- :page_facing_up: [**preact-render-to-string**](https://git.io/preact-render-to-string): Universal rendering. -- :eyes: [**preact-render-spy**](https://github.com/mzgoddard/preact-render-spy): Enzyme-lite: Renderer with access to the produced virtual dom for testing. -- :loop: [**preact-render-to-json**](https://git.io/preact-render-to-json): Render for Jest Snapshot testing. -- :earth_americas: [**preact-router**](https://git.io/preact-router): URL routing for your components -- :bookmark_tabs: [**preact-markup**](https://git.io/preact-markup): Render HTML & Custom Elements as JSX & Components -- :satellite: [**preact-portal**](https://git.io/preact-portal): Render Preact components into (a) SPACE :milky_way: -- :pencil: [**preact-richtextarea**](https://git.io/preact-richtextarea): Simple HTML editor component -- :bookmark: [**preact-token-input**](https://github.com/developit/preact-token-input): Text field that tokenizes input, for things like tags -- :card_index: [**preact-virtual-list**](https://github.com/developit/preact-virtual-list): Easily render lists with millions of rows ([demo](https://jsfiddle.net/developit/qqan9pdo/)) -- :repeat: [**preact-cycle**](https://git.io/preact-cycle): Functional-reactive paradigm for Preact -- :triangular_ruler: [**preact-layout**](https://download.github.io/preact-layout/): Small and simple layout library -- :thought_balloon: [**preact-socrates**](https://github.com/matthewmueller/preact-socrates): Preact plugin for [Socrates](http://github.com/matthewmueller/socrates) -- :rowboat: [**preact-flyd**](https://github.com/xialvjun/preact-flyd): Use [flyd](https://github.com/paldepind/flyd) FRP streams in Preact + JSX -- :speech_balloon: [**preact-i18nline**](https://github.com/download/preact-i18nline): Integrates the ecosystem around [i18n-js](https://github.com/everydayhero/i18n-js) with Preact via [i18nline](https://github.com/download/i18nline). -- :microscope: [**preact-jsx-chai**](https://git.io/preact-jsx-chai): JSX assertion testing _(no DOM, right in Node)_ -- :tophat: [**preact-classless-component**](https://github.com/ld0rman/preact-classless-component): create preact components without the class keyword -- :hammer: [**preact-hyperscript**](https://github.com/queckezz/preact-hyperscript): Hyperscript-like syntax for creating elements -- :white_check_mark: [**shallow-compare**](https://github.com/tkh44/shallow-compare): simplified `shouldComponentUpdate` helper. -- :shaved_ice: [**preact-codemod**](https://github.com/vutran/preact-codemod): Transform your React code to Preact. -- :construction_worker: [**preact-helmet**](https://github.com/download/preact-helmet): A document head manager for Preact -- :necktie: [**preact-delegate**](https://github.com/NekR/preact-delegate): Delegate DOM events -- :art: [**preact-stylesheet-decorator**](https://github.com/k1r0s/preact-stylesheet-decorator): Add Scoped Stylesheets to your Preact Components -- :electric_plug: [**preact-routlet**](https://github.com/k1r0s/preact-routlet): Simple `Component Driven` Routing for Preact using ES7 Decorators -- :fax: [**preact-bind-group**](https://github.com/k1r0s/preact-bind-group): Preact Forms made easy, Group Events into a Single Callback -- :hatching_chick: [**preact-habitat**](https://github.com/zouhir/preact-habitat): Declarative Preact widgets renderer in any CMS or DOM host ([demo](https://codepen.io/zouhir/pen/brrOPB)). -- :tada: [**proppy-preact**](https://github.com/fahad19/proppy): Functional props composition for Preact components - -#### UI Component Libraries - -> Want to prototype something or speed up your development? Try one of these toolkits: - -- [**preact-material-components**](https://github.com/prateekbh/preact-material-components): Material Design Components for Preact ([website](https://material.preactjs.com/)) -- [**preact-mdc**](https://github.com/BerndWessels/preact-mdc): Material Design Components for Preact ([demo](https://github.com/BerndWessels/preact-mdc-demo)) -- [**preact-mui**](https://git.io/v1aVO): The MUI CSS Preact library. -- [**preact-photon**](https://git.io/preact-photon): build beautiful desktop UI with [photon](http://photonkit.com) -- [**preact-mdl**](https://git.io/preact-mdl): [Material Design Lite](https://getmdl.io) for Preact -- [**preact-weui**](https://github.com/afeiship/preact-weui): [Weui](https://github.com/afeiship/preact-weui) for Preact - +You can find some awesome libraries in the [awesome-preact list](https://github.com/preactjs/awesome-preact) :sunglasses: --- @@ -166,349 +49,49 @@ Preact supports modern browsers and IE9+: The easiest way to get started with Preact is to install [Preact CLI](https://github.com/preactjs/preact-cli). This simple command-line tool wraps up the best possible Webpack and Babel setup for you, and even keeps you up-to-date as the underlying tools change. Best of all, it's easy to understand! It builds your app in a single command (`preact build`), doesn't need any configuration, and bakes in best practices 🙌. -The following guide assumes you have some sort of ES2015 build set up using babel and/or webpack/browserify/gulp/grunt/etc. - -You can also start with [preact-boilerplate] or a [CodePen Template](http://codepen.io/developit/pen/pgaROe?editors=0010). - - -### Import what you need +### Getting Started -The `preact` module provides both named and default exports, so you can either import everything under a namespace of your choosing, or just what you need as locals: +With Preact, you create user interfaces by assembling trees of components and elements. Components are functions or classes that return a description of what their tree should output. These descriptions are typically written in [JSX](https://facebook.github.io/jsx/) (shown underneath), or [HTM](https://github.com/developit/htm) which leverages standard JavaScript Tagged Templates. Both syntaxes can express trees of elements with "props" (similar to HTML attributes) and children. -##### Named: - -```js -import { h, render, Component } from 'preact'; - -// Tell Babel to transform JSX into h() calls: -/** @jsx h */ -``` - -##### Default: - -```js -import preact from 'preact'; - -// Tell Babel to transform JSX into preact.h() calls: -/** @jsx preact.h */ -``` - -> Named imports work well for highly structured applications, whereas the default import is quick and never needs to be updated when using different parts of the library. -> -> Instead of declaring the `@jsx` pragma in your code, it's best to configure it globally in a `.babelrc`: -> -> **For Babel 5 and prior:** -> -> ```json -> { "jsxPragma": "h" } -> ``` -> -> **For Babel 6:** -> -> ```json -> { -> "plugins": [ -> ["transform-react-jsx", { "pragma":"h" }] -> ] -> } -> ``` -> -> **For Babel 7:** -> -> ```json -> { -> "plugins": [ -> ["@babel/plugin-transform-react-jsx", { "pragma":"h" }] -> ] -> } -> ``` -> **For using Preact along with TypeScript add to `tsconfig.json`:** -> -> ```json -> { -> "jsx": "react", -> "jsxFactory": "h", -> } -> ``` - - -### Rendering JSX - -Out of the box, Preact provides an `h()` function that turns your JSX into Virtual DOM elements _([here's how](http://jasonformat.com/wtf-is-jsx))_. It also provides a `render()` function that creates a DOM tree from that Virtual DOM. - -To render some JSX, just import those two functions and use them like so: +To get started using Preact, first look at the render() function. This function accepts a tree description and creates the structure described. Next, it appends this structure to a parent DOM element provided as the second argument. Future calls to render() will use the existing tree and update it in-place in the DOM. Internally, render() will calculate the difference from previous outputted structures in an attempt to perform as few DOM operations as possible. ```js import { h, render } from 'preact'; +// Tells babel to use h for JSX. It's better to configure this globally. +// See https://babeljs.io/docs/en/babel-plugin-transform-react-jsx#usage +// In tsconfig you can specify this with the jsxFactory +/** @jsx h */ -render(( -
    - Hello, world! - -
    -), document.body); -``` - -This should seem pretty straightforward if you've used hyperscript or one of its many friends. If you're not, the short of it is that the `h()` function import gets used in the final, transpiled code as a drop in replacement for `React.createElement()`, and so needs to be imported even if you don't explicitly use it in the code you write. Also note that if you're the kind of person who likes writing your React code in "pure JavaScript" (you know who you are) you will need to use `h()` wherever you would otherwise use `React.createElement()`. - -Rendering hyperscript with a virtual DOM is pointless, though. We want to render components and have them updated when data changes - that's where the power of virtual DOM diffing shines. :star2: - - -### Components - -Preact exports a generic `Component` class, which can be extended to build encapsulated, self-updating pieces of a User Interface. Components support all of the standard React [lifecycle methods], like `shouldComponentUpdate()` and `componentWillReceiveProps()`. Providing specific implementations of these methods is the preferred mechanism for controlling _when_ and _how_ components update. - -Components also have a `render()` method, but unlike React this method is passed `(props, state)` as arguments. This provides an ergonomic means to destructure `props` and `state` into local variables to be referenced from JSX. - -Let's take a look at a very simple `Clock` component, which shows the current time. - -```js -import { h, render, Component } from 'preact'; - -class Clock extends Component { - render() { - let time = new Date(); - return ; - } -} - -// render an instance of Clock into : -render(, document.body); -``` - - -That's great. Running this produces the following HTML DOM structure: - -```html -10:28:57 PM -``` - -In order to have the clock's time update every second, we need to know when `` gets mounted to the DOM. _If you've used HTML5 Custom Elements, this is similar to the `attachedCallback` and `detachedCallback` lifecycle methods._ Preact invokes the following lifecycle methods if they are defined for a Component: - -| Lifecycle method | When it gets called | -|-----------------------------|--------------------------------------------------| -| `componentWillMount` | before the component gets mounted to the DOM | -| `componentDidMount` | after the component gets mounted to the DOM | -| `componentWillUnmount` | prior to removal from the DOM | -| `componentWillReceiveProps` | before new props get accepted | -| `shouldComponentUpdate` | before `render()`. Return `false` to skip render | -| `componentWillUpdate` | before `render()` | -| `componentDidUpdate` | after `render()` | - - - -So, we want to have a 1-second timer start once the Component gets added to the DOM, and stop if it is removed. We'll create the timer and store a reference to it in `componentDidMount()`, and stop the timer in `componentWillUnmount()`. On each timer tick, we'll update the component's `state` object with a new time value. Doing this will automatically re-render the component. - -```js -import { h, render, Component } from 'preact'; - -class Clock extends Component { - constructor() { - super(); - // set initial time: - this.state = { - time: Date.now() - }; - } - - componentDidMount() { - // update time every second - this.timer = setInterval(() => { - this.setState({ time: Date.now() }); - }, 1000); - } - - componentWillUnmount() { - // stop when not renderable - clearInterval(this.timer); - } - - render(props, state) { - let time = new Date(state.time).toLocaleTimeString(); - return { time }; - } -} - -// render an instance of Clock into : -render(, document.body); -``` - -Now we have [a ticking clock](http://jsfiddle.net/developit/u9m5x0L7/embedded/result,js/)! - - -### Props & State - -The concept (and nomenclature) for `props` and `state` is the same as in React. `props` are passed to a component by defining attributes in JSX, `state` is internal state. Changing either triggers a re-render, though by default Preact re-renders Components asynchronously for `state` changes and synchronously for `props` changes. You can tell Preact to render `prop` changes asynchronously by setting `options.syncComponentUpdates` to `false`. - - ---- - - -## Linked State - -One area Preact takes a little further than React is in optimizing state changes. A common pattern in ES2015 React code is to use Arrow functions within a `render()` method in order to update state in response to events. Creating functions enclosed in a scope on every render is inefficient and forces the garbage collector to do more work than is necessary. - -One solution to this is to bind component methods declaratively. -Here is an example using [decko](http://git.io/decko): - -```js -class Foo extends Component { - @bind - updateText(e) { - this.setState({ text: e.target.value }); - } - render({ }, { text }) { - return ; - } -} -``` - -While this achieves much better runtime performance, it's still a lot of unnecessary code to wire up state to UI. - -Fortunately there is a solution, in the form of a module called [linkstate](https://github.com/developit/linkstate). Calling `linkState(component, 'text')` returns a function that accepts an Event and uses its associated value to update the given property in your component's state. Calls to `linkState()` with the same arguments are cached, so there is no performance penalty. Here is the previous example rewritten using _Linked State_: - -```js -import linkState from 'linkstate'; - -class Foo extends Component { - render({ }, { text }) { - return ; - } -} -``` - -Simple and effective. It handles linking state from any input type, or an optional second parameter can be used to explicitly provide a keypath to the new state value. - -> **Note:** In Preact 7 and prior, `linkState()` was built right into Component. In 8.0, it was moved to a separate module. You can restore the 7.x behavior by using linkstate as a polyfill - see [the linkstate docs](https://github.com/developit/linkstate#usage). - - - -## Examples - -Here is a somewhat verbose Preact `` component: - -```js -class Link extends Component { - render(props, state) { - return {props.children}; - } -} -``` - -Since this is ES6/ES2015, we can further simplify: - -```js -class Link extends Component { - render({ href, children }) { - return ; - } -} - -// or, for wide-open props support: -class Link extends Component { - render(props) { - return ; - } -} +// create our tree and append it to document.body: +render(

    Hello

    , document.body); -// or, as a stateless functional component: -const Link = ({ children, ...props }) => ( -
    { children } -); +// update the tree in-place: +render(

    Hello World!

    , document.body); +// ^ this second invocation of render(...) will use a single DOM call to update the text of the

    ``` - -## Extensions - -It is likely that some projects based on Preact would wish to extend Component with great new functionality. - -Perhaps automatic connection to stores for a Flux-like architecture, or mixed-in context bindings to make it feel more like `React.createClass()`. Just use ES2015 inheritance: +Hooray! render() has taken our structure and output a User Interface! This approach demonstrates a simple case, but would be difficult to use as an application grows in complexity. Each change would be forced to calculate the difference between the current and updated stucture for the entire application. Components can help here – by dividing the User Interface into nested Components each can calculate their difference from their mounted point. Here's an example: ```js -class BoundComponent extends Component { - constructor(props) { - super(props); - this.bind(); - } - bind() { - this.binds = {}; - for (let i in this) { - this.binds[i] = this[i].bind(this); - } - } -} - -// example usage -class Link extends BoundComponent { - click() { - open(this.href); - } - render() { - let { click } = this.binds; - return { children }; - } -} -``` +import { render, h } from 'preact'; +import { useState } from 'preact/hooks'; +/** @jsx h */ -The possibilities are pretty endless here. You could even add support for rudimentary mixins: +const App = () => { + const [input, setInput] = useState(''); -```js -class MixedComponent extends Component { - constructor() { - super(); - (this.mixins || []).forEach( m => Object.assign(this, m) ); - } + return ( +
    +

    Do you agree to the statement: "Preact is awesome"?

    + setInput(e.target.value)} /> +
    + ) } -``` - -## Debug Mode - -You can inspect and modify the state of your Preact UI components at runtime using the -[React Developer Tools](https://github.com/facebook/react-devtools) browser extension. -1. Install the [React Developer Tools](https://github.com/facebook/react-devtools) extension -2. Import the "preact/debug" module in your app -3. Set `process.env.NODE_ENV` to 'development' -4. Reload and go to the 'React' tab in the browser's development tools - - -```js -import { h, Component, render } from 'preact'; - -// Enable debug mode. You can reduce the size of your app by only including this -// module in development builds. eg. In Webpack, wrap this with an `if (module.hot) {...}` -// check. -require('preact/debug'); -``` - -### Runtime Error Checking - -To enable debug mode, you need to set `process.env.NODE_ENV=development`. You can do this -with webpack via a builtin plugin. - -```js -// webpack.config.js - -// Set NODE_ENV=development to enable error checking -new webpack.DefinePlugin({ - 'process.env': { - 'NODE_ENV': JSON.stringify('development') - } -}); +render(, document.body); ``` -When enabled, warnings are logged to the console when undefined components or string refs -are detected. - -### Developer Tools (v8 or earlier) - -If you only want to include devtool integration, without runtime error checking, you can -replace `preact/debug` in the above example with `preact/devtools`. This option doesn't -require setting `NODE_ENV=development`. - - - ## Backers Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/preact#backer)] @@ -588,8 +171,4 @@ MIT [preact/compat]: https://github.com/preactjs/preact/tree/master/compat -[ES6 Class]: https://facebook.github.io/react/docs/reusable-components.html#es6-classes -[Functional Components]: https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html#stateless-functional-components [hyperscript]: https://github.com/dominictarr/hyperscript -[preact-boilerplate]: https://github.com/developit/preact-boilerplate -[lifecycle methods]: https://facebook.github.io/react/docs/component-specs.html