From 0bd436593ad43cb68584e9751c5371c0d582149a Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 13 Oct 2018 11:51:13 -0700 Subject: [PATCH 1/7] Move component construction UTs into a common describe block --- test/browser/components.js | 142 +++++++++++++++++++++++++------------ 1 file changed, 95 insertions(+), 47 deletions(-) diff --git a/test/browser/components.js b/test/browser/components.js index 73d28ea3e1..6bd2db8bc6 100644 --- a/test/browser/components.js +++ b/test/browser/components.js @@ -41,71 +41,119 @@ describe('Components', () => { teardown(scratch); }); - it('should render components', () => { - class C1 extends Component { - render() { - return
C1
; + describe('Component construction', () => { + + it('should render components', () => { + class C1 extends Component { + render() { + return
C1
; + } } - } - sinon.spy(C1.prototype, 'render'); - render(, scratch); + sinon.spy(C1.prototype, 'render'); + render(, scratch); - expect(C1.prototype.render) - .to.have.been.calledOnce - .and.to.have.been.calledWithMatch({}, {}) - .and.to.have.returned(sinon.match({ tag: 'div' })); + expect(C1.prototype.render) + .to.have.been.calledOnce + .and.to.have.been.calledWithMatch({}, {}) + .and.to.have.returned(sinon.match({ tag: 'div' })); - expect(scratch.innerHTML).to.equal('
C1
'); - }); + expect(scratch.innerHTML).to.equal('
C1
'); + }); - it('should render functional components', () => { - const PROPS = { foo: 'bar', onBaz: () => {} }; + it('should render functional components', () => { + const PROPS = { foo: 'bar', onBaz: () => {} }; - const C3 = sinon.spy( props =>
); + const C3 = sinon.spy( props =>
); - render(, scratch); + render(, scratch); - expect(C3) - .to.have.been.calledOnce - .and.to.have.been.calledWithMatch(PROPS) - .and.to.have.returned(sinon.match({ - tag: 'div', - props: PROPS - })); + expect(C3) + .to.have.been.calledOnce + .and.to.have.been.calledWithMatch(PROPS) + .and.to.have.returned(sinon.match({ + tag: 'div', + props: PROPS + })); - expect(scratch.innerHTML).to.equal('
'); - }); + expect(scratch.innerHTML).to.equal('
'); + }); - it('should render components with props', () => { - const PROPS = { foo: 'bar', onBaz: () => {} }; - let constructorProps; + it('should render components with props', () => { + const PROPS = { foo: 'bar', onBaz: () => {} }; + let constructorProps; - class C2 extends Component { - constructor(props) { - super(props); - constructorProps = props; + class C2 extends Component { + constructor(props) { + super(props); + constructorProps = props; + } + render(props) { + return
; + } } - render(props) { - return
; + sinon.spy(C2.prototype, 'render'); + + render(, scratch); + + expect(constructorProps).to.deep.equal(PROPS); + + expect(C2.prototype.render) + .to.have.been.calledOnce + .and.to.have.been.calledWithMatch(PROPS, {}) + .and.to.have.returned(sinon.match({ + tag: 'div', + props: PROPS + })); + + expect(scratch.innerHTML).to.equal('
'); + }); + + + it('should render Component classes that don\'t pass args into the Component constructor', () => { + + /** @type {object} */ + let instance; + const props = { text: 'Hello' }; + + function Foo () { + Component.call(this); + instance = this; } - } - sinon.spy(C2.prototype, 'render'); + Foo.prototype.render = sinon.spy((props, state, context) =>
{props.text}
); - render(, scratch); + render(h(Foo, props), scratch); - expect(constructorProps).to.deep.equal(PROPS); + expect(scratch.innerHTML).to.equal('
Hello
'); + expect(Foo.prototype.render).to.have.been.calledOnceWith(props, {}, {}); + expect(instance.props).to.deep.equal(props); + expect(instance.state).to.deep.equal({}); + expect(instance.context).to.deep.equal({}); + }); - expect(C2.prototype.render) - .to.have.been.calledOnce - .and.to.have.been.calledWithMatch(PROPS, {}) - .and.to.have.returned(sinon.match({ - tag: 'div', - props: PROPS - })); + it('should render Component classes that don\'t pass args into the Component constructor and initialize state', () => { - expect(scratch.innerHTML).to.equal('
'); + /** @type {object} */ + let instance; + const props = { text: 'Hello' }; + const initialState = { text: 'World!' }; + + function Foo() { + Component.call(this); + instance = this; + this.state = { text: 'World!' }; + } + Foo.prototype.render = sinon.spy((props, state, context) =>
{props.text + ' ' + state.text}
); + + render(h(Foo, props), scratch); + + expect(scratch.innerHTML).to.equal('
Hello World!
'); + expect(Foo.prototype.render).to.have.been.calledOnceWith(props, initialState, {}); + expect(instance.props).to.deep.equal(props); + expect(instance.state).to.deep.equal(initialState); + expect(instance.context).to.deep.equal({}); + }); }); it('should render string', () => { From de9bc43a0d7bacb12c9f0d45a7facf073f4f3e77 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 13 Oct 2018 12:11:27 -0700 Subject: [PATCH 2/7] Add some more stubs for test cases --- test/browser/components.js | 52 +++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/test/browser/components.js b/test/browser/components.js index 6bd2db8bc6..e4ed263414 100644 --- a/test/browser/components.js +++ b/test/browser/components.js @@ -110,7 +110,6 @@ describe('Components', () => { expect(scratch.innerHTML).to.equal('
'); }); - it('should render Component classes that don\'t pass args into the Component constructor', () => { /** @type {object} */ @@ -132,9 +131,26 @@ describe('Components', () => { expect(instance.context).to.deep.equal({}); }); + it('should render components that don\'t pass args into the Component constructor (unistore pattern)', () => { + + /** @type {object} */ + let instance; + const props = { text: 'Hello' }; + const initialState = { text: 'World!' }; + + // Pattern unistore uses for connect: https://git.io/fxRqu + function Wrapper() { + this.render = sinon.spy(props =>
); + } + (Wrapper.prototype = new Component()).constructor = Wrapper; + + render(, scratch); + }); + it('should render Component classes that don\'t pass args into the Component constructor and initialize state', () => { /** @type {object} */ + // TODO: Share this setup with this describe block let instance; const props = { text: 'Hello' }; const initialState = { text: 'World!' }; @@ -154,6 +170,40 @@ describe('Components', () => { expect(instance.state).to.deep.equal(initialState); expect(instance.context).to.deep.equal({}); }); + + it('should render components that don\'t call Component constructor', () => { + + /** @type {object} */ + let instance; + const props = { text: 'Hello' }; + const initialState = { text: 'World!' }; + + function Foo() {} + Foo.prototype = Object.create(Component); + Foo.prototype.render = sinon.spy(() =>
); + + render(, scratch); + }); + + it('should render components that don\'t inherit from Component', () => { + class Foo { + render() { + return
; + } + } + + render(, scratch); + }) + + it('should render components that don\'t inherit from Component (unistore pattern)', () => { + // Pattern unistore uses for Provider: https://git.io/fxRqR + function Provider(props) { + this.getChildContext = () => ({ store: props.store }); + } + Provider.prototype.render = props => props.children; + + render(, scratch); + }); }); it('should render string', () => { From 13c21877672d8c94bd90c2c17c4cca216e08dd26 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 14 Oct 2018 16:43:30 -0700 Subject: [PATCH 3/7] Fill out test stubs --- test/browser/components.js | 182 ++++++++++++++++--------------------- 1 file changed, 80 insertions(+), 102 deletions(-) diff --git a/test/browser/components.js b/test/browser/components.js index e4ed263414..a6aa8b837c 100644 --- a/test/browser/components.js +++ b/test/browser/components.js @@ -43,6 +43,18 @@ describe('Components', () => { describe('Component construction', () => { + + /** @type {object} */ + let instance; + let PROPS; + let STATE; + + beforeEach(() => { + instance = null; + PROPS = { foo: 'bar', onBaz: () => {} }; + STATE = { text: 'Hello' }; + }); + it('should render components', () => { class C1 extends Component { render() { @@ -62,8 +74,6 @@ describe('Components', () => { it('should render functional components', () => { - const PROPS = { foo: 'bar', onBaz: () => {} }; - const C3 = sinon.spy( props =>
); render(, scratch); @@ -81,7 +91,6 @@ describe('Components', () => { it('should render components with props', () => { - const PROPS = { foo: 'bar', onBaz: () => {} }; let constructorProps; class C2 extends Component { @@ -111,98 +120,112 @@ describe('Components', () => { }); it('should render Component classes that don\'t pass args into the Component constructor', () => { - - /** @type {object} */ - let instance; - const props = { text: 'Hello' }; - function Foo () { Component.call(this); instance = this; + this.state = STATE; } - Foo.prototype.render = sinon.spy((props, state, context) =>
{props.text}
); + Foo.prototype.render = sinon.spy((props, state) =>
{state.text}
); - render(h(Foo, props), scratch); + render(, scratch); - expect(scratch.innerHTML).to.equal('
Hello
'); - expect(Foo.prototype.render).to.have.been.calledOnceWith(props, {}, {}); - expect(instance.props).to.deep.equal(props); - expect(instance.state).to.deep.equal({}); + expect(Foo.prototype.render) + .to.have.been.calledOnce + .and.to.have.been.calledWithMatch(PROPS, STATE, {}) + .and.to.have.returned(sinon.match({ tag: 'div', props: PROPS })); + expect(instance.props).to.deep.equal(PROPS); + expect(instance.state).to.deep.equal(STATE); expect(instance.context).to.deep.equal({}); + + expect(scratch.innerHTML).to.equal('
Hello
'); }); it('should render components that don\'t pass args into the Component constructor (unistore pattern)', () => { - - /** @type {object} */ - let instance; - const props = { text: 'Hello' }; - const initialState = { text: 'World!' }; - // Pattern unistore uses for connect: https://git.io/fxRqu function Wrapper() { - this.render = sinon.spy(props =>
); + instance = this; + this.render = sinon.spy((props, state) =>
{state.text}
); + this.state = STATE; } - (Wrapper.prototype = new Component()).constructor = Wrapper; + (Wrapper.prototype = new Component()).constructor = Wrapper; - render(, scratch); - }); + render(, scratch); - it('should render Component classes that don\'t pass args into the Component constructor and initialize state', () => { + expect(instance.render) + .to.have.been.calledOnce + .and.to.have.been.calledWithMatch(PROPS, STATE, {}) + .and.to.have.returned(sinon.match({ tag: 'div', props: PROPS })); + expect(instance.props).to.deep.equal(PROPS); + expect(instance.state).to.deep.equal({}); + expect(instance.context).to.deep.equal({}); - /** @type {object} */ - // TODO: Share this setup with this describe block - let instance; - const props = { text: 'Hello' }; - const initialState = { text: 'World!' }; + expect(scratch.innerHTML).to.equal('
Hello
'); + }); + it('should render components that don\'t call Component constructor', () => { function Foo() { - Component.call(this); instance = this; - this.state = { text: 'World!' }; + this.state = STATE; } - Foo.prototype.render = sinon.spy((props, state, context) =>
{props.text + ' ' + state.text}
); + Foo.prototype = Object.create(Component); + Foo.prototype.render = sinon.spy((props, state) =>
{state.text}
); - render(h(Foo, props), scratch); + render(, scratch); - expect(scratch.innerHTML).to.equal('
Hello World!
'); - expect(Foo.prototype.render).to.have.been.calledOnceWith(props, initialState, {}); - expect(instance.props).to.deep.equal(props); - expect(instance.state).to.deep.equal(initialState); + expect(Foo.prototype.render) + .to.have.been.calledOnce + .and.to.have.been.calledWithMatch(PROPS, STATE, {}) + .and.to.have.returned(sinon.match({ tag: 'div', props: PROPS })); + expect(instance.props).to.deep.equal(PROPS); + expect(instance.state).to.deep.equal(STATE); expect(instance.context).to.deep.equal({}); - }); - - it('should render components that don\'t call Component constructor', () => { - - /** @type {object} */ - let instance; - const props = { text: 'Hello' }; - const initialState = { text: 'World!' }; - - function Foo() {} - Foo.prototype = Object.create(Component); - Foo.prototype.render = sinon.spy(() =>
); - render(, scratch); + expect(scratch.innerHTML).to.equal('
Hello
'); }); it('should render components that don\'t inherit from Component', () => { class Foo { + constructor() { + instance = this; + this.state = STATE; + } render() { return
; } } - render(, scratch); - }) + render(, scratch); + + expect(Foo.prototype.render) + .to.have.been.calledOnce + .and.to.have.been.calledWithMatch(PROPS, STATE, {}) + .and.to.have.returned(sinon.match({ tag: 'div', props: PROPS })); + expect(instance.props).to.deep.equal(PROPS); + expect(instance.state).to.deep.equal(STATE); + expect(instance.context).to.deep.equal({}); + + expect(scratch.innerHTML).to.equal('
Hello
'); + }); it('should render components that don\'t inherit from Component (unistore pattern)', () => { // Pattern unistore uses for Provider: https://git.io/fxRqR - function Provider(props) { - this.getChildContext = () => ({ store: props.store }); + function Provider() { + instance = this; + this.state = STATE; } - Provider.prototype.render = props => props.children; + Provider.prototype.render = sinon.spy((props, state) =>
{state.text}
); + + render(, scratch); + + expect(Provider.prototype.render) + .to.have.been.calledOnce + .and.to.have.been.calledWithMatch(PROPS, STATE, {}) + .and.to.have.returned(sinon.match({ tag: 'div', props: PROPS })); + expect(instance.props).to.deep.equal(PROPS); + expect(instance.state).to.deep.equal(STATE); + expect(instance.context).to.deep.equal({}); - render(, scratch); + expect(scratch.innerHTML).to.equal('
Hello
'); }); }); @@ -239,50 +262,6 @@ describe('Components', () => { expect(scratch.innerHTML).to.equal(''); }); - it('should render Component classes that don\'t pass props into the Component constructor', () => { - - /** @type {object} */ - let instance; - const props = { text: 'Hello' }; - - function Foo () { - Component.call(this); - instance = this; - } - Foo.prototype.render = sinon.spy((props, state, context) =>
{props.text}
); - - render(h(Foo, props), scratch); - - expect(scratch.innerHTML).to.equal('
Hello
'); - expect(Foo.prototype.render).to.have.been.calledOnceWith(props, {}, {}); - expect(instance.props).to.deep.equal(props); - expect(instance.state).to.deep.equal({}); - expect(instance.context).to.deep.equal({}); - }); - - it('should render Component classes that don\'t the Component constructor but initialize state', () => { - - /** @type {object} */ - let instance; - const props = { text: 'Hello' }; - const initialState = { text: 'World!' }; - - function Foo() { - Component.call(this); - instance = this; - this.state = { text: 'World!' }; - } - Foo.prototype.render = sinon.spy((props, state, context) =>
{props.text + ' ' + state.text}
); - - render(h(Foo, props), scratch); - - expect(scratch.innerHTML).to.equal('
Hello World!
'); - expect(Foo.prototype.render).to.have.been.calledOnceWith(props, initialState, {}); - expect(instance.props).to.deep.equal(props); - expect(instance.state).to.deep.equal(initialState); - expect(instance.context).to.deep.equal({}); - }); - // Test for Issue #73 it('should remove orphaned elements replaced by Components', () => { class Comp extends Component { @@ -303,7 +282,6 @@ describe('Components', () => { expect(scratch.innerHTML).to.equal('span in a component'); }); - // Test for Issue developit/preact#176 it('should remove children when root changes to text node', () => { let comp; From a67437cab9898e5f20c55ea542c8a4dfd1bdf234 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 14 Oct 2018 17:16:16 -0700 Subject: [PATCH 4/7] Move private component field init to diff (+2 B) --- src/component.js | 6 +++--- src/diff/index.js | 2 ++ test/browser/components.js | 9 +++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/component.js b/src/component.js index 42dc75f21a..11c93295dc 100644 --- a/src/component.js +++ b/src/component.js @@ -13,9 +13,9 @@ export function Component(props, context) { this.props = props; this.context = context; // if (this.state==null) this.state = {}; - this.state = {}; - this._dirty = true; - this._renderCallbacks = []; // Only class components + this.state = {}; // TODO: Consider removing + // this._dirty = true; + // this._renderCallbacks = []; // Only class components // Other properties that Component will have set later, // shown here as commented out for quick reference diff --git a/src/diff/index.js b/src/diff/index.js index 21b893e877..ff3f8b1520 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -224,6 +224,8 @@ export function diff(dom, parent, newTree, oldTree, context, isSvg, append, exce c.props = newTree.props; if (!c.state) c.state = {}; c.context = context; + c._dirty = true; + c._renderCallbacks = []; } c._vnode = newTree; diff --git a/test/browser/components.js b/test/browser/components.js index a6aa8b837c..ce0e38320e 100644 --- a/test/browser/components.js +++ b/test/browser/components.js @@ -144,8 +144,8 @@ describe('Components', () => { // Pattern unistore uses for connect: https://git.io/fxRqu function Wrapper() { instance = this; - this.render = sinon.spy((props, state) =>
{state.text}
); this.state = STATE; + this.render = sinon.spy((props, state) =>
{state.text}
); } (Wrapper.prototype = new Component()).constructor = Wrapper; @@ -156,7 +156,7 @@ describe('Components', () => { .and.to.have.been.calledWithMatch(PROPS, STATE, {}) .and.to.have.returned(sinon.match({ tag: 'div', props: PROPS })); expect(instance.props).to.deep.equal(PROPS); - expect(instance.state).to.deep.equal({}); + expect(instance.state).to.deep.equal(STATE); expect(instance.context).to.deep.equal({}); expect(scratch.innerHTML).to.equal('
Hello
'); @@ -189,10 +189,11 @@ describe('Components', () => { instance = this; this.state = STATE; } - render() { - return
; + render(props, state) { + return
{state.text}
; } } + sinon.spy(Foo.prototype, 'render'); render(, scratch); From e87dba52c5cd4bb1a110633149085b713806e502 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 14 Oct 2018 17:27:19 -0700 Subject: [PATCH 5/7] Add tests for component constructor side-effects and for components that don't initialize state --- test/browser/components.js | 60 +++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/test/browser/components.js b/test/browser/components.js index ce0e38320e..f595b8ac84 100644 --- a/test/browser/components.js +++ b/test/browser/components.js @@ -89,7 +89,6 @@ describe('Components', () => { expect(scratch.innerHTML).to.equal('
'); }); - it('should render components with props', () => { let constructorProps; @@ -119,6 +118,22 @@ describe('Components', () => { expect(scratch.innerHTML).to.equal('
'); }); + it('should initialize props, context, and state in Component constructor', () => { + class Foo extends Component { + constructor(props, context) { + super(props, context); + expect(this.props).to.equal(props); + expect(this.state).to.deep.equal({}); + expect(this.context).to.equal(context); + } + render() { + return
; + } + } + + render(, scratch); + }); + it('should render Component classes that don\'t pass args into the Component constructor', () => { function Foo () { Component.call(this); @@ -183,6 +198,25 @@ describe('Components', () => { expect(scratch.innerHTML).to.equal('
Hello
'); }); + it('should render components that don\'t call Component constructor and don\'t initialize state', () => { + function Foo () { + instance = this; + } + Foo.prototype.render = sinon.spy((props) =>
Hello
); + + render(, scratch); + + expect(Foo.prototype.render) + .to.have.been.calledOnce + .and.to.have.been.calledWithMatch(PROPS, {}, {}) + .and.to.have.returned(sinon.match({ tag: 'div', props: PROPS })); + expect(instance.props).to.deep.equal(PROPS); + expect(instance.state).to.deep.equal({}); + expect(instance.context).to.deep.equal({}); + + expect(scratch.innerHTML).to.equal('
Hello
'); + }); + it('should render components that don\'t inherit from Component', () => { class Foo { constructor() { @@ -228,6 +262,30 @@ describe('Components', () => { expect(scratch.innerHTML).to.equal('
Hello
'); }); + + it('should render components that don\'t inherit from Component and don\'t initialize state', () => { + class Foo { + constructor() { + instance = this; + } + render(props, state) { + return
Hello
; + } + } + sinon.spy(Foo.prototype, 'render'); + + render(, scratch); + + expect(Foo.prototype.render) + .to.have.been.calledOnce + .and.to.have.been.calledWithMatch(PROPS, {}, {}) + .and.to.have.returned(sinon.match({ tag: 'div', props: PROPS })); + expect(instance.props).to.deep.equal(PROPS); + expect(instance.state).to.deep.equal({}); + expect(instance.context).to.deep.equal({}); + + expect(scratch.innerHTML).to.equal('
Hello
'); + }); }); it('should render string', () => { From 3aa0ed18b50bdac5450dec701263dd0054c4b9fd Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 14 Oct 2018 19:47:18 -0700 Subject: [PATCH 6/7] Don't initialize state to an empty object in the base component constructor (-4 B) --- CHANGELOG.md | 3 +++ src/component.js | 2 +- test/browser/components.js | 29 ++++++++++++++++++++++------- test/browser/render.js | 2 +- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9114122f73..1792f456ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ 6. `setState` no longer modifies `this.state` synchronously 7. Falsy attributes values are no longer removed from the DOM. For some attributes (e.g. `spellcheck`) the values `false` and `''` have different meaning so being able to render `false` is important +8. The Component constructor no longer initializes state to an empty object. If state has not been + previously set, it will be set to an empty object the first time the component is rendered, after + the constructor has been called ### Minor changes diff --git a/src/component.js b/src/component.js index 11c93295dc..9ceb99d34a 100644 --- a/src/component.js +++ b/src/component.js @@ -13,7 +13,7 @@ export function Component(props, context) { this.props = props; this.context = context; // if (this.state==null) this.state = {}; - this.state = {}; // TODO: Consider removing + // this.state = {}; // this._dirty = true; // this._renderCallbacks = []; // Only class components diff --git a/test/browser/components.js b/test/browser/components.js index f595b8ac84..fcab380544 100644 --- a/test/browser/components.js +++ b/test/browser/components.js @@ -118,20 +118,35 @@ describe('Components', () => { expect(scratch.innerHTML).to.equal('
'); }); - it('should initialize props, context, and state in Component constructor', () => { + it('should initialize props & context but not state in Component constructor', () => { + // Not initializing state matches React behavior: https://codesandbox.io/s/rml19v8o2q class Foo extends Component { constructor(props, context) { super(props, context); expect(this.props).to.equal(props); - expect(this.state).to.deep.equal({}); + expect(this.state).to.deep.equal(undefined); expect(this.context).to.equal(context); + + instance = this; } - render() { - return
; + render(props) { + return
Hello
; } } - render(, scratch); + sinon.spy(Foo.prototype, 'render'); + + render(, scratch); + + expect(Foo.prototype.render) + .to.have.been.calledOnce + .and.to.have.been.calledWithMatch(PROPS, {}, {}) + .and.to.have.returned(sinon.match({ tag: 'div', props: PROPS })); + expect(instance.props).to.deep.equal(PROPS); + expect(instance.state).to.deep.equal({}); + expect(instance.context).to.deep.equal({}); + + expect(scratch.innerHTML).to.equal('
Hello
'); }); it('should render Component classes that don\'t pass args into the Component constructor', () => { @@ -391,7 +406,7 @@ describe('Components', () => { class GoodContainer extends Component { constructor(props) { super(props); - this.state.alt = false; + this.state = { alt: false }; good = this; } @@ -409,7 +424,7 @@ describe('Components', () => { class BadContainer extends Component { constructor(props) { super(props); - this.state.alt = false; + this.state = { alt: false }; bad = this; } diff --git a/test/browser/render.js b/test/browser/render.js index 6041e6c2f0..0f2e968c32 100644 --- a/test/browser/render.js +++ b/test/browser/render.js @@ -548,7 +548,7 @@ describe('render()', () => { class Thing extends Component { constructor(props, context) { super(props, context); - this.state.html = this.props.html; + this.state = { html: this.props.html }; thing = this; } render(props, { html }) { From 618984312bd3f2ded04d754e176157fa3fe4c06b Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 14 Oct 2018 22:14:23 -0700 Subject: [PATCH 7/7] Define a default implementation of render to enable unistore component pattern (+7 B) --- src/component.js | 11 +++++++++++ test/browser/components.js | 24 +++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/component.js b/src/component.js index 9ceb99d34a..c91e155690 100644 --- a/src/component.js +++ b/src/component.js @@ -87,6 +87,17 @@ Component.prototype.forceUpdate = function(callback) { if (callback!=null) callback(); }; +/** + * Accepts `props` and `state`, and returns a new Virtual DOM tree to build. + * Virtual DOM is generally constructed via [JSX](http://jasonformat.com/wtf-is-jsx). + * @param {object} props Props (eg: JSX attributes) received from parent + * element/component + * @param {object} state The component's current state + * @param {object} context Context object, as returned by the nearest + * ancestor's `getChildContext()` + * @returns {import('./index').ComponentChildren | void} + */ +Component.prototype.render = function () {}; /** * The render queue diff --git a/test/browser/components.js b/test/browser/components.js index fcab380544..993bd87fcd 100644 --- a/test/browser/components.js +++ b/test/browser/components.js @@ -43,7 +43,6 @@ describe('Components', () => { describe('Component construction', () => { - /** @type {object} */ let instance; let PROPS; @@ -301,6 +300,29 @@ describe('Components', () => { expect(scratch.innerHTML).to.equal('
Hello
'); }); + + it('should render class components that inherit from Component without a render method', () => { + class Foo extends Component { + constructor(props, context) { + super(props, context); + instance = this; + } + } + + sinon.spy(Foo.prototype, 'render'); + + render(, scratch); + + expect(Foo.prototype.render) + .to.have.been.calledOnce + .and.to.have.been.calledWithMatch(PROPS, {}, {}) + .and.to.have.returned(undefined); + expect(instance.props).to.deep.equal(PROPS); + expect(instance.state).to.deep.equal({}); + expect(instance.context).to.deep.equal({}); + + expect(scratch.innerHTML).to.equal(''); + }); }); it('should render string', () => {