From 14c79a5984e083ad3d05eeaa3366f1c3c246c01d Mon Sep 17 00:00:00 2001 From: Brian Kim Date: Tue, 28 Jul 2020 02:00:54 -0400 Subject: [PATCH] final pass --- website/guides/02-elements.md | 8 +-- website/guides/03-components.md | 55 ++++++++++++------- website/guides/04-handling-events.md | 24 ++++---- website/guides/05-async-components.md | 32 ++++++----- website/guides/06-special-props-and-tags.md | 2 +- website/guides/07-lifecycles.md | 30 +++++----- website/guides/08-reusable-logic.md | 4 +- website/guides/10-custom-renderers.md | 52 ++++++++++-------- .../11-reference-for-react-developers.md | 41 +++++++------- 9 files changed, 136 insertions(+), 112 deletions(-) diff --git a/website/guides/02-elements.md b/website/guides/02-elements.md index 6377f0a30..604ef8211 100644 --- a/website/guides/02-elements.md +++ b/website/guides/02-elements.md @@ -18,7 +18,7 @@ const el =
An element
; const el1 = createElement("div", {id: "element"}, "An element"); ``` -The `createElement` function returns an *element*, a JavaScript object. Elements on their own don’t do anything special; instead, Crank provides special classes called *renderers* which interpret elements to produce DOM nodes, HTML strings, WebGL scene graphs, or whatever else you can think of. +The `createElement` function provided by Crank returns an *element*, a JavaScript object. Elements on their own don’t do anything special; instead, we use special classes called *renderers* to interpret elements and produce DOM nodes, HTML strings, WebGL-backed scene graphs, or whatever else you can think of. Crank ships with two renderer subclasses for web development: one for managing DOM nodes, available through the module `@bikeshaving/crank/dom`, and one for creating HTML strings, available through the module `@bikeshaving/crank/html`. You can use these modules to render interactive user interfaces in the browser and HTML responses on the server. @@ -68,7 +68,7 @@ const el1 = createElement("div", {id: "my-id", "class": myClass}); console.log(el.props); // {id: "my-id", "class": "my-class"} ``` -We call this object the *props* object, short for “properties.” The value of each prop is a string if the string-like syntax is used (`key="value"`), or it can be an interpolated JavaScript expression by placing the value in curly brackets (`key={value}`). You can use props to “pass” values into host and component elements, similar to how we “pass” arguments into functions when invoking them. +We call this object the *props* object, short for “properties.” The value of each prop is a string if the string-like syntax is used (`key="value"`), or it can be an interpolated JavaScript expression by placing the value in curly brackets (`key={value}`). You can use props to “pass” values into host and component elements, similar to how you “pass” arguments into functions when invoking them. If you already have an object that you want to use as props, you can use the special JSX `...` syntax to “spread” it into an element. This works similarly to [ES6 spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax). @@ -107,7 +107,7 @@ renderer.render(el, document.body); console.log(document.body.innerHTML); //
a2
``` -Crank also allows arbitrarily nested iterables of values to be interpolated as children, so, for instance, you can insert an array or a set of values into element trees. +Crank also allows arbitrarily nested iterables of values to be interpolated as children, so, for instance, you can insert arrays or sets of elements into element trees. ```jsx const arr = [1, 2, 3]; @@ -141,4 +141,4 @@ console.log(document.body.firstChild === div); // true console.log(document.body.firstChild.firstChild === span); // true ``` -**Note:** We usually avoid using the term “virtual DOM” in Crank, insofar as the core renderer can be extended to target multiple environments; instead, we use the term “element diffing” to mean mostly the same thing. +**Note:** The documentation avoids the terms “virtual DOM” or “DOM diffing” insofar as the core renderer can be extended to target multiple environments; instead, we use the terms “virtual elements” and “element diffing” to mean mostly the same thing. diff --git a/website/guides/03-components.md b/website/guides/03-components.md index 3a786d75e..6a58a4b24 100644 --- a/website/guides/03-components.md +++ b/website/guides/03-components.md @@ -13,7 +13,8 @@ function Greeting({name}) { } renderer.render(, document.body); -console.log(document.body.innerHTML); // "
Hello World
" +console.log(document.body.innerHTML); +// "
Hello World
" ``` Component elements can be passed children just as host elements can. The `createElement` function will add children to the props object under the name `children`, and it is up to the component to place these children somewhere in the returned element tree. If you don’t use the `children` prop, it will not appear in the rendered output. @@ -34,7 +35,8 @@ renderer.render( document.body, ); -console.log(document.body.innerHTML); // "
Message for Nemo: Howdy
" +console.log(document.body.innerHTML); +// "
Message for Nemo: Howdy
" ``` ## Stateful Components @@ -72,10 +74,10 @@ console.log(document.body.innerHTML); // "
You have updated this component 1 time
" ``` -By yielding elements rather than returning them, we can make components stateful using variables in the generator’s local scope. Every time a generator component is rendered, Crank resumes the generator, pausing at the next `yield`. The yielded expression, usually an element, is then recursively rendered, just as if it were returned from a function component. Furthermore, Crank uses the same diffing algorithm which reuses DOM nodes to reuse generator objects, so that the execution of generator components are preserved between renders. +By yielding elements rather than returning them, we can make components stateful using variables in the generator’s local scope. Crank uses the same diffing algorithm which reuses DOM nodes to reuse generator objects, so that their executions are preserved between renders. Every time a generator component is rendered, Crank resumes the generator and executes the generator until the next `yield`. The yielded expression, usually an element, is then rendered as the element’s children, just as if it were returned from a function component. ### Contexts -In the preceding example, the `Counter` component’s local state changes when it is rerendered, but we may want to write components which update themselves instead according to timers or events. Crank allows components to control themselves by passing in an object called a *context* as the `this` keyword of each component. Contexts provide several utility methods, most important of which is the `refresh` method, which tells Crank to update the related component instance in place. +In the preceding example, the `Counter` component’s local state changed when it was rerendered, but we may want to write components which update themselves according to timers or events instead. Crank allows components to control their own execution by passing in an object called a *context* as the `this` keyword of each component. Contexts provide several utility methods, most important of which is the `refresh` method, which tells Crank to update the related component instance in place. ```jsx function *Timer() { @@ -99,10 +101,10 @@ function *Timer() { This `Timer` component is similar to the `Counter` one, except now the state (the local variable `seconds`) is updated in the callback passed to `setInterval`, rather than when the component is rerendered. Additionally, the `refresh` method is called to ensure that the generator is stepped through whenever the `setInterval` callback fires, so that the rendered DOM actually reflects the updated `seconds` variable. -One important detail about the `Timer` example is that it cleans up after itself with `clearInterval`. Crank will call the `return` method on generator components when the element is unmounted, so that the finally block executes and `clearInterval` is called. In this way, you can use the natural lifecycle of a generator to write setup and teardown logic for components, all within the same scope. +One important detail about the `Timer` example is that it cleans up after itself with `clearInterval` in the `finally` block. Crank will call the `return` method on an element’s related generator object when it is unmounted. ### Props Updates -The generator components we’ve seen so far haven’t used props. Generator components can accept props as its first parameter just like regular function components. +The generator components we’ve seen so far haven’t used props. Generator components can accept props as their first parameter just like regular function components. ```jsx function *LabeledCounter({message}) { @@ -113,13 +115,22 @@ function *LabeledCounter({message}) { } } -renderer.render(, document.body); +renderer.render( + , + document.body, +); + console.log(document.body.innerHTML); // "
The count is now: 1
" -renderer.render(, document.body); + +renderer.render( + , + document.body, +); + console.log(document.body.innerHTML); // "
The count is now: 2
" renderer.render( - , + , document.body, ); @@ -127,7 +138,7 @@ renderer.render( console.log(document.body.innerHTML); // "
The count is now: 3
" ``` -This mostly works, except now we have a bug where the component kept yielding the initial message even though a new message was passed in via props. To fix this, we can make sure props are kept up to date by iterating over the context: +This mostly works, except we have a bug where the component keeps yielding elements with the initial message even though a new message was passed in via props. We can make sure props are kept up to date by iterating over the context: ```jsx function *Counter({message}) { @@ -140,21 +151,26 @@ function *Counter({message}) { } } -renderer.render(, document.body); +renderer.render( + , + document.body, +); + console.log(document.body.innerHTML); // "
The count is now: 1
" + renderer.render( - , + , document.body, ); -console.log(document.body.innerHTML); -// "
What if I update the message: 2
" + +console.log(document.body.innerHTML); // "
Le décompte est maintenant: 2
" ``` -By replacing the `while` loop with a `for…of` loop which iterates over `this`, you can get the latest props each time the generator is resumed. This is possible because contexts are an iterable of the latest props passed to elements. +By replacing the `while` loop with a `for…of` loop which iterates over `this`, you can get the latest props each time the generator is resumed. This is possible because contexts are an iterable of the latest props passed to components. ### Comparing Old and New Props -One idiom we see in the preceding example is that we overwrite the variables declared via the generator’s parameters with the destructuring expression in the `for…of` statement. This is an easy way to make sure those variables stay in sync with the current props of the component. However, there is no requirement that you must always overwrite old props in the `for` expression, meaning you can assign new props to a different variable and compare them against the old ones: +One Crank idiom we see in the preceding example is that we overwrite the variables declared via the generator’s parameters with the destructuring expression in the `for…of` statement. This is an easy way to make sure those variables stay in sync with the current props of the component. However, there is no requirement that you must always overwrite old props in the `for` expression, meaning you can assign new props to a different variable and compare them against the old props. ```jsx function *Greeting({name}) { @@ -185,7 +201,7 @@ console.log(document.body.innerHTML); // "
Hello again Bob
" The fact that state is just local variables allows us to blur the lines between props and state, in a way that is easy to understand and without lifecycle methods like `componentWillUpdate` from React. With generators and `for` loops, comparing old and new props is as easy as comparing adjacent elements of an array. ## Default Props -You may have noticed in the preceding examples that we used [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring) on the props parameter for convenience. You can further assign default values to a specific prop by using JavaScript’s default value syntax. +You may have noticed in the preceding examples that we used [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring) on the props parameter for convenience. You can further assign default values to specific props by using JavaScript’s default value syntax. ```jsx function Greeting({name="World"}) { @@ -195,12 +211,13 @@ function Greeting({name="World"}) { renderer.render(, document.body); // "
Hello World
" ``` -This works well for function components, but for generator components, you should make sure that you use the same default value in both the parameter list and the `for` statement. +This syntax works well for function components, but for generator components, you should make sure that you use the same default value in both the parameter list and the `for` statement. ```jsx function *Greeting({name="World"}) { + yield
Hello, {name}
; for ({name="World"} of this) { - yield
Hello, {name}
; + yield
Hello again, {name}
; } } ``` diff --git a/website/guides/04-handling-events.md b/website/guides/04-handling-events.md index 39b3246b5..0d63dc3d1 100644 --- a/website/guides/04-handling-events.md +++ b/website/guides/04-handling-events.md @@ -2,7 +2,7 @@ title: Handling Events --- -Most web applications require some measure of interactivity, where the user interface updates according to user input. To facilitate this, Crank provides two APIs for listening to events on rendered DOM nodes. +Most web applications require some measure of interactivity, where the user interface updates according to input. To facilitate this, Crank provides two APIs for listening to events on rendered DOM nodes. ## DOM onevent Props You can attach event callbacks to host element directly using onevent props. These props start with `on`, are all lowercase, and correspond to the properties as specified according to the DOM’s [GlobalEventHandlers mixin API](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers). By combining event props, local variables and `this.refresh`, you can write interactive components. @@ -49,9 +49,7 @@ The local state `count` is now updated in the event listener, which triggers whe **NOTE:** When using the context’s `addEventListener` method, you do not have to call the `removeEventListener` method if you merely want to remove event listeners when the component is unmounted. This is done automatically. -## Event Delegation - -The context’s `addEventListener` method only attaches to the top-level node or nodes which each component renders, so if you want to listen to events on a nested node, you must use event delegation: +The context’s `addEventListener` method only attaches to the top-level node or nodes which each component renders, so if you want to listen to events on a nested node, you must use event delegation. ```jsx function *Clicker() { @@ -77,14 +75,14 @@ function *Clicker() { Because the event listener is attached to the outer `div`, we have to filter events by `ev.target.tagName` in the listener to make sure we’re not incrementing `count` based on clicks which don’t target the `button` element. ## onevent vs EventTarget -The props-based onevent API and the context-based EventTarget API both have their advantages. On the one hand, using onevent props means you don’t have to filter events by target, and you can register them on exactly the element you’d like to listen to. +The props-based onevent API and the context-based EventTarget API both have their advantages. On the one hand, using onevent props means you don’t have to filter events by target. You register them on exactly the element you’d like to listen to. -On the other, using the `addEventListener` method allows you to take full advantage of the EventTarget API, including registering passive event listeners or events which are dispatched during the capture phase. Additionally, the EventTarget API can be used without referencing or accessing the child elements which a component renders, meaning you can use it to listen to components whose children are passed in, or in utility functions which don’t have access to produced elements. +On the other, using the `addEventListener` method allows you to take full advantage of the EventTarget API, which includes registering passive event listeners or listeners which are dispatched during the capture phase. Additionally, the EventTarget API can be used without referencing or accessing the child elements which a component renders, meaning you can use it to listen to components which are passed children, or in utility functions which don’t have access to produced elements. Crank supports both API styles for convenience and flexibility. -## Dispatching events -Crank contexts implement the full EventTarget interface, meaning you can use the `dispatchEvent` method and the [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) class to dispatch events to ancestor components: +## Dispatching Events +Crank contexts implement the full EventTarget interface, meaning you can use [the `dispatchEvent` method](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent) and [the `CustomEvent` class](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) to dispatch custom events to ancestor components: ```jsx function MyButton(props) { @@ -132,11 +130,11 @@ The preceding example also demonstrates a slight difference in the way the `addE ## Form Elements -Form elements like inputs and textareas are stateful and by default update themselves automatically according to user input. JSX libraries like React and Inferno handle these types of elements by allowing their virtual representations to be “controlled” or “uncontrolled,” where being controlled means that the internal DOM node’s state is synced to the virtual representation’s props. +Form elements like inputs and textareas are stateful and by default update themselves automatically according to user input. JSX libraries like React and Inferno handle these types of elements by allowing their virtual representations to be “controlled” or “uncontrolled,” where being controlled means that the internal DOM node’s state is synced to the virtual representation’s props. These APIs manifest as special “uncontrolled” versions of props like `defaultValue` and `defaultChecked`. -Crank’s philosophy with regard to this issue is slightly different, in that we do not view the virtual elements as the “source of truth” for the underlying DOM node. In practice, this design decision means that renderers do not retain the previously rendered props for host elements. For instance, Crank will not compare old and new props between renders to avoid mutating props which have not changed, and instead attempt to update every prop in props when it is rerendered. +Crank’s approach to this issue is slightly different, in that we do not view the virtual elements as the “source of truth” for the underlying DOM nodes. In practice, this design decision means that renderers do not retain the previously rendered props for host elements. For instance, Crank will not compare old and new props between renders to avoid mutating props which have not changed, and instead attempt to update every prop found in props. -Another consequence is that we don’t delete props which were present in one rendering and absent in the next. In the following example, the checkbox will not be unchecked if you press the button. +Another consequence is that we don’t delete props which were present in one rendering and absent in the next. In the following example, the checkbox will never uncheck itself. ```jsx function *App() { @@ -195,7 +193,7 @@ function *App() { } ``` -This design decision means that we now have a way to have form elements be “uncontrolled” and “controlled” within the same component. Here, for instance, is an input element which is uncontrolled, except that it resets when the button is clicked. +This design decision means that we now have a way to make the same element prop both “uncontrolled” and “controlled” for an element. Here, for instance, is an input element which is uncontrolled, except that it resets when the button is clicked. ```jsx function* ResettingInput() { @@ -220,4 +218,4 @@ function* ResettingInput() { } ``` -We use the `reset` flag to check whether we need to set the `value` of the underlying input DOM element, and we omit the `value` prop when we aren’t performing a reset. The input is therefore both uncontrolled in that we do not track its value, but also controllable via normal rendering when we want to reset its `value` to the empty string. +In the above example, we use the `reset` flag to check whether we need to set the `value` prop of the underlying input DOM element, and we omit the `value` prop when we aren’t performing a reset. Because the prop is not cleared when absent from the virtual element’s props, Crank leaves it alone. Crank’s approach means we do not need special alternative props for uncontrolled behavior, and we can continue to use virtual element rendering over raw DOM mutations in those circumstances where we do need control. diff --git a/website/guides/05-async-components.md b/website/guides/05-async-components.md index 4eda5f731..da6247e78 100644 --- a/website/guides/05-async-components.md +++ b/website/guides/05-async-components.md @@ -2,7 +2,7 @@ title: Async Components --- -## Async function components +## Async Function Components So far, every component we’ve seen has worked synchronously, and Crank will respect this as an intentional decision by the developer by keeping the entire process of rendering synchronous from start to finish. However, modern JavaScript includes promises and `async`/`await`, which allow you to write concurrently executing code as if it were synchronous. To facilitate these features, Crank allows components to be asynchronous functions as well, and we call these components, *async function components*. ```jsx @@ -18,9 +18,9 @@ async function IPAddress () { })(); ``` -When a Crank renderer runs a component which returns a promise, the rendering process becomes asynchronous as well. Concretely, this means that `render` or `refresh` calls which render async compenents return a promise which fulfills when all async components in the tree have fulfilled. It also means that no actual DOM updates will be triggered until that moment. +When Crank renders an async component anywhere in the tree, the entire process becomes asynchronous. Concretely, this means that `renderer.render` or `this.refresh` calls return a promise which fulfills when rendering has finished. It also means that no actual DOM updates will be triggered until this moment. -### Concurrent updates +### Concurrent Updates Because async function components can be rerendered while they are still pending, Crank implements a couple rules to make concurrent updates predictable and performant: 1. There can only be one pending run of an async function component at the same time for an element in the tree. If the same async component is rerendered concurrently while it is still pending, another call is enqueued with the latest props. @@ -35,23 +35,23 @@ async function Delay ({message}) { const p1 = renderer.render(, document.body); console.log(document.body.innerHTML); // "" await p1; - console.log(document.body.innerHTML); // "Run 1" + console.log(document.body.innerHTML); // "
Run 1
" const p2 = renderer.render(, document.body); // These renders are enqueued because the second render is still pending. const p3 = renderer.render(, document.body); const p4 = renderer.render(, document.body); - console.log(document.body.innerHTML); // "Run 1" + console.log(document.body.innerHTML); // "
Run 1
" await p2; - console.log(document.body.innerHTML); // "Run 2" + console.log(document.body.innerHTML); // "
Run 2
" // By the time the third render fulfills, the fourth render has already completed. await p3; - console.log(document.body.innerHTML); // "Run 4" + console.log(document.body.innerHTML); // "
Run 4
" await p4; - console.log(document.body.innerHTML); // "Run 4" + console.log(document.body.innerHTML); // "
Run 4
" })(); ``` -In the preceding example, at no point is there more than one simultaneous call to the `Delay` component, despite the fact that it is rerendered concurrently for its second through fourth renders. And because these renderings are enqueued, only the second and fourth renderings have any effect. This is because the element is busy with the second render by the time the third and fourth renderings are requested, and then, only the fourth rendering is actually executed because third rendering’s props are obsolete by the time the component is ready to update again. This behavior allows async components to always be kept up-to-date without producing excess calls to async code. +In the preceding example, at no point is there more than one simultaneous call to the `Delay` component, despite the fact that it is rerendered concurrently for its second through fourth renders. And because these renderings are enqueued, only the second and fourth renderings have any effect. This is because the element is busy with the second render by the time the third and fourth renderings are requested, and then, only the fourth rendering is actually executed because third rendering’s props are obsolete by the time the component is ready to update again. This behavior allows async components to always be kept up-to-date without producing excess calls to the function. 2. If two different async components are rendered in the same position, the components are raced. If the earlier component fulfills first, it shows until the later component fulfills. If the later component fulfills first, the earlier component is never rendered. @@ -97,7 +97,7 @@ TODO: this section is too hard to understand and requires code examples so we’ When Crank encounters an async component anywhere in the element tree, the entire rendering process becomes asynchronous. Therefore, async child components make parent components asynchronous, and sync function and generator components behave differently when they produce async children. On the one hand, sync function components transparently pass updates along to async children, so that when a renderer updates a sync function component concurrently, its async children will also enqueue an update immediately. On the other hand, sync generator components which produce async elements will not resume until those async children have fulfilled. This is because sync generators expect to be resumed after their children have rendered, and the actual DOM nodes which are created are passed back into the generator, but they wouldn’t be available if the generator was concurrently resumed before the async children had settled. --> -## Async generator components +## Async Generator Components Just as you can write stateful components with sync generator functions, you can also write stateful *async* components with *async generator functions*. ```jsx @@ -131,10 +131,14 @@ async function *AsyncLabeledCounter ({message}) { })(); ``` -`AsyncLabeledCounter` is an async version of the `LabeledCounter` example introduced in [the section on props updates](./components#props-updates). This example demonstrates several key differences between sync and async generator components. Firstly, rather than using `while` or `for…of` loops as with sync generator components, we now use a `for await…of` loop. This is possible because contexts are not just an *iterable* of props, but also an *async iterable* of props as well. Secondly, you’ll notice that the async generator yields multiple times per iteration over `this`, once to show a loading message and once to show the actual count. While it is possible for sync generators components to yield multiple times per iteration over `this`, it wouldn’t necessarily make sense to do so because generators suspend at each yield, and upon resuming a second time within the same loop, the props would be stale. In contrast, async generator components are continuously resumed. Rather than suspending at each yield, we rely on the `for await…of` loop, which suspends at the bottom until the next update. +`AsyncLabeledCounter` is an async version of the `LabeledCounter` example introduced in [the section on props updates](./components#props-updates). This example demonstrates several key differences between sync and async generator components. Firstly, rather than using `while` or `for…of` loops as with sync generator components, we now use [a `for await…of` loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of). This is possible because contexts are not just an *iterable* of props, but also an *async iterable* of props as well. -## Responsive Loading Indicators -The async components we’ve seen so far have been all or nothing, in the sense that Crank can’t show anything until all components in the tree have fulfilled. This can be a problem when you have an async call which takes a long time. It would be nice if parts of the element tree could be shown without waiting, to create responsive user experiences. However, because loading indicators which show immediately can paradoxically make your app seem less responsive, we can use the async rules described previously along with async generator functions to show loading indicators which appear only when certain promises take too long to settle. +Secondly, you’ll notice that the async generator yields multiple times per iteration over `this`, once to show a loading message and once to show the actual count. While it is possible for sync generators components to yield multiple times per iteration over `this`, it wouldn’t necessarily make sense to do so because generators suspend at each yield, and upon resuming a second time within the same loop, the props would be stale. In contrast, async generator components are continuously resumed. Rather than suspending at each yield, we rely on the `for await…of` loop, which suspends at its end until the next update. + +### Loading Indicators +The async components we’ve seen so far have been all or nothing, in the sense that Crank can’t show anything until all promises in the tree have fulfilled. This can be a problem when you have an async call which takes longer than expected. It would be nice if parts of the element tree could be shown without waiting, to create responsive user experiences. + +However, because loading indicators which show immediately can paradoxically make your app seem less responsive, we use the async rules described previously along with async generator functions to show loading indicators which appear only when certain components take too long. ```jsx async function LoadingIndicator() { @@ -214,4 +218,4 @@ async function *Suspense({timeout, fallback, children}) { })(); ``` -No special tags are needed for async loading states, and the functionality to write this complex logic is implemented using the same element diffing algorithm that governs synchronous components. Additionally, this approach is more flexible in the sense that you can extend it; for instance, you can add another yield to the `for await…of` loop to show a second fallback state which waits ten seconds, to inform the user that something went wrong or that servers are slow to respond. +No special tags are needed for async loading states, and the functionality to write this logic is implemented using the same element diffing algorithm that governs synchronous components. Additionally, this approach is more flexible in the sense that you can extend it; for instance, you can add another yield to the `for await…of` loop to show a second fallback state which waits ten seconds, to inform the user that something went wrong or that servers are slow to respond. diff --git a/website/guides/06-special-props-and-tags.md b/website/guides/06-special-props-and-tags.md index c6dca80be..c00e16fd3 100644 --- a/website/guides/06-special-props-and-tags.md +++ b/website/guides/06-special-props-and-tags.md @@ -2,7 +2,7 @@ title: Special Props and Tags --- -Crank provides certain APIs in the form of special props or element tags. +Crank provides certain APIs in the form of special props or element tags. The following is an overview of these props and tags. ## Special Props The following props apply to all elements, regardless of tag or renderer. diff --git a/website/guides/07-lifecycles.md b/website/guides/07-lifecycles.md index e0725ae83..430d7f139 100644 --- a/website/guides/07-lifecycles.md +++ b/website/guides/07-lifecycles.md @@ -4,9 +4,9 @@ title: Lifecycles Crank uses generator functions rather than hooks or classes to define component lifecycles. Internally, this is achieved by calling the `next`, `return` and `throw` methods of the returned generator object as components are mounted, updated and unmounted from the element tree. As a developer, you can use the `yield`, `return`, `try`, `catch`, and `finally` keywords within your generator components to take full advantage of the generator’s natural lifecycle. -## Returning +## Returning Values -Usually, you’ll yield in generator components so that they can continue to respond to updates, but you may want to also `return` a final state. Unlike function components, which are called and returned once for each update, once a generator component returns, it will never update again. +In most generator components, you will yield children within a loop so that they can continue to respond to updates. However, you may also want to return a final state. Unlike function components, which are called and returned once for each update, once a generator component returns, its rendered value is final, and the component will never update again. ```jsx function *Stuck({message}) { @@ -21,7 +21,7 @@ renderer.render(, document.bo console.log(document.body.innerHTML); // "
Hello
" ``` -You should be careful when writing generator components to make sure that you always place your `yield` operators in a `for` or `while` loop. If you forget and implicitly return from the generator, it will stop updating, nothing will be rendered, and the only way to restart the component will be to unmount and remount the component into the element tree. +You should be careful when writing generator components to make sure that you always place your `yield` operators in a `for` or `while` loop. If you forget and implicitly return from the generator, it will stop updating and nothing will be rendered ever again. ```jsx function *Numbers() { @@ -40,15 +40,11 @@ renderer.render(, document.body); console.log(document.body.innerHTML); // "" renderer.render(, document.body); console.log(document.body.innerHTML); // "" - -renderer.render(null, document.body); -renderer.render(, document.body); -console.log(document.body.innerHTML); // "1" ``` ## Cleaning Up -When a generator component is removed from the tree, Crank calls the `return` method on the 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. +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. @@ -70,10 +66,10 @@ renderer.render(null, document.body); console.log(document.body); // "" ``` -[The same best practices](https://eslint.org/docs/rules/no-unsafe-finally) which apply to `try`/`finally` blocks 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 produced values and doing so might cause your components to inadvertently swallow errors or suspend in an unexpected location. +[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. ## Catching Errors -We all make mistakes, and it can be useful to catch errors in our components so that we can show the user something or notify error-logging services. To facilitate this, Crank will catch errors thrown when rendering child elements and throw them back into parent generator components by calling the `throw` method on the generator object. You can think of it as whatever `yield` expression your component was suspended on being replaced with a `throw` statement with the error set to whatever was thrown by the component’s children. +We all make mistakes, and it can be useful to catch errors thrown by our components so that we can show the user something or notify error-logging services. To facilitate this, Crank will catch errors thrown when rendering child elements and throw them back into parent generator components using the `throw` method on the component’s generator object. You can think of it as whatever `yield` expression your component was suspended on being replaced with a `throw` statement with the error set to whatever was thrown by the component’s children. You can take advantage of this behavior by wrapping your `yield` operations in a `try`/`catch` block to catch errors caused by children. @@ -98,7 +94,7 @@ renderer.render(, document.body); console.log(document.body.innerHTML); // "
Error: Hmmm
" ``` -This component “sticks” at the return so that the same error message is shown until the component is unmounted. However, you may also want to recover from errors as well, and you can do this by ignoring or handling the error. +As explained previously, this component “sticks” because it uses a return statement, so that the same error message is shown until the component is unmounted. However, you may also want to recover from errors as well, and you can do this by ignoring or handling the error. ```jsx function T1000() { @@ -117,13 +113,17 @@ function *Terminator() { } renderer.render(, document.body); -console.log(document.body.innerHTML); // "
Come with me if you want to live
" +console.log(document.body.innerHTML); +// "
Come with me if you want to live
" renderer.render(, document.body); -console.log(document.body.innerHTML); // "
I’ll be back
" +console.log(document.body.innerHTML); +// "
I’ll be back
" renderer.render(, document.body); -console.log(document.body.innerHTML); // "
Come with me if you want to live
" +console.log(document.body.innerHTML); +// "
Come with me if you want to live
" renderer.render(, document.body); -console.log(document.body.innerHTML); // "
I’ll be back
" +console.log(document.body.innerHTML); +// "
I’ll be back
" ``` ## Accessing Rendered Values diff --git a/website/guides/08-reusable-logic.md b/website/guides/08-reusable-logic.md index 2c7c2562c..a54ff827d 100644 --- a/website/guides/08-reusable-logic.md +++ b/website/guides/08-reusable-logic.md @@ -49,11 +49,11 @@ renderer.render( console.log(document.body); // "

Hello, Brian

" ``` -Provisions allow libraries to define components which interact with their descendants without rigidly defined component hierarchies or requiring the developer to pass data manually between components as props. This makes them useful, for instance, when writing multiple components which communicate with each other, like custom `select` and `option` form elements, or drag-and-drop components. +Provisions allow libraries to define components which interact with their descendants without rigidly defined component hierarchies or requiring data to be passed manually between components via props. This makes them useful, for instance, when writing multiple components which communicate with each other, like custom `select` and `option` form elements, or drag-and-drop components. Anything can be passed as a key to the `provide` and `consume` methods, so you can use a symbol to ensure that the provision you pass between your components are private and do not collide with provisions set by others. -**Note:** Crank does not link “providers” and “consumers” in any way, and doesn’t automatically refresh consumer components when `provide` is called, so it’s up to you to make sure consumers update when providers update. +**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 parameter. They only 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. diff --git a/website/guides/10-custom-renderers.md b/website/guides/10-custom-renderers.md index 37f9aa729..1f451a6dd 100644 --- a/website/guides/10-custom-renderers.md +++ b/website/guides/10-custom-renderers.md @@ -47,9 +47,9 @@ class Renderer< For example, the HTML string renderer has an internal node representation, but converts these nodes to strings before they’re exposed to consumers. This is because the internal nodes must be a referentially unique object which is mutated during rendering, while JavaScript strings are referentially transparent and immutable. Therefore, the `TResult` type of the HTML renderer is `string`. ## Methods -The following is a description of the signatures of internal renderer methods and when they’re executed. +The following is a description of the signatures of internal renderer methods and when they’re executed. When creating a custom renderer, you are expected to override these methods via inheritance. -### Renderer.prototype.create +### `Renderer.prototype.create` ```ts create( @@ -57,11 +57,12 @@ create( ): TNode; ``` -The `create` method is called for each host element the first time the element is committed. This method is passed the current host element and scope, and should return the node which will be associated with the host element. This node will remain constant for an element for the duration that the element is mounted in the tree. +The `create` method is called for each host element the first time the element is committed. This method is passed the current host element and scope, and should return the node which will be associated with the host element. This node will remain constant for an element for as long as the element is rendered. By default, this method will throw a `Not Implemented` error, so custom renderers should always implement this method. -### Renderer.prototype.read +### `Renderer.prototype.read` + ```ts read(value: Array | TNode | string | undefined): TResult; ``` @@ -75,22 +76,22 @@ The renderer exposes rendered values in the following places: - Via the context’s `value` getter method - As the yield value of generator components -When an element or elements are read in this way, we call the `read` method to give renderers a chance to manipulate what is exposed, so as to hide internal implementation details and return something which makes sense for the target environment. The parameter passed to read can be a node, a string, an array of nodes and strings, or undefined. The return value is what is actually exposed. +When an element or elements are read in this way, we call the `read` method to give renderers a final chance to manipulate what is exposed, so as to hide internal implementation details and return something which makes sense for the target environment. The parameter passed to the `read` method can be a node, a string, an array of nodes and strings, or `undefined`. The return value is what is actually exposed. This method is optional. By default, read is an identity function which returns the value passed in. -### Renderer.prototype.patch +### `Renderer.prototype.patch` + ```ts -patch( - el: Element, node: TNode, -): unknown; +patch(el: Element, node: TNode): unknown; ``` -The `patch` method is called for each host element whenever it is committed. This method is passed the current host element and its related node, and its return value is ignored. This method is usually where you would mutate the properties of the internal node according to the props of the host element. +The `patch` method is called for each host element whenever it is committed. This method is passed the current host element and its related node, and its return value is ignored. This method is usually where you would mutate the internal node according to the props of the host element. Implementation of this method is optional for renderers. -### Renderer.prototype.arrange +### `Renderer.prototype.arrange` + ```ts arrange( el: Element, @@ -99,22 +100,22 @@ arrange( ): unknown; ``` -The `arrange` method is called whenever an element’s children have changed. It is called with the current host element, the host element’s related node, and the rendered values of all the element’s descendants as an array. In addition to when a host element commits, the `arrange` method may also be called when a child refreshes or otherwise causes the host element’s rendered children to change. Because the `arrange` method is called for every root/portal element, the parent can be of type `TRoot` as well as `TNode`. +The `arrange` method is called whenever an element’s children have changed. It is called with the current host element, the host element’s related node, and the rendered values of all the element’s descendants as an array. In addition to when a host element commits, the `arrange` method may also be called when a child refreshes or otherwise causes a host element’s rendered children to change. Because the `arrange` method is called for every root/portal element, the parent can be of type `TRoot` as well as `TNode`. + +This method is where the magic happens, and is where you connect the nodes of your target environment into an internal tree. -This method is where the magic happens, and is where you connect the nodes of your target environment as an internal tree. +### `Renderer.prototype.scope` -### Renderer.prototype.scope ```ts -scope( - el: Element, scope: TScope | undefined -): TScope; +scope(el: Element, scope: TScope | undefined): TScope; ``` -The `scope` method is called for each host or portal element as elements are mounted or updated. Unlike the other custom renderer methods, the `scope` method is called during the pre-order traversal of the tree, much as components are. The `scope` method is passed the current host element and scope, and the return value becomes the scope argument passed to the `create` and `scope` method calls for child host elements. +The `scope` method is called for each host or portal element as elements are mounted or updated. Unlike the other custom renderer methods, the `scope` method is called during the pre-order traversal of the tree, much as components are. The `scope` method is passed the current host element and scope as parameters, and the return value becomes the scope argument passed to the `create` and `scope` method calls for descendant host elements. -By default, the scope method returns `undefined`, meaning the scope will be `undefined` throughout your application. +By default, the `scope` method returns `undefined`, meaning the scope will be `undefined` throughout your application. + +### `Renderer.prototype.escape` -### Renderer.prototype.escape ```ts escape(text: string, scope: TScope): string; ``` @@ -125,7 +126,8 @@ One important detail is that `escape` should not return text nodes or anything b By default, the `escape` method returns the string which was passed in. -### Renderer.prototype.parse +### `Renderer.prototype.parse` + ```ts parse(text: string, scope: TScope): TNode | string; ``` @@ -134,7 +136,8 @@ When the renderer encounters a `Raw` element whose `value` prop is a string, it By default, the `parse` method returns the string which was passed in. -### Renderer.prototype.dispose +### `Renderer.prototype.dispose` + ```ts dispose(el: Element, node: TNode): unknown ``` @@ -143,11 +146,12 @@ The `dispose` method is called whenever a host element is unmounted. It is calle This method is optional and its return value is ignored. -### Renderer.prototype.complete +### `Renderer.prototype.complete` + ```ts complete(root: TRoot): unknown; ``` -The `complete` method is called at the end of every render execution, when all elements have been committed and all other renderer methods have been called. It is useful, if your rendering target needs some final render method to be executed before any mutations take effect. +The `complete` method is called at the end of every render execution, when all elements have been committed and all other renderer methods have been called. It is useful, for instance, if your rendering target needs some final code to execute before any mutations take effect. This method is optional and its return value is ignored. diff --git a/website/guides/11-reference-for-react-developers.md b/website/guides/11-reference-for-react-developers.md index 9f2e3634c..e2adaf006 100644 --- a/website/guides/11-reference-for-react-developers.md +++ b/website/guides/11-reference-for-react-developers.md @@ -31,7 +31,7 @@ async function *ReactComponent(props) { } ``` -The example is pseudocode which demonstrates where React’s class methods would be called relative to an async generator component. Refer to the [guide on lifecycles](./lifecycles) for more information on using generator functions. +This example is pseudocode which demonstrates where React’s class methods would be called relative to an async generator component. Refer to the [guide on lifecycles](./lifecycles) for more information on using generator functions. The following are specific equivalents for React methods. @@ -50,7 +50,7 @@ Setup code for components can be written at the top of generator components. It As an alternative to React’s `shouldComponentUpdate` method, you can use `Copy` elements to prevent the rerendering of a specific subtree. Refer to [the description of `Copy` elements](./special-props-and-tags#copy) for more information. ### `getDerivedStateFromProps`, `componentWillUpdate` and `getSnapshotBeforeUpdate` -Code which compares old and new props or state can be written directly in your components. See the section on [`prop updates`](./components#comparing-old-and-new-props) for an example of a component which compares old and new props. +Code which compares old and new props or state can be written directly in your components. See the section on [prop updates](./components#comparing-old-and-new-props) for an example of a component which compares old and new props. ### `componentDidUpdate` To execute code after rendering, you can use async generator components or [the `schedule` method](./api-reference#schedule). See [the guide on accessing rendered values](./lifecycles#accessing-rendered-values) for more information. @@ -62,22 +62,21 @@ You can use a `try`/`finally` block to run code when a component is unmounted. Y To catch errors which occur in child components, you can use generator components and wrap `yield` operations in a `try`/`catch` block. Refer to [the relevant guide on catching errors](./lifecycles#catching-errors). ## Hooks -Crank does not implement any APIs similar to React Hooks. The following are alternatives to specific hooks. +Crank does not implement any APIs similar to React Hooks. The main appeal of hooks for library authors is that you can encapsulate entire APIs in one or two hooks. Refer to [the guide on reusable logic](./reusable-logic#strategies-for-reusing-logic) for a description of strategies you can use to reuse logic and write library wrappers in Crank. + +The following are alternatives to specific hooks. ### `useState` and `useReducer` Crank uses generator functions and local variables for local state. Refer to [the section on stateful components](./components#stateful-components). ### `useEffect` and `useLayoutEffect` -Crank does not have any requirements that rendering should be “pure.” In other words, you can trigger side-effects directly while rendering because Crank does not execute components more times than you might expect. +Crank does not have any requirements that rendering should be “pure.” In other words, you can trigger side-effects directly while rendering because Crank does not execute components more times than you might expect. Refer to [the guide on accessing rendered values](./lifecycles#accessing-rendered-values) for more information on code which executes after rendering. ### `useMemo` and `useCallback` Because the execution of generator components is preserved, there is no need to “memoize” or “cache” callbacks or other values. You can simply assign them to a constant variable. ### `useImperativeHandle` -Crank does not have a way to access component instances, and parent components should not access child components directly. A wrapper which uses web components for the definition of custom elements with imperative methods and properties is planned. - -### Custom Hooks -The main appeal of hooks for library authors is that you can encapsulate entire APIs in one or two hooks. Refer to [the guide on reusable logic](./reusable-logic#strategies-for-reusing-logic) for various patterns and strategies to wrap APIs and logic in Crank. +Crank does not have a way to access component instances, and parent components should not access child components directly. A web component wrapper for defining custom elements with imperative APIs is planned. ## Suspense and Concurrent Mode Crank uses async functions and promises for scheduling and coordinating async processes. See the [guide on async components](./async-components) for an introduction to async components, as well as a demonstration of how you can implement the `Suspense` component directly in user space. @@ -89,7 +88,7 @@ Crank is written in TypeScript, and you can add type checking to components by t Crank does not restrict children in JSX elements to just arrays. You can interpolate ES6 maps, sets or any other iterable into your Crank elements. Additionally, Crank does not warn you if elements in the iterable are unkeyed. ## Fragments -The [`Fragment` element](./special-props-and-tags#fragment) works almost exactly the same as it does in React, except in Crank you can also use a callback ref to access its contents. +The [`Fragment` element](./special-props-and-tags#fragment) works almost exactly as it does in React, except that in Crank you can also use a callback ref to access its contents. ## `React.cloneElement` You can clone elements using [the `cloneElement` function](./api-reference#cloneelement). @@ -98,45 +97,47 @@ You can clone elements using [the `cloneElement` function](./api-reference#clone The `createPortal` function is replaced by the special `Portal` element, whose behavior and expected props varies according to the target rendering environment. Refer to [the guide on the `Portal` element](./special-props-and-tags#portal) for more information. ## `React.memo` -See [the guide on `Copy` elements](./special-props-and-tags#copy) for a demonstration of how you can use the `Copy` tag to implement `React.memo` in user space. +See [the guide on `Copy` tags](./special-props-and-tags#copy) for a demonstration of how you can use `Copy` elements to implement `React.memo` in user space. ## DOM element props -The following are a list of the differences in APIs when passings props to DOM elements. +The following are a list of the differences in props APIs for DOM elements. ### `className` and `htmlFor` -We prefer attribute names rather than the JS property equivalents when the two diverge. +Crank prefers attribute names rather than the JS property DOM equivalents when these names are mismatched. ```jsx ```` -See [the section on prop naming conventions](./special-props-and-tags#prop-naming-conventions) for more information. +In short, Crank is optimized for easy copy-pasting, which using props like `className` and `htmlFor` does not encourage. See [the section on prop naming conventions](./special-props-and-tags#prop-naming-conventions) for more information. ### `style` -The `style` prop value can be an object of CSS declarations. However, unlike React, CSS property names match the case of their CSS equivalents, and we do not add units to numbers. Additionally, Crank allows the style prop to be a CSS string as well. +The `style` prop can be an object of CSS declarations. However, unlike React, CSS property names match the case of their CSS equivalents, and we do not add units to numbers. Additionally, Crank allows the style prop to be a CSS string as well. ```jsx
Hello
``` +Refer to the guide on [the style prop](./special-props-and-tags#style) for more information. + ### Event props -Host elements can be listened to using `onevent` props, but the prop name is always lowercase. Crank also provides an `EventTarget` API for components to add and remove event listeners from the top-level node or nodes of each component. In both cases, Crank does not use a synthetic event system or polyfill events in any way. Refer to [the guide on event handling](./handling-events). +Host elements can be listened to using `onevent` props, but the prop name will be all lowercase. Crank also provides an `EventTarget` API for components to add and remove event listeners from the top-level node or nodes of each component. In both cases, Crank does not use a synthetic event system or polyfill events in any way. Refer to [the guide on event handling](./handling-events) for a longer explanation of event handling in Crank. ### Controlled and Uncontrolled Props -Crank does not have a concept of controlled or uncontrolled props, and does not provide `defaultValue`-style props for DOM elements. See [the section on form elements](./handling-events#form-elements) for a detailed description of how Crank handles form elements. +Crank does not have a concept of controlled or uncontrolled props, and does not provide `defaultValue`-style props for DOM elements. See [the section on form elements](./handling-events#form-elements) for a detailed description of how Crank handles stateful form elements. ### `dangerouslySetInnerHTML` -Host DOM elements accept an `innerHTML` prop; Crank does not provide the `dangerouslySetInnerHTML={{__html}}` API like React. Alternatively, you can use the special `Raw` tag to insert HTML strings or even DOM nodes directly into an element tree without a parent. [See the guide on the `Raw` element](./special-props-and-tag#raw) for more information. +Host DOM elements accept an `innerHTML` prop; Crank does not provide the `dangerouslySetInnerHTML={{__html}}` API like React. Alternatively, you can use the special `Raw` tag to insert HTML strings or even DOM nodes directly into an element tree without a parent. Refer to the sections [on the `innerHTML` prop](./special-props-and-tags#innerhtml) and [on the `Raw` tag](./special-props-and-tags#raw) for more information. ## Keys -Crank provides keyed rendering via the `crank-key` prop. The prop was renamed because “key” is a common word and because the prop is not erased from the props object passed into components. +Crank provides keyed rendering via the special `crank-key` prop. The prop was renamed because “key” is a common word and because the prop is not erased from the props object passed into components. Keys work similarly to the way they do in React. The main difference is that Crank does not warn about unkeyed elements which appear in arrays or iterables. ## Refs -Crank provides the callback-style ref API from React via the `crank-ref` prop. Unlike React, all elements can be read using the `crank-ref` prop, including Fragments, and . See the [guide on the `crank-ref` prop](./special-props-and-tags#crank-ref). +Crank provides the callback-style ref API from React via the `crank-ref` prop. Unlike React, all elements can be read using the `crank-ref` prop, including Fragment elements. See the [guide on the `crank-ref` prop](./special-props-and-tags#crank-ref). -You can also access values many other ways. See the [guide on accessing rendered values](./lifecycles#accessing-rendered-values) for more information. +You can also access rendered values in many other ways. Refer to [this section](./lifecycles#accessing-rendered-values) for more information. ## React Contexts Because we refer to the `this` keyword of components as “the component’s context” (“controller” would have been three more characters), we refer to the equivalent concept of [React’s Context API](https://reactjs.org/docs/context.html) as “provisions” instead. We use the context methods `provide` and `consume` to define provisions between ancestor and descendant components. See [the guide on provisions](./reusable-logic#provisions) for more information.