diff --git a/website/documents/guides/07-lifecycles.md b/website/documents/guides/07-lifecycles.md index 849c93be..970060c0 100644 --- a/website/documents/guides/07-lifecycles.md +++ b/website/documents/guides/07-lifecycles.md @@ -165,6 +165,60 @@ function *Component() { renderer.render(, document.body); ``` +## Cleanup logic +While you can use context iterators to write cleanup logic after `for...of` and `for await...of` loops, this does not handle errors, and this cleanup logic cannot be written outside of component functions. To solve the first issue, you can use `try`/`finally`. When a generator component is removed from the tree, Crank calls the `return` method on the component’s generator object. You can think of it as whatever `yield` expression your component was suspended on being replaced by a `return` statement. This means any loops your component was in when the generator suspended are broken out of, and code after the yield does not execute. + +You can take advantage of this behavior by wrapping your `yield` loops in a `try`/`finally` block to release any resources that your component may have used. + +```jsx +import {renderer} from "@b9g/crank/dom"; + +function *Cleanup() { + try { + while (true) { + yield "Hi"; + } + } finally { + console.log("finally block executed"); + } +} + +renderer.render(, document.body); +console.log(document.body); // "Hi" +renderer.render(null, document.body); +// "finally block executed" +console.log(document.body); // "" +``` + +[The same best practices](https://eslint.org/docs/rules/no-unsafe-finally) which apply to `try`/`finally` statements in regular functions apply to generator components. In short, you should not yield or return anything in the `finally` block. Crank will not use the yielded or returned values and doing so might cause your components to inadvertently swallow errors or suspend in unexpected locations. + +To write cleanup logic which can be abstractd outside the component function, you can use the `cleanup()` method on the context. This method is similar to `flush() and `schedule()` in that it takes a callback. + + +```jsx live +import {renderer} from "@b9g/crank/dom"; +function addGlobalEventListener(ctx, type, listener, options) { + window.addEventListener(type, listener, options); + // ctx.cleanup allows you to write cleanup logic outside the component + ctx.cleanup(() => window.removeEventListener(type, listener, options)); +} + +function *KeyboardListener() { + let key = ""; + const listener = (ev) => { + key = ev.key; + this.refresh(); + }; + + addGlobalEventListener(this, "keypress", listener); + for ({} of this) { + yield
Last key pressed: {key || "N/A"}
+ } +} + +renderer.render(, document.body); +``` + ## Catching Errors It can be useful to catch errors thrown by components to show the user an error notification or to notify error-logging services. To facilitate this, Crank will cause `yield` expressions to rethrow errors which happen when rendering children. You can take advantage of this behavior by wrapping your `yield` operations in a `try`/`catch` block to catch errors caused by children. @@ -202,32 +256,28 @@ function *Catcher() { renderer.render(, document.body); ``` -## Additional cleanup methods - -When a generator component is removed from the tree, Crank calls the `return` method on the component’s generator object. You can think of it as whatever `yield` expression your component was suspended on being replaced by a `return` statement. This means any loops your component was in when the generator suspended are broken out of, and code after the yield does not execute. +## Returning values from generator components -You can take advantage of this behavior by wrapping your `yield` loops in a `try`/`finally` block to release any resources that your component may have used. +When you return from a generator component, the returned value is rendered and the component scope is thrown away, same as would happen when using a function component. This means that while the component cannot have local variables, but represent sequences of renderings. -```jsx +```jsx live import {renderer} from "@b9g/crank/dom"; +function *Component() { + yield
1
; + yield
2
; + return
3
; +} -function *Cleanup() { - try { - while (true) { - yield "Hi"; - } - } finally { - console.log("finally block executed"); +function *App() { + for ({} of this) { + yield ( +
+ + +
+ ); } } -renderer.render(, document.body); -console.log(document.body); // "Hi" -renderer.render(null, document.body); -// "finally block executed" -console.log(document.body); // "" +renderer.render(, document.body); ``` - -[The same best practices](https://eslint.org/docs/rules/no-unsafe-finally) which apply to `try`/`finally` statements in regular functions apply to generator components. In short, you should not yield or return anything in the `finally` block. Crank will not use the yielded or returned values and doing so might cause your components to inadvertently swallow errors or suspend in unexpected locations. - -## Returning Values diff --git a/website/documents/guides/08-reusable-logic.md b/website/documents/guides/08-reusable-logic.md index 14d9c56a..04c57843 100644 --- a/website/documents/guides/08-reusable-logic.md +++ b/website/documents/guides/08-reusable-logic.md @@ -55,10 +55,13 @@ Anything can be passed as a key to the `provide` and `consume` methods, so you c **Note:** Crank does not link “providers” and “consumers” in any way, and doesn’t automatically refresh consumer components when the `provide` method is called. It’s up to you to ensure consumers update when providers update. -### context.schedule -You can pass a callback to the `schedule` method to listen for when the component renders. Callbacks passed to `schedule` fire synchronously after the component commits, with the rendered value of the component as its only argument. Scheduled callbacks fire once per call and callback function (think `requestAnimationFrame`, not `setInterval`). This means you have to continuously call the `schedule` method for each update if you want to execute some code every time your component commits. +### `context.schedule()` +You can pass a callback to the `schedule()` method to listen for when the component renders. Callbacks passed to `schedule` fire synchronously after the component renders, with the rendered value of the component as its only argument. Scheduled callbacks fire once per call and callback function per update (think `requestAnimationFrame()`, not `setInterval()`). This means you have to continuously call the `schedule` method for each update if you want to execute some code every time your component commits. -### context.cleanup +### `context.flush()` +Similar to the `schedule()` method, this method fires when rendering has finished. Unlike the `schedule()` method, this method fires when a component’s rendered DOM is live. This is useful when you need to do something like calling `focus()` on an `` element or perform DOM measurement calculations. Callbacks fire once per call and callback function per update. + +### `context.cleanup()` Similarly, you can pass a callback to the `cleanup` method to listen for when the component unmounts. Callbacks passed to `cleanup` fire synchronously when the component is unmounted. Each registered callback fires only once. The callback is called with the last rendered value of the component as its only argument. ## Strategies for Reusing Logic @@ -73,7 +76,7 @@ import {Context} from "@bikeshaving/crank"; const ContextIntervalSymbol = Symbol.for("ContextIntervalSymbol"); -Context.prototype.setInterval = function(callback, delay, ...args) { +Context.prototype.setInterval = function(callback, delay, ...args) {` const interval = window.setInterval(callback, delay, ...args); if (typeof this[ContextIntervalSymbol] === "undefined") { this[ContextIntervalSymbol] = new Set(); diff --git a/website/src/views/guide.ts b/website/src/views/guide.ts index 2c95613a..834e40d0 100644 --- a/website/src/views/guide.ts +++ b/website/src/views/guide.ts @@ -36,7 +36,6 @@ export default async function Guide({ <${Sidebar} docs=${docs} url=${url} title="Guides" /> <${Main}>

${title}

- 🚧 The docs are a work in progress.🚧 <${Marked} markdown=${body} components=${components} />