Skip to content

Commit

Permalink
Merge branch 'main' into refactor-tiny-react-reverse-insert-order
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa committed Oct 19, 2023
2 parents 15d616e + bf01cb9 commit b720834
Show file tree
Hide file tree
Showing 19 changed files with 362 additions and 347 deletions.
5 changes: 5 additions & 0 deletions .changeset/polite-months-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hiogawa/tiny-react": patch
---

fix: fix `memo`
5 changes: 5 additions & 0 deletions .changeset/weak-carpets-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hiogawa/tiny-react": minor
---

feat: optimize jsx runtime
2 changes: 1 addition & 1 deletion packages/tiny-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hiogawa/tiny-react",
"version": "0.0.1",
"version": "0.0.2-pre.3",
"homepage": "https://github.com/hi-ogawa/js-utils/tree/main/packages/tiny-react",
"repository": {
"type": "git",
Expand Down
56 changes: 42 additions & 14 deletions packages/tiny-react/src/compat/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { once } from "@hiogawa/utils";
import { useEffect, useRef, useState } from "../hooks";
import { render } from "../reconciler";
import { type BNode, EMPTY_NODE, type FC, type VNode } from "../virtual-dom";
import {
type BNode,
EMPTY_NODE,
type FC,
NODE_TYPE_CUSTOM,
type VCustom,
type VNode,
} from "../virtual-dom";

// non comprehensive compatibility features

Expand Down Expand Up @@ -39,23 +47,43 @@ export function createRoot(container: Element) {
}

// https://react.dev/reference/react/memo
export function memo<P extends object>(
fc: FC<P>,
propsAreEqual: (
prevProps: Readonly<P>,
nextProps: Readonly<P>
) => boolean = objectShallowEqual
export function memo<P extends {}>(
Fc: FC<P>,
isEqualProps: (prev: {}, next: {}) => boolean = objectShallowEqual
): FC<P> {
function Memo(props: P) {
const prev = useRef<{ props: Readonly<P>; result: VNode } | undefined>(
undefined
);
if (!prev.current || !propsAreEqual(prev.current.props, props)) {
prev.current = { props, result: fc(props) };
// we need to make `VCustom.render` stable,
// but `once(Fc)` has to be invalidated on props change.
// after "identical vnode" optimization is implemented,
// it can be simplified to
// {
// type: NODE_TYPE_CUSTOM,
// render: Fc,
// props,
// }
const [state] = useState(() => {
const state: {
render: FC;
current?: { vnode: VCustom; onceFc: FC };
} = {
render: (props: any) => state.current!.onceFc(props),
};
return state;
});

if (!state.current || !isEqualProps(state.current.vnode.props, props)) {
state.current = {
vnode: {
type: NODE_TYPE_CUSTOM,
render: state.render,
props,
},
onceFc: once(Fc),
};
}
return prev.current.result;
return state.current.vnode;
}
Object.defineProperty(Memo, "name", { value: `Memo(${fc.name})` });
Object.defineProperty(Memo, "name", { value: `Memo(${Fc.name})` });
return Memo;
}

Expand Down
13 changes: 0 additions & 13 deletions packages/tiny-react/src/helper/common.ts

This file was deleted.

69 changes: 20 additions & 49 deletions packages/tiny-react/src/helper/hyperscript.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { Fragment, h } from "./hyperscript";
import { Fragment } from "../virtual-dom";
import { h } from "./hyperscript";

describe("hyperscript", () => {
it("basic", () => {
Expand All @@ -21,92 +22,62 @@ describe("hyperscript", () => {
);
expect(vnode).toMatchInlineSnapshot(`
{
"child": {
"key": undefined,
"name": "div",
"props": {
"children": [
{
"key": "abc",
"props": {
"children": {
"type": "empty",
},
"children": undefined,
"value": "hello",
},
"render": [Function],
"type": "custom",
},
{
"type": "empty",
},
{
"data": "0",
"type": "text",
},
null,
0,
{
"key": undefined,
"props": {
"children": {
"type": "empty",
},
"children": undefined,
},
"render": [Function],
"type": "custom",
},
undefined,
{
"type": "empty",
},
{
"child": {
"data": "world",
"type": "text",
},
"key": 0,
"name": "span",
"props": {
"children": "world",
"className": "text-red",
"ref": [Function],
},
"ref": [Function],
"type": "tag",
},
{
"child": {
"data": "0",
"type": "text",
},
"key": undefined,
"name": "span",
"props": {},
"ref": undefined,
"props": {
"children": 0,
},
"type": "tag",
},
{
"child": {
"key": undefined,
"name": "span",
"props": {
"children": [
{
"data": "0",
"type": "text",
},
{
"data": "1",
"type": "text",
},
0,
1,
],
"type": "fragment",
},
"key": undefined,
"name": "span",
"props": {},
"ref": undefined,
"type": "tag",
},
],
"type": "fragment",
},
"key": undefined,
"name": "div",
"props": {
"className": "flex",
},
"ref": undefined,
"type": "tag",
}
`);
Expand Down
91 changes: 3 additions & 88 deletions packages/tiny-react/src/helper/hyperscript.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,10 @@
import {
EMPTY_NODE,
NODE_TYPE_CUSTOM,
NODE_TYPE_FRAGMENT,
NODE_TYPE_TAG,
NODE_TYPE_TEXT,
type NodeKey,
type Props,
type ComponentChildren,
type VNode,
createElement,
} from "../virtual-dom";
import type {
ComponentChild,
ComponentChildren,
ComponentType,
} from "./common";
import { type JSX } from "./jsx-namespace";

export function createElement(
tag: ComponentType,
props: Props,
...children: ComponentChildren[]
): VNode {
const { key, ...propsNoKey } = props as { key?: NodeKey };

// unwrap single child to skip trivial fragment.
// this should be "safe" by the assumption that
// example such as:
// createElement("div", {}, ...["some-varing", "id-list"].map(key => h("input", { key })))
// should be written without spreading
// createElement("div", {}, ["some-varing", "id-list"].map(key => h("input", { key })))
// this should be guaranteed when `h` is used via jsx-runtime-based transpilation.
const child = normalizeComponentChildren(
children.length <= 1 ? children[0] : children
);

if (typeof tag === "string") {
const { ref, ...propsNoKeyNoRef } = propsNoKey as { ref?: any };
return {
type: NODE_TYPE_TAG,
name: tag,
key,
ref,
props: propsNoKeyNoRef,
child,
};
} else if (typeof tag === "function") {
return {
type: NODE_TYPE_CUSTOM,
key,
props: {
...propsNoKey,
children: child,
},
render: tag,
};
}
return tag satisfies never;
}

// we can probably optimize Fragment creation directly as { type: "fragment" }
// but for now we wrap as { type: "custom" }, which also helps testing the robustness of architecture
export function Fragment(props: { children?: ComponentChildren }): VNode {
return normalizeComponentChildren(props.children);
}

function normalizeComponentChildren(children?: ComponentChildren): VNode {
if (Array.isArray(children)) {
return {
type: NODE_TYPE_FRAGMENT,
children: children.map((c) => normalizeComponentChildren(c)),
};
}
return normalizeComponentChild(children);
}

function normalizeComponentChild(child: ComponentChild): VNode {
// TODO: instantiating new object for child/children would break shallow equal used for `memo(Component)`
if (
child === null ||
typeof child === "undefined" ||
typeof child === "boolean"
) {
return EMPTY_NODE;
}
if (typeof child === "string" || typeof child === "number") {
return {
type: NODE_TYPE_TEXT,
data: String(child),
};
}
return child;
}

//
// type-safe createElement wrapper
//
Expand Down Expand Up @@ -120,5 +34,6 @@ type HyperscriptIntrinsic = {
type HyperscriptCustom = <P>(
tag: (props: P) => VNode,
props: P & JSX.IntrinsicAttributes,
// TODO: infer `children` types from `P`?
...children: ComponentChildren[]
) => VNode;
8 changes: 6 additions & 2 deletions packages/tiny-react/src/helper/jsx-namespace.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { NodeKey, VNode } from "../virtual-dom";
import type { ComponentChildren, ComponentType } from "./common";
import type {
ComponentChildren,
ComponentType,
NodeKey,
VNode,
} from "../virtual-dom";

// JSX namespace convention for type-checker

Expand Down
25 changes: 8 additions & 17 deletions packages/tiny-react/src/helper/jsx-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import type { NodeKey, VNode } from "../virtual-dom";
import type { ComponentChildren, ComponentType } from "./common";
import { Fragment, createElement } from "./hyperscript";
import { Fragment, createVNode } from "../virtual-dom";
import type { JSX } from "./jsx-namespace";

// jsx-runtime convention for transpilers
Expand All @@ -9,17 +7,10 @@ import type { JSX } from "./jsx-namespace";
// https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md#detailed-design
// https://github.com/preactjs/preact/blob/08b07ccea62bfdb44b983bfe69ae73eb5e4f43c7/jsx-runtime/src/index.js

export function jsx(
tag: ComponentType,
props: { children?: ComponentChildren },
key?: NodeKey
): VNode {
// TODO
// as the main motivation of jsx-runtime, we can similary delay props normalization until render time.
// we can simply keep "raw" props (except key) on VTag and VCustom
// then ref/children normalization is done when reconciled to BTag and BCustom.
const { children, ...propsNoChildren } = props;
return createElement(tag, { key, ...propsNoChildren }, props.children);
}

export { jsx as jsxs, jsx as jsxDEV, Fragment, type JSX };
export {
createVNode as jsx,
createVNode as jsxs,
createVNode as jsxDEV,
Fragment,
type JSX,
};
3 changes: 2 additions & 1 deletion packages/tiny-react/src/hmr/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {
createHmrRegistry,
setupHmrVite,
} from ".";
import { createElement, h } from "../helper/hyperscript";
import { h } from "../helper/hyperscript";
import { useEffect, useState } from "../hooks";
import { render } from "../reconciler";
import { sleepFrame } from "../test-utils";
import { createElement } from "../virtual-dom";

describe(setupHmrVite, () => {
it("basic", async () => {
Expand Down
Loading

0 comments on commit b720834

Please sign in to comment.