From c55e8f873ae674a00aa809458126e1644d61d2e0 Mon Sep 17 00:00:00 2001 From: Brian Kim Date: Mon, 13 Jul 2020 14:20:51 -0400 Subject: [PATCH] tweaks --- README.md | 2 +- website/build.tsx | 10 +- ...-props.md => 05-special-props-and-tags.md} | 0 website/guides/06-lifecycles.md | 4 - website/guides/07-reusable-logic.md | 4 +- website/guides/08-working-with-typescript.md | 1 - website/guides/10-differences-from-react.md | 83 --------- website/guides/10-mapping-react-to-crank.md | 157 ++++++++++++++++++ website/src/index.css | 5 +- 9 files changed, 168 insertions(+), 98 deletions(-) rename website/guides/{05-special-tags-and-props.md => 05-special-props-and-tags.md} (100%) delete mode 100644 website/guides/10-differences-from-react.md create mode 100644 website/guides/10-mapping-react-to-crank.md diff --git a/README.md b/README.md index 041718b7f..f91808bf1 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Crank uses the same JSX syntax and diffing algorithm popularized by React, allow ### Just Functions All components in Crank are just functions or generator functions. No classes, hooks, proxies or template languages are needed. -### Promise-fluent +### Native Promise Support Crank provides first-class support for promises. You can use async/await directly in components, and race async components to display fallback UIs. ### Lightweight diff --git a/website/build.tsx b/website/build.tsx index b2a01baaa..c5bef1acd 100644 --- a/website/build.tsx +++ b/website/build.tsx @@ -197,7 +197,7 @@ function Home(): Element {
-

Declarative components

+

Declarative Components

Crank uses the same JSX syntax and diffing algorithm popularized by React, allowing you to write HTML-like code directly in your @@ -212,11 +212,11 @@ function Home(): Element {

-

Promises today

+

Native Promise Support

- Crank provides first-class support for promises. You can use - async/await directly in components, and race components to display - fallback UIs. + Crank provides first-class support for promises. You can use async + functions as components, and race components to display fallback + UIs.

diff --git a/website/guides/05-special-tags-and-props.md b/website/guides/05-special-props-and-tags.md similarity index 100% rename from website/guides/05-special-tags-and-props.md rename to website/guides/05-special-props-and-tags.md diff --git a/website/guides/06-lifecycles.md b/website/guides/06-lifecycles.md index 975912b8a..7fab4d083 100644 --- a/website/guides/06-lifecycles.md +++ b/website/guides/06-lifecycles.md @@ -4,10 +4,6 @@ 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 generator object as components are inserted, updated and removed 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. -## A review of generator functions - -TODO - ## Returning from a generator 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. diff --git a/website/guides/07-reusable-logic.md b/website/guides/07-reusable-logic.md index a64a42b7c..c8323cb19 100644 --- a/website/guides/07-reusable-logic.md +++ b/website/guides/07-reusable-logic.md @@ -250,7 +250,7 @@ async function *Counter() { - Uniform logic to dispose of resources. **Cons:** -- Promises and async iterators can be prone to race conditions and deadlocks. +- Promises and async iterators can cause race conditions and deadlocks, without any language-level features to help you debug them. **Use cases:** -If you use async iterators/generators already, Crank is the perfect fit for your application. +If you use async iterators/generators already, Crank is the perfect framework for your application. diff --git a/website/guides/08-working-with-typescript.md b/website/guides/08-working-with-typescript.md index 77dd43e6f..7cb340271 100644 --- a/website/guides/08-working-with-typescript.md +++ b/website/guides/08-working-with-typescript.md @@ -124,4 +124,3 @@ function MyButton (props) { ## Typing provisions By default, calls to `Context.prototype.get` and `Context.prototype.set` will be loosely typed. If you want stricter typings of these methods, you can use module augmentation to extend the `ProvisionMap` interface provided by Crank. - diff --git a/website/guides/10-differences-from-react.md b/website/guides/10-differences-from-react.md deleted file mode 100644 index f454b266c..000000000 --- a/website/guides/10-differences-from-react.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Differences from React ---- - -Though Crank is very much inspired by and similar to React, exact compatibility is a non-goal, and we’ve used this as opportunity to “fix” a lot of pain points with React which bothered us over the years. The following is a list of differences with React, as well as justifications for why we chose to implement features differently. - -## No classes -Crank uses functions, generator functions and async functions to implement all of what React implements with classes. Here for instance, is the entirety of the React class-based API implemented with a single async generator function: - -```jsx -async function *ReactComponent(props) { - let state = componentWillMount(props); - let ref = yield render(props, state); - state = componentDidMount(props, state, ref); - try { - for await (const newProps of this) { - if (shouldComponentUpdate(props, newProps, state, ref)) { - state = componentWillUpdate(props, newProps, state, ref); - ref = yield render(props, state); - state = componentDidUpdate(props, newProps, state, ref); - props = newProps; - } - } - } catch (err) { - componentDidCatch(err); - } finally { - componentWillUnmount(ref); - } -} -``` - -## No hooks -Crank does not implement hooks. Hooks bad. - -## No `setState` or `useState` -React has always tightly coupled component updates with local state. Because Crank uses generator functions, state is just local variables, and you can call `this.refresh()` to update the UI to match state. Decoupling these two concerns allows for more nuanced updates to local state without `shouldComponentUpdate` hacks, and is easier to reason about than relying on the framework to provide local state. - -## No `Suspense` -The project known as React `Suspense` is both sub-optimal and likely to be vaporware. It relies on the unusual mechanism of throwing promises, has the hard requirement of a caching mechanism, and is generally difficult to reason about. By leveraging async functions and async generators, Crank allows you to implement the `Suspense` element in user space. No argument from the React team about the necessity of `Suspense` will ever justify it over the convenience provided by being able to use the `await` operator directly in components. - -## Props match HTML attributes rather than JS APIs -### `for` not `htmlFor`, `class` not `className` - -Crank does not place any restrictions on the names of JSX props. This means that you can write JSX like ``. -## style can be a `cssText` string, style object uses snake-case, and `px` is not magically added to numbers. -```jsx -
Hello
-``` -## No “controlled”/“uncontrolled”, “value”/“defaultValue” components. -If you don’t want your component to be updated, don’t update it. -## No `dangerouslySetInnerHTML={{__html}}` props. -Just use the `innerHTML` prop. React doesn’t do anything to make setting `innerHTML` safe; they just make you type more and search for the exact spelling of `dangerouslySetInnerHTML` every month or so. -## Fragments can have `innerHTML`. -TKTKTK update for Raw -Fragment behavior can be overridden by renderers, and both the DOM and HTML renderers allow fragments to accept an innerHTML prop, allowing arbitrarily HTML to be written without a parent. -## Portals are just a special element tag. -Their behavior is defined by renderers, and all element trees are wrapped implicitly or explicitly in a root portal. -### No `componentDidUpdate` or `React.memo`. -Crank uses the special `Copy` element to prevent child updates. -## No `React.cloneElement` -Elements are just plain-old JavaScript objects, and there is no need to use a special method to copy them. You can re-use the same elements within generators, mutate them, or use spread syntax to shallowly copy them. Crank will handle reused elements gracefully. -## No event props -Event props in React are terrible for the following reasons: - The EventTarget API takes more than just a function, it also takes options which allow you to register event listeners in the `capture` phase, register `passive` listeners, or register event listeners `once`. React has attempted to allow event handlers to be registered in the capture phase by adding props like `onClickCapture`, but embedding all these options in the prop name would be madness (`onClickCaptureOncePassive`). By emulating the event target API, Crank provides the full power of the `EventTarget` API. -## Stop doxxing event listeners. -Event listeners are difficult to name and make the most sense as anonymous functions which are made meaningful by the `type` it’s associated with. React developers often adopt a naming scheme to cache event listeners on component instances like `this.handleClick` to avoid `PureComponent`/`componentDidUpdate` de-optimizations, but if they only had to be defined once, we wouldn’t have to do this. Generator functions naturally allow components to define anonymous event listeners once when the component mounts, and the event target API provided by Crank automatically unregisters these listeners when components are thrown away. This means you never have to reason about when these functions are redefined or what they are capturing in their closures. -## Custom events are events, and they can be prevented or bubbled like regular DOM events. -When attempting to define custom event handler props in React, React developers will typically mimic the component props API and allow callbacks to be passed into the component, which the component author will then call directly when they want to trigger the event. This is a painful to do, because you often have to make sure the callback is defined on props, because they are usually optional, and then React developers will also arbitrarily pass data to the callback which is not an event, making custom `onWhatever` props disanalogous with DOM event props, because DOM event props call callbacks with an event. There is no standard for what event-like callback props are called with in React, and there is no way for components to allow parents to prevent default behavior by calling `ev.preventDefault` as you would with a regular DOM event. Worst of all, these props must be passed directly from parent to child, so if a component wants to listen to an event in a deeply nested component, event handlers must either be passed using React contexts, or passed explicitly through each component layer, at each layer renamed to make sense for each nested component API. - -Crank avoids this sitution by mimicking the DOM EventTarget API, and allowing developers to create and bubble real `Event` or `CustomEvent` objects with `this.dispatchEvent`. These events can be namespaced, typed, and components can allow parents to cancel events. -## No refs - React’s `ref` API has undergone multiple revisions over the years and it’s only gotten more confusing/difficult to use. Crank passes rendered DOM nodes and strings back into generator functions, so you can access them by reading the result of `yield` expressions in generators. You can assign these “refs” to local variables and treat them as you would any local variable without worry. -## Children can contain any kind of iterable, not just arrays. - There’s no reason to restrict children in JSX elements to just arrays. You can interpolate ES6 Maps, Sets or any other user-defined iterable into your Crank elements, and Crank will simply render them in an implicit `Fragment`. -## Keys -### key has been named to `crank-key`. -In React, the `key` prop is special and erased from the props visible to components. Insofar as `key` is a common word, Crank namespaces `key` as `crank-key`. -### The elements of iterables don’t require unique keys. -Pestering the user to add unique keys to every element of an array is not something Crank will ever do, insofar as most of the time, developers just set the `key` to an `index` into the array. If the developer needs state to be preserved, they will eventually discover that it isn’t preserved in the course of normal development and add a key. -## No render props -Crank strongly discourages the React idiom of passing a function as children (or any other prop for that matter. “Render props” produce ugly and non-sensical syntax, and were a direct response to the lack of composability of React class components. Most if not all “render prop” patterns are easier to implement in Crank with the use of higher-order components which are passed component functions and props and produce elements of their own accord. -## Contexts -A feature equivalent to React Contexts is planned but not yet implemented. diff --git a/website/guides/10-mapping-react-to-crank.md b/website/guides/10-mapping-react-to-crank.md new file mode 100644 index 000000000..a3c81a241 --- /dev/null +++ b/website/guides/10-mapping-react-to-crank.md @@ -0,0 +1,157 @@ +--- +title: Mapping React to Crank +unpublished: true +--- + +Though Crank is inspired by React, compatibility is a non-goal, and certain concepts may be implemented using different, non-compatible APIs. The following is a reference for React developers to help them map React concepts APIs to their equivalents in Crank. + +## Class Components +Crank uses functions exclusively for components; it does not provide a class-based component API. You can emulate most of React’s Component class API with the natural lifecycle of an async generator function: + +```jsx +async function *ReactComponent(props) { + let state = componentWillMount(props); + let ref = yield render(props, state); + state = componentDidMount(props, state, ref); + try { + for await (const newProps of this) { + if (shouldComponentUpdate(props, newProps, state, ref)) { + state = componentWillUpdate(props, newProps, state, ref); + ref = yield render(props, state); + state = componentDidUpdate(props, newProps, state, ref); + props = newProps; + } else { + yield ; + } + } + } catch (err) { + componentDidCatch(err); + } finally { + componentWillUnmount(ref); + } +} +``` + +This is pseudocode which demonstrates where the methods of React would be called relative to an async generator component. Refer to the [guide on lifecycles](./lifecycles) for more information on using generator functions. + +### `setState` +Crank uses generator functions and local variables for local state. Refer to [the section on stateful components](#TTKTKTKTKTKTKTK). + +### `forceUpdate` +Crank is not “reactive” in the same sense as React, in that it does not actually know when your component’s local state is updated. You can either use `Context.prototype.refresh` to manually refresh the component, much like `forceUpdate`, or you can use async generator components, which refresh automatically whenever the async generator object fulfills. + +### `defaultProps` +Crank doesn’t have a `defaultProps` implementation. Instead, you can provide default values when destructuring props. + +### `shouldComponentUpdate` +Components themselves do not provide a way to prevent updates to themselves. Instead, you can use `Copy` elements to prevent the rerendering of a specific subtree. [Refer to the description of `Copy` elements](#TTKTKTK) for more information. + +### `componentWillMount` and `componentDidMount` +Setup code can simply be written at the top of a generator component. + +### getDerivedStateFromProps`, `componentWillUpdate` and `getSnapshotBeforeUpdate` +Code which compares old and new props or state and performs side-effects can be written directly in your components. See the section on [`prop updates`](#TK) for examples of comparing old and new props. Additionally, check out the [`Context.prototoype.schedule`](#TKTKTK), which takes a callback which is called whenever the component commits. + +### `componentWillUnmount` +You can use `try`/`finally` to run code when the component is unmounted. You can also use the [`Context.prototype.cleanup`] method if you’re writing extensions which don’t run in the main execution of the component. + +### `componentDidUpdate` +Crank uses the special `Copy` element to prevent child updates. See the guide on the `Copy` element to see how you might reimplement `React.memo` directly in user space. + +### Error Boundaries/`componentDidCatch` +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 section in guides](#TK). + +## Hooks +Crank does not implement any APIs similar to React Hooks. 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](#TTKTKTKTKTKTKTK). + +### `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. + +### `useMemo`/`useCallback` +TTKTK + +### `useImperativeHandle` +Crank does not yet have a way to access component instances, and parent components should not access child contexts directly. An imperative wrapper which uses web components is planned. + +### Custom Hooks +The main appeal of hooks for library authors is that you can encapsulate shared logic in hooks. Refer to the [guide on reusable logic](./reusable-logic) for some patterns for reusing logic between components. + +## Suspense and Concurrent Mode +Crank does not implement any sort of Suspense-like API. As an alternative, you can use async functions and async generator functions directly. 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. + +## `PropTypes` +Crank is written in TypeScript, and you can add type checking to component elements by typing the props parameter of the component function. + +## Fragments and array children + There’s no reason to restrict children in JSX elements to just arrays. You can interpolate ES6 Maps, Sets or any other user-defined iterable into your Crank elements, and Crank will simply render them in an implicit `Fragment`. + +## `React.cloneElement` +You can clone elements using the `cloneElement` function. + +## `ReactDOM.createPortal` +The `createPortal` function is replaced by a special `Portal` element, whose behavior and expected props varies according to the target rendering environment. Refer to [the guide on the `Portal` element](#KTKTKTKT) for more information. + +## `React.memo` +You can use `Copy` elements to implement `React.memo` in user space: +```jsx +function equals(props, newProps) { + for (const name in {...props, ...newProps}) { + if (props[name] !== newProps[name]) { + return false; + } + } + + return true; +} + +function memo(Component) { + return function *Wrapped({props}) { + yield ; + for (const newProps of this) { + if (equals(props, newProps)) { + yield ; + } else { + yield ; + } + + props = newProps; + } + }; +} +``` + +See [the guide on component elements](#TKTKTKTK) for more information. + +## DOM props +### `htmlFor` and `className` +Crank does not place any restrictions on the names of props. This means that you can write JSX like ``. + +### The `style` prop. +```jsx +
Hello
+``` + +The `style` prop value can be a `cssText` string, or an object, similar to React. Unlike React, the CSS property names match the case of their CSS equivalents, and we do not add units to numbers. + +### `onevent` props +Crank provides `onevent` props, but they work using the DOM `onevent` props. Crank also provides an `EventTarget` API for components which adds and removes event listeners from the top-level nodes of each component. In both cases, Crank does not use a synthetic event API or shim events. See [the guide on events for more information](#TKTKTKTKTK). + +### Controlled and Uncontrolled Props +Crank does not have a concept of controlled or uncontrolled props, and does not have `defaultValue` props. + +### `dangerouslySetInnerHTML` +Crank elements accept an `innerHTML` prop. Alternatively, you can use the special `Raw` tag to insert arbitrary HTML strings or even nodes in the tree. [See the guide on the `Raw` element](#TKTK) 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 passed directly into child objects. + +Keys work similarly to the way they do in React. The main difference is that Crank does not require elements which appear in arrays or iterables to be keyed. + +## Refs +Crank provides the callback ref API from React via the `crank-ref` prop. Crank does not TKTKTKT + +## React Contexts +TKTKTKTKT diff --git a/website/src/index.css b/website/src/index.css index bd404827b..9aafe0598 100644 --- a/website/src/index.css +++ b/website/src/index.css @@ -247,19 +247,20 @@ li { pre { padding: .8em; - background: #e1e4ea; margin: 1.5em 0; font-size: .9rem; + background: #e1e4ea; color: #0b2f5b; overflow: auto; tab-size: 2; } :not(pre) > code { - background: #2b303c; padding: 0 .1em; margin: 0; display: inline; + background: #e1e4ea; + color: #0b2f5b; } .token.comment,