Skip to content

Commit

Permalink
Allow top-level re-rendering of components
Browse files Browse the repository at this point in the history
This enables the persisting of component state between renders.
  • Loading branch information
tbranyen committed Oct 30, 2022
1 parent db5a310 commit 8eb019d
Show file tree
Hide file tree
Showing 10 changed files with 63 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { $$hooks } from '../util/symbols';
const { release, Internals } = diff;

/**
* Called whenever a component is being removed.
* This is called whenever a component is removed or in the case of a function
* component is called whenever a component is updated.
*
* @param {VTree} vTree - The respecting tree pointing to the component
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/diffhtml-components/lib/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ function render(oldTree, newTree, transaction) {

// When there is an oldTree and it has childNodes, attempt to look up first
// by the top-level element, or by the first element.
if (oldTree && oldTree.childNodes) {
if (oldTree) {
// First try and lookup the old tree as a component.
oldComponentTree = ComponentTreeCache.get(oldTree);

// If that fails, try looking up its first child.
if (!oldComponentTree) {
if (!oldComponentTree && oldTree.childNodes) {
oldComponentTree = ComponentTreeCache.get(oldTree.childNodes[0]);
}
}
Expand Down
5 changes: 3 additions & 2 deletions packages/diffhtml-components/lib/render-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ export default function renderComponent(vTree, transaction) {
const props = vTree.attributes;
const RawComponent = vTree.rawNodeName;
const isNewable = RawComponent.prototype && RawComponent.prototype.render;
const isInstance = InstanceCache.has(vTree);

/** @type {VTree|null} */
let renderedTree = (null);

// Existing class component rerender.
if (InstanceCache.has(vTree)) {
if (isInstance) {
const instance = InstanceCache.get(vTree);

if (instance.componentWillReceiveProps) {
Expand Down Expand Up @@ -113,7 +114,7 @@ export default function renderComponent(vTree, transaction) {

/** @type {VTree | null} */
[$$vTree] = null;
}
}

const instance = new FunctionComponent(props)

Expand Down
30 changes: 30 additions & 0 deletions packages/diffhtml-components/test/integration/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -415,5 +415,35 @@ describe('Hooks', function() {

strictEqual(this.fixture.outerHTML, `<div>true</div>`);
});

it('will support nested setState with top-level re-rendering', async () => {
let setComponentValue = null;
let setNestedValue = null;

function Nested() {
const [ value, _setValue ] = createState(false);

setNestedValue = _setValue;

return html`${String(value)}`;
}

function Component() {
const [ value, _setValue ] = createState(false);

setComponentValue = _setValue;

return html`<${Nested} />`;
}

this.fixture = document.createElement('div');

await innerHTML(this.fixture, html`<${Component} />`);
await setNestedValue(123);
strictEqual(this.fixture.outerHTML, `<div>123</div>`);

await innerHTML(this.fixture, html`<${Component} />`);
strictEqual(this.fixture.outerHTML, `<div>123</div>`);
});
});
});
16 changes: 9 additions & 7 deletions packages/diffhtml/lib/tasks/reconcile-trees.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ export default function reconcileTrees(transaction) {
// We rebuild the tree whenever the DOM Node changes, including the first
// time we patch a DOM Node. This is currently problematic when someone
// passes a DOM Node as an interpolated value we empty the VTree of its
// contents, but the newTree still has a reference and is expecting
// populated values.
// contents, but the newTree still has a reference and is expecting populated
// values.
if (state.isDirty || !state.oldTree) {
// Always release when the state is dirty. This ensures a completely clean
// slate.
release(mountAsHTMLEl);

// Ensure the mutation observer is reconnected.
Expand Down Expand Up @@ -69,11 +71,11 @@ export default function reconcileTrees(transaction) {
// match the browser behavior here, it will be significantly easier to
// convince of it's validity and to document.
//
// To mimic browser behavior, we loop the input and take any tree that matches
// the root element and unwrap into the root element. We take the attributes
// from that element and apply to the root element. This ultimately renders a
// flat tree and allows for whitespace to be provided in the `html` function
// without needing to trim.
// To mimic browser behavior, we loop the input and take any tree that
// matches the root element and unwrap into the root element. We take the
// attributes from that element and apply to the root element. This
// ultimately renders a flat tree and allows for whitespace to be provided in
// the `html` function without needing to trim.
if (
!inner &&
inputAsVTree.nodeType === NODE_TYPE.FRAGMENT &&
Expand Down
8 changes: 6 additions & 2 deletions packages/diffhtml/lib/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const tasks = {

export default class Transaction {
/**
* This abstracts away the concept of `new` in case in the future we want
* to migrate a Transaction off of a class.
*
* @param {Mount} mount
* @param {ValidInput} input
Expand Down Expand Up @@ -132,15 +134,14 @@ export default class Transaction {
this.input = input;
this.config = config;

const isDirtyCheck = () => this.state.isDirty = true;
const useObserver = !config.disableMutationObserver && 'MutationObserver' in (globalThis.window || EMPTY.OBJ);

this.state = StateCache.get(mount) || /** @type {TransactionState} */ ({
measure: makeMeasure(this),
svgElements: new Set(),
scriptsToExecute: new Map(),
activeTransaction: this,
mutationObserver: useObserver && new globalThis.window.MutationObserver(isDirtyCheck),
mutationObserver: useObserver && new globalThis.window.MutationObserver(EMPTY.FUN),
});

this.tasks = /** @type {Function[]} */ (
Expand Down Expand Up @@ -228,6 +229,9 @@ export default class Transaction {
}
// If there is no MutationObserver, then the DOM is dirty by default and
// rescanned every time.
//
// FIXME This should be smarter for NodeJS, otherwise this is dirty every
// re-render.
else {
state.isDirty = true;
}
Expand Down
7 changes: 3 additions & 4 deletions packages/diffhtml/lib/util/make-measure.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import getConfig from "./config";
import { VTree } from "./types";
import { EMPTY, VTree } from "./types";

const prefix = 'diffHTML';
const marks = new Map();
const nop = () => {};
let count = 0;

/**
Expand All @@ -20,7 +19,7 @@ export default function makeMeasure(transaction) {

// Marks will only be available if the user has requested they want to collect
// metrics.
if (!getConfig('collectMetrics', false)) { return nop; }
if (!getConfig('collectMetrics', false)) { return EMPTY.FUN; }

return name => {
name = `[${id}] ${name}`;
Expand Down Expand Up @@ -52,4 +51,4 @@ export default function makeMeasure(transaction) {
performance.mark(name);
}
};
}
}
9 changes: 5 additions & 4 deletions packages/diffhtml/lib/util/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { NodeCache, VTree } from './types';
const { protect, unprotect, memory } = Pool;

/**
* Ensures that vTree is not recycled during a render cycle.
* Ensures that vTree is not recycled during a render cycle. This effectively
* allocates a VTree to a DOM node representation for as long as it's needed.
*
* @param {VTree} vTree
* @return {void}
Expand All @@ -20,9 +21,9 @@ export function protectVTree(vTree) {
}

/**
* Recycles a VTree by unprotecting itself, removing its DOM Node reference, and
* recursively unprotecting all nested children. Resets the VTree's attributes
* and childNode properties afterwards, as these can contribute to unwanted
* Recycles a VTree by unprotecting itself, removing its DOM Node reference,
* and recursively unprotecting all nested children. Resets the VTree
* attributes and childNode properties as these can contribute to unwanted
* increases in the heap.
*
* @param {VTree} vTree
Expand Down
1 change: 1 addition & 0 deletions packages/diffhtml/lib/util/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const EMPTY = {
MAP: new Map(),
SET: new Set(),
DOM: /** @type {HTMLElement} */ ({}),
FUN: () => {},
};

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/diffhtml/test/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -1457,7 +1457,7 @@ describe('Util', function() {
const input = createTree(mount);
const measure = makeMeasure({ mount, input });

strictEqual(measure.name, 'nop');
strictEqual(measure.name, 'FUN');
});

it(`will return a NOP when the user has search, but no diff`, () => {
Expand All @@ -1467,7 +1467,7 @@ describe('Util', function() {
location.href = 'about:blank?';

const measure = makeMeasure({ mount, input });
strictEqual(measure.name, 'nop');
strictEqual(measure.name, 'FUN');
});

it('will return a real measure function if requested', () => {
Expand Down

0 comments on commit 8eb019d

Please sign in to comment.