Skip to content

Commit

Permalink
Merge pull request #5 from canjs/use-use-observer
Browse files Browse the repository at this point in the history
Use ylem's useObserver() to solve issues with observations and ReactDom render timing [redux]
  • Loading branch information
bmomberger-bitovi authored Jan 24, 2020
2 parents 62d5d6d + 5974344 commit 2c1d7c3
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 18 deletions.
94 changes: 94 additions & 0 deletions lib/can-observer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import canReflect from 'can-reflect';
import ObservationRecorder from 'can-observation-recorder';
import recorderHelpers from 'can-observation/recorder-dependency-helpers';
import queues from 'can-queues';

ObservationRecorder.resume = function resume(deps) {
ObservationRecorder.stack.push(deps);
};

let ORDER;
let weLeftSomethingOnTheStack = false;

export default class Observer {
constructor(onUpdate) {
this.newDependencies = ObservationRecorder.makeDependenciesRecorder();
this.oldDependencies = null;
this.onUpdate = onUpdate;

this.onDependencyChange = (newVal, oldVal) => {
this.dependencyChange(newVal, oldVal);
};
}

startRecording() {
if (weLeftSomethingOnTheStack) {
const deps = ObservationRecorder.stop();
weLeftSomethingOnTheStack = false;

if (!deps.ylem) {
throw new Error(
'If you see this error with another error, clearing that should solve this. If you see ' +
'this error alone, please open an issue at https://github.com/canjs/react-to-can-webcomponent/issues'
);
}
}

this.oldDependencies = this.newDependencies;
this.nextDependencies = ObservationRecorder.start();
this.nextDependencies.ylem = true;
weLeftSomethingOnTheStack = true;

if (this.order !== undefined) {
ORDER = this.order;
} else if (ORDER !== undefined) {
this.order = ORDER;
ORDER += 1;
} else {
// the root component
ORDER = 0;
this.order = ORDER;
}
}

stopRecording() {
if (weLeftSomethingOnTheStack) {
const deps = ObservationRecorder.stop();
weLeftSomethingOnTheStack = false;

if (!deps.ylem) {
throw new Error(
'If you see this error with another error, clearing that should solve this. If you see ' +
'this error alone, please open an issue at https://github.com/canjs/react-to-can-webcomponent/issues'
);
}
}

this.newDependencies = this.nextDependencies;
recorderHelpers.updateObservations(this);
}

dependencyChange() {
queues.deriveQueue.enqueue(this.onUpdate, this, [], {
priority: this.order,
});
}

teardown() {
recorderHelpers.stopObserving(this.newDependencies, this.onDependencyChange);
queues.deriveQueue.dequeue(this.onUpdate);
}

// eslint-disable-next-line class-methods-use-this
ignore(fn) {
return ObservationRecorder.ignore(fn)();
}
}

if (process.env.NODE_ENV !== 'production') {
canReflect.assignSymbols(Observer.prototype, {
'can.getName': function getName() {
return `${canReflect.getName(this.constructor)}<${canReflect.getName(this.onUpdate)}>`;
},
});
}
25 changes: 25 additions & 0 deletions lib/use-observer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Observer from './can-observer';

// Unlike the ylem version, this hook is passed a
// React instance so it can be used seamlessly
// by Preact et al.
export default function useObserver(React) {
const { useEffect, useLayoutEffect, useState, useRef } = React;

const [, update] = useState();

const observer = useRef(new Observer(() => update({})));

observer.current.startRecording();
useLayoutEffect(() => {
observer.current.stopRecording();
});

// eslint-disable-next-line arrow-body-style
useEffect(() => {
return () => {
observer.current.teardown();
observer.current = null;
};
}, []);
}
14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@
"description": "Convert react components to native Web Components that work with CanJS.",
"main": "react-to-can-webcomponent",
"dependencies": {
"can-observation": "^4.2.0"
"can-observation": "^4.2.0",
"can-observation-recorder": "^1.3.1",
"can-queues": "^1.3.1",
"can-reflect": "^1.18.0"
},
"devDependencies": {
"@webcomponents/custom-elements": "^1.2.4",
"can-observable-array": "^1.0.6",
"can-observable-object": "^1.0.1",
"can-stache": "^5.1.1",
"can-stache-bindings": "^5.0.4",
"preact": "^8.5.3",
"preact-compat": "^3.19.0",
"preact": "^10.2.1",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-dom": "^16.12.0",
Expand All @@ -31,6 +33,12 @@
"release:major": "npm version major && npm publish",
"test": "testee --browsers firefox test.html"
},
"steal": {
"map": {
"preact/compat": "preact/compat/dist/compat",
"preact/hooks": "preact/hooks/dist/hooks"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/bitovi/react-to-can-webcomponent.git"
Expand Down
114 changes: 109 additions & 5 deletions react-to-can-webcomponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@ import QUnit from "steal-qunit";
import React from 'react';
import ReactDOM from "react-dom";
import PropTypes from 'prop-types';
import PreactCompat from "preact-compat";
import PreactCompat from "preact/compat";
import stache from "can-stache";
import stacheBindings from "can-stache-bindings";
import ObservableObject from "can-observable-object";
import ObservableArray from "can-observable-array";


import reactToWebComponent from "./react-to-can-webcomponent";
stache.addBindings(stacheBindings);

import reactToWebComponent from "./react-to-can-webcomponent";

QUnit.module("react-to-can-webcomponent");

Expand Down Expand Up @@ -163,7 +162,6 @@ QUnit.test("works with nested properties of observable objects and arrays", func
}

class MyWelcome extends reactToWebComponent(Welcome, React, ReactDOM) {}

customElements.define("nested-props-welcome", MyWelcome);

var fixture = document.getElementById("qunit-fixture");
Expand Down Expand Up @@ -196,7 +194,6 @@ QUnit.test("works with nested properties of observable objects and arrays", func

assert.equal(myWelcome.childNodes[0].innerHTML, "Hello, Ramiya Meyer", "can update object properties");
assert.equal(myWelcome.childNodes[1].innerHTML, "I see you like basketball and school", "can update array elements");

});

QUnit.test("subproperties update with can-stache and can-stache-bindings", function(assert){
Expand Down Expand Up @@ -234,3 +231,110 @@ QUnit.test("subproperties update with can-stache and can-stache-bindings", funct
person.name = "Cherif";
assert.equal(myWelcome.childNodes[0].innerHTML, "Hello, Cherif", "can update");
});

QUnit.test("sibling subcomponents only update for their own changes", function(assert){
class Welcome extends React.Component {
render() {
return <h1>Hello, {
this.props.name.first
}</h1>;
}
}
class Farewell extends React.Component {
render() {
return <h1>Goodbye, Mr. {
this.props.name.last
}</h1>;
}
}

class HelloGoodbye extends React.Component {
render() {
return <section>
<Welcome name={this.props.name} />
<Farewell name={this.props.name} />
</section>
}
}

class MyHelloGoodbye extends reactToWebComponent(HelloGoodbye, React, ReactDOM) {}
customElements.define("can-hello-goodbye", MyHelloGoodbye);

var myHelloGoodbye = new MyHelloGoodbye();
myHelloGoodbye.name = new ObservableObject({
first: "Justin",
last: "Meyer",
});

var fixture = document.getElementById("qunit-fixture");
fixture.appendChild(myHelloGoodbye);

assert.equal(myHelloGoodbye.nodeName, "CAN-HELLO-GOODBYE", "able to read nodeName");
assert.equal(myHelloGoodbye.childNodes.length, 1, "able to render something");

assert.equal(myHelloGoodbye.firstElementChild.firstElementChild.innerHTML, "Hello, Justin", "can update");
assert.equal(myHelloGoodbye.firstElementChild.lastElementChild.innerHTML, "Goodbye, Mr. Meyer", "can update");

myHelloGoodbye.childNodes[0].firstElementChild.innerHTML = "Hello, Brad";
myHelloGoodbye.name.last = "Momberger";

assert.equal(myHelloGoodbye.firstElementChild.firstElementChild.innerHTML, "Hello, Brad", "doesn't rerender for no reason");
assert.equal(myHelloGoodbye.firstElementChild.lastElementChild.innerHTML, "Goodbye, Mr. Momberger", "rerenders on change");
});

QUnit.test("sibling wrapped components only update with their own changes", function(assert){
class Welcome extends React.Component {
render() {
return <h1>Hello, {
this.props.name.first
}</h1>;
}
}
Welcome.propTypes = {
name: PropTypes.object
};
class Farewell extends React.Component {
render() {
return <h1>Goodbye, Mr. {
this.props.name.last
}</h1>;
}
}
Farewell.propTypes = {
name: PropTypes.object
};

class MyWelcome extends reactToWebComponent(Welcome, React, ReactDOM) {}
customElements.define("can-welcome-iii", MyWelcome);
class MyFarewell extends reactToWebComponent(Farewell, React, ReactDOM) {}
customElements.define("can-farewell", MyFarewell);

var tmpl = stache(`
<can-welcome-iii name:from="this" />
<can-farewell name:from="this" />
`)
var vm = new ObservableObject({
first: "Justin",
last: "Meyer",
});
var frag = tmpl(vm);

var fixture = document.getElementById("qunit-fixture");
fixture.appendChild(frag);
var myWelcome = fixture.firstElementChild;
var myFarewell = fixture.lastElementChild;

assert.equal(myWelcome.nodeName, "CAN-WELCOME-III", "able to read nodeName");
assert.equal(myWelcome.childNodes.length, 1, "able to render something");
assert.equal(myFarewell.nodeName, "CAN-FAREWELL", "able to read nodeName");
assert.equal(myFarewell.childNodes.length, 1, "able to render something");

assert.equal(myWelcome.firstElementChild.innerHTML, "Hello, Justin", "can update");
assert.equal(myFarewell.firstElementChild.innerHTML, "Goodbye, Mr. Meyer", "can update");

myWelcome.firstElementChild.innerHTML = "Hello, Brad";
vm.last = "Momberger";

assert.equal(myWelcome.firstElementChild.innerHTML, "Hello, Brad", "doesn't rerender for no reason");
assert.equal(myFarewell.firstElementChild.innerHTML, "Goodbye, Mr. Momberger", "rerenders on change");
});
22 changes: 12 additions & 10 deletions react-to-can-webcomponent.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Observation from "can-observation";
// TODO if ylem hooks branch is ever released, import use-observer from ylem instead.
import useObserver from "./lib/use-observer";
var reactComponentSymbol = Symbol.for("r2wc.reactComponent");
var renderSymbol = Symbol.for("r2wc.reactRender");
var shouldRenderSymbol = Symbol.for("r2wc.shouldRender");
var observationSymbol = Symbol.for("r2wc.observation");

var define = {
// Creates a getter/setter that re-renders everytime a property is set.
Expand All @@ -22,6 +22,7 @@ var define = {
}
}


export default function(ReactComponent, React, ReactDOM) {
var renderAddedProperties = {isConnected: "isConnected" in HTMLElement.prototype};
var rendering = false;
Expand Down Expand Up @@ -74,15 +75,10 @@ export default function(ReactComponent, React, ReactDOM) {
// Once connected, it will keep updating the innerHTML.
// We could add a render method to allow this as well.
this[shouldRenderSymbol] = true;
// Also catch any sub-properties of observables which
// are read while rendering the React component.
this[observationSymbol] = this[observationSymbol] || new Observation(() => {
this[renderSymbol]();
});
this[observationSymbol].on();
this[renderSymbol]();
};
targetPrototype.disconnectedCallback = function() {
this[observationSymbol].off();
this[shouldRenderSymbol] = false;
};

targetPrototype[renderSymbol] = function() {
Expand All @@ -94,7 +90,13 @@ export default function(ReactComponent, React, ReactDOM) {
}
}, this);
rendering = true;
this[reactComponentSymbol] = ReactDOM.render(React.createElement(ReactComponent, data), this);
this[reactComponentSymbol] = ReactDOM.render(
React.createElement(props => {
useObserver(React);
return React.createElement(ReactComponent, props);
}, data),
this
);
rendering = false;
}
};
Expand Down

0 comments on commit 2c1d7c3

Please sign in to comment.