Skip to content

Commit

Permalink
Fix hooks (#260)
Browse files Browse the repository at this point in the history
* Fix hooks by tracking position

This is still a naive implementation and will be improved to become more
robust in the future. For now this should allow seamless integration of
multiple hooks together.

* Change signature of createSideEffect

* Update docs
  • Loading branch information
tbranyen committed Mar 31, 2022
1 parent 327d044 commit 0b01f42
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 30 deletions.
15 changes: 12 additions & 3 deletions docs/components.html
Original file line number Diff line number Diff line change
Expand Up @@ -464,19 +464,28 @@ <h3 id="examples"><a href="#create-state-examples"><u>Examples</u></a></h3>
<h2 id="createsideeffect"><a href="#create-side-effect">createSideEffect</a></h2>
<p>The function <code>createSideEffect</code> is used to schedule some work after a component
has mounted, unmounted, or updated. This works similar to the <code>useEffect</code> hook
found in React.</p>
found in React. There are some differences though. With React, a useEffect hook
is triggered on both mount and update with the same function. The unmount logic
is also triggered before every update.</p>
<p>With <code>createSideEffect</code> you will pass one or two functions which represent
mount and unmount respectively. Only one is required. They map directly to
<code>componentDidMount</code> and <code>componentWillUnmount</code>. If you wish to hook into
<code>componentDidUpdate</code>, simply return a new function from the <code>componentDidMount</code>
handler.</p>
<p><a name="create-side-effect-examples"></a></p>
<h3 id="examples-1"><a href="#create-side-effect-examples"><u>Examples</u></a></h3>
<pre><code class="language-javascript">import { innerHTML, html } from &#39;diffhtml&#39;;
import { createSideEffect } from &#39;diffhtml-components&#39;;

function Example() {
createSideEffect(() =&gt; {
console.log(&#39;Component has mounted or updated&#39;);
console.log(&#39;Component has mounted&#39;);

return () =&gt; {
console.log(&#39;Component has unmounted&#39;);
console.log(&#39;Component has updated&#39;);
};
}, () =&gt; {
console.log(&#39;Component has unmounted&#39;);
});

return html`
Expand Down
13 changes: 11 additions & 2 deletions packages/diffhtml-components/lib/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,8 @@ export default class Component {
/** @type {VTree | null} */
[$$vTree] = null;

/** @type {Function[]} */
[$$hooks] = [];
/** @type {{ fns: Function[], i: number }} */
[$$hooks] = { fns: [], i: 0 };

/**
* Stateful render. Used when a component changes and needs to re-render
Expand All @@ -249,6 +249,10 @@ export default class Component {

ActiveRenderState.push(this);

if ($$hooks in this) {
this[$$hooks].i = 0;
}

/** @type {Promise<Transaction>} */
const promise = /** @type {any} */ (innerHTML(
/** @type {any} */ (this).shadowRoot,
Expand Down Expand Up @@ -280,6 +284,11 @@ export default class Component {

// Render directly from the Component.
ActiveRenderState.push(this);

if ($$hooks in this) {
this[$$hooks].i = 0;
}

let renderTree = this.render(this.props, this.state);
ActiveRenderState.length = 0;

Expand Down
45 changes: 28 additions & 17 deletions packages/diffhtml-components/lib/create-side-effect.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,51 @@
import { $$hooks } from './util/symbols';
import { EMPTY, ActiveRenderState } from './util/types';
import { ActiveRenderState } from './util/types';

/**
* Allow a function component to hook into lifecycle methods in a manner
* consistent with class components.
* consistent with class components. Meaning you can leverage existing lifecycle
* events in a function component.
*
* @param {Function} sideEffectFn - A function that is called whenever the
* component is mounted or updated. To invoke cleanup pass a second function
* which will run whenever the component is removed.
* @param {Function=} didMountOrUpdate - A function that is called whenever the
* component is mounted. To hook into component updates, return a function. This
* returned function will be called whenever the component updates.
*
* @param {Function=} unMount - A function that is called whenever a component
* is unmounted.
*
* @returns {void}
*/
export function createSideEffect(sideEffectFn) {
export function createSideEffect(didMountOrUpdate, unMount) {
if (ActiveRenderState.length === 0) {
throw new Error('Cannot create side effect unless in render function');
}

if (typeof sideEffectFn !== 'function') {
if (typeof didMountOrUpdate !== 'function' && typeof unMount !== 'function') {
throw new Error('Missing function for side effect');
}

const [ activeComponent ] = ActiveRenderState;
const activeHook = activeComponent[$$hooks].shift();

// Only do this the first time.
if (!activeHook) {
// First schedule a componentDidMount
activeComponent.componentDidMount = activeComponent.componentDidUpdate = () => {
const unMount = sideEffectFn() || EMPTY.FUN;
// Schedule a componentDidMount if a function was provided
if (typeof didMountOrUpdate === 'function') {
activeComponent.componentDidMount = () => {
const didUpdate = didMountOrUpdate();

if (typeof unMount === 'function') {
activeComponent.componentWillUnmount = () => unMount();
// Then if the user specifies a return function, use that as didUpdate
if (typeof didUpdate === 'function') {
activeComponent.componentDidUpdate = () => {
didUpdate();
};
}

};
}

// Return currentValue and setState.
activeComponent[$$hooks].push(sideEffectFn);
// Schedule a componentWillUnmount if a function is provided
if (typeof unMount === 'function') {
activeComponent.componentWillUnmount = () => unMount();
}

// Increment the hooks count.
activeComponent[$$hooks].i += 1;
}
9 changes: 7 additions & 2 deletions packages/diffhtml-components/lib/create-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export function createState(defaultValue = {}) {
}

const [ activeComponent ] = ActiveRenderState;
const activeHook = activeComponent[$$hooks].shift();
const hooks = activeComponent[$$hooks];
const activeHook = hooks.fns[hooks.i];
const currentValue = activeHook ? activeHook[0] : defaultValue;
const retVal = activeHook || [currentValue];

Expand All @@ -37,6 +38,10 @@ export function createState(defaultValue = {}) {
}

// Return currentValue and setState.
activeComponent[$$hooks].push(retVal);
hooks.fns[hooks.i] = retVal;

// Increment the hooks count.
hooks.i += 1;

return retVal;
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ export default function componentWillUnmount(vTree) {
// Empty out all hooks for gc. If using a stateless class or function, they
// may not have this value set.
if (instance[$$hooks]) {
instance[$$hooks].length = 0;
instance[$$hooks].fns.length = 0;
instance[$$hooks].i = 0;
}

ComponentTreeCache.delete(vTree);
Expand Down
4 changes: 3 additions & 1 deletion packages/diffhtml-components/lib/render-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
InstanceCache,
VTree,
} from './util/types';
import { $$vTree } from './util/symbols';
import { $$hooks, $$vTree } from './util/symbols';
import diff from './util/binding';
import Component from './component';

Expand Down Expand Up @@ -47,6 +47,8 @@ export default function renderComponent(vTree) {
});

ActiveRenderState.push(instance);
// Reset the hooks iterator.
instance[$$hooks].i = 0;
renderedTree = createTree(instance.render(props, instance.state));
ActiveRenderState.length = 0;

Expand Down
38 changes: 37 additions & 1 deletion packages/diffhtml-components/test/integration/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ describe('Hooks', function() {
function Component() {
createSideEffect(() => {
firedOnUpdate++;
return () => firedOnUpdate++;
});

return html`<div></div>`;
Expand All @@ -120,6 +121,7 @@ describe('Hooks', function() {
function Component() {
createSideEffect(() => {
firedOnUpdate++;
return () => firedOnUpdate++;
});

return html`<div></div>`;
Expand All @@ -139,7 +141,7 @@ describe('Hooks', function() {
let firedOnUnmount = 0;

function Component() {
createSideEffect(() => () => {
createSideEffect(null, () => {
firedOnUnmount++;
});

Expand All @@ -153,6 +155,40 @@ describe('Hooks', function() {

strictEqual(firedOnUnmount, 1);
});

it('will work with createState', async () => {
let firedOnUpdate = 0;
let firedOnUnmount = 0;
let setState;

function Component() {
const [ value, setValue ] = createState({});

setState = setValue;

createSideEffect(
() => {
firedOnUpdate++;
return () => firedOnUpdate++;
},

() => {
firedOnUnmount++;
}
);

return html`<div></div>`;
}

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

await innerHTML(this.fixture, html`<${Component} />`);
await setState({});
await innerHTML(this.fixture, html``);

strictEqual(firedOnUpdate, 2);
strictEqual(firedOnUnmount, 1);
});
});

describe('createState', () => {
Expand Down
16 changes: 13 additions & 3 deletions packages/diffhtml-website/pages/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,15 @@ innerHTML(main, html`<${Example} />`);

The function `createSideEffect` is used to schedule some work after a component
has mounted, unmounted, or updated. This works similar to the `useEffect` hook
found in React.
found in React. There are some differences though. With React, a useEffect hook
is triggered on both mount and update with the same function. The unmount logic
is also triggered before every update.

With `createSideEffect` you will pass one or two functions which represent
mount and unmount respectively. Only one is required. They map directly to
`componentDidMount` and `componentWillUnmount`. If you wish to hook into
`componentDidUpdate`, simply return a new function from the `componentDidMount`
handler.

<a name="create-side-effect-examples"></a>

Expand All @@ -268,11 +276,13 @@ import { createSideEffect } from 'diffhtml-components';

function Example() {
createSideEffect(() => {
console.log('Component has mounted or updated');
console.log('Component has mounted');

return () => {
console.log('Component has unmounted');
console.log('Component has updated');
};
}, () => {
console.log('Component has unmounted');
});

return html`
Expand Down

0 comments on commit 0b01f42

Please sign in to comment.