Skip to content

Commit

Permalink
feat(jsx/dom): more react staff (#2132)
Browse files Browse the repository at this point in the history
* fix(jsx): fix context provider with async component (#2124)

* test(jsx): add test for Context with Suspense

* fix(jsx): fix context provider with async component

* chore: denoify

* feat(jsx): Introduce useMemo and useLayoutEffect hooks

* feat(jsx): Introduce isValidElement and cloneElement.

* fix(jsx/dom): fix context handling in renderToDom

* feat(jsx): enable to use 'hono/jsx/dom' for replacement of 'react'.

You can use dom-specific version of hooks and utils by importing from 'hono/jsx/dom'.

* refactor(jsx): refactor memo()

* test(jsx/dom): migrate existing tests to use `requestAnimationFrame`

* test(jsx): add tests

* refactor(jsx): move constant to its own file

* chore: denoify
  • Loading branch information
usualoma committed Feb 3, 2024
1 parent ebbd444 commit 6adc806
Show file tree
Hide file tree
Showing 28 changed files with 860 additions and 152 deletions.
4 changes: 2 additions & 2 deletions deno_dist/helper/css/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { raw } from '../../helper/html/index.ts'
import { DOM_RENDERER } from '../../jsx/constants.ts'
import { createCssJsxDomObjects } from '../../jsx/dom/css.ts'
import { RENDER_TO_DOM } from '../../jsx/dom/render.ts'
import type { HtmlEscapedCallback, HtmlEscapedString } from '../../utils/html.ts'
import type { CssClassName as CssClassNameCommon, CssVariableType } from './common.ts'
import {
Expand Down Expand Up @@ -138,7 +138,7 @@ export const createCssContext = ({ id }: { id: Readonly<string> }) => {
? raw(`<style id="${id}">${(children as unknown as CssClassName)[STYLE_STRING]}</style>`)
: raw(`<style id="${id}"></style>`)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(Style as any)[RENDER_TO_DOM] = StyleRenderToDom
;(Style as any)[DOM_RENDERER] = StyleRenderToDom

return {
css,
Expand Down
4 changes: 2 additions & 2 deletions deno_dist/jsx/components.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { raw } from '../helper/html/index.ts'
import type { HtmlEscapedString, HtmlEscapedCallback } from '../utils/html.ts'
import { HtmlEscapedCallbackPhase, resolveCallback } from '../utils/html.ts'
import { DOM_RENDERER } from './constants.ts'
import { ErrorBoundary as ErrorBoundaryDomRenderer } from './dom/components.ts'
import type { HasRenderToDom } from './dom/render.ts'
import { RENDER_TO_DOM } from './dom/render.ts'
import type { FC, Child } from './index.ts'

let errorBoundaryCounter = 0
Expand Down Expand Up @@ -180,4 +180,4 @@ d.remove()
return raw(resArray.join(''))
}
}
;(ErrorBoundary as HasRenderToDom)[RENDER_TO_DOM] = ErrorBoundaryDomRenderer
;(ErrorBoundary as HasRenderToDom)[DOM_RENDERER] = ErrorBoundaryDomRenderer
3 changes: 3 additions & 0 deletions deno_dist/jsx/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const DOM_RENDERER = Symbol('RENDERER')
export const DOM_ERROR_HANDLER = Symbol('ERROR_HANDLER')
export const DOM_STASH = Symbol('STASH')
37 changes: 21 additions & 16 deletions deno_dist/jsx/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { raw } from '../helper/html/index.ts'
import type { HtmlEscapedString } from '../utils/html.ts'
import { DOM_RENDERER } from './constants.ts'
import { createContextProviderFunction } from './dom/context.ts'
import { RENDER_TO_DOM } from './dom/render.ts'
import { JSXFragmentNode } from './index.ts'
import type { FC } from './index.ts'

Expand All @@ -10,35 +10,40 @@ export interface Context<T> {
Provider: FC<{ value: T }>
}

export const globalContexts: Context<unknown>[] = []

export const createContext = <T>(defaultValue: T): Context<T> => {
const values = [defaultValue]
const context: Context<T> = {
values,
Provider(props): HtmlEscapedString | Promise<HtmlEscapedString> {
values.push(props.value)
const string = props.children
? (Array.isArray(props.children)
? new JSXFragmentNode('', {}, props.children)
: props.children
).toString()
: ''
values.pop()
let string
try {
string = props.children
? (Array.isArray(props.children)
? new JSXFragmentNode('', {}, props.children)
: props.children
).toString()
: ''
} finally {
values.pop()
}

if (string instanceof Promise) {
return Promise.resolve().then<HtmlEscapedString>(async () => {
values.push(props.value)
const awaited = await string
const promiseRes = raw(awaited, (awaited as HtmlEscapedString).callbacks)
values.pop()
return promiseRes
})
return string.then((resString) =>
raw(resString, (resString as HtmlEscapedString).callbacks)
)
} else {
return raw(string)
}
},
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(context.Provider as any)[RENDER_TO_DOM] = createContextProviderFunction(values)
;(context.Provider as any)[DOM_RENDERER] = createContextProviderFunction(values)

globalContexts.push(context as Context<unknown>)

return context
}

Expand Down
6 changes: 3 additions & 3 deletions deno_dist/jsx/dom/components.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { FC, Child } from '../index.ts'
import type { FallbackRender, ErrorHandler } from '../components.ts'
import { DOM_ERROR_HANDLER } from '../constants.ts'
import { Fragment } from './jsx-runtime.ts'
import { ERROR_HANDLER } from './render.ts'

/* eslint-disable @typescript-eslint/no-explicit-any */
export const ErrorBoundary: FC<{
Expand All @@ -10,7 +10,7 @@ export const ErrorBoundary: FC<{
onError?: ErrorHandler
}> = (({ children, fallback, fallbackRender, onError }: any) => {
const res = Fragment({ children })
;(res as any)[ERROR_HANDLER] = (err: any) => {
;(res as any)[DOM_ERROR_HANDLER] = (err: any) => {
if (err instanceof Promise) {
throw err
}
Expand All @@ -22,7 +22,7 @@ export const ErrorBoundary: FC<{

export const Suspense: FC<{ fallback: any }> = (({ children, fallback }: any) => {
const res = Fragment({ children })
;(res as any)[ERROR_HANDLER] = (err: any, retry: () => void) => {
;(res as any)[DOM_ERROR_HANDLER] = (err: any, retry: () => void) => {
if (!(err instanceof Promise)) {
throw err
}
Expand Down
9 changes: 6 additions & 3 deletions deno_dist/jsx/dom/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Child } from '../index.ts'
import { DOM_ERROR_HANDLER } from '../constants.ts'
import type { Context } from '../context.ts'
import { globalContexts } from '../context.ts'
import { Fragment } from './jsx-runtime.ts'
import { ERROR_HANDLER } from './render.ts'

export const createContextProviderFunction =
<T>(values: T[]) =>
Expand All @@ -22,7 +23,7 @@ export const createContextProviderFunction =
],
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(res as any)[ERROR_HANDLER] = (err: unknown) => {
;(res as any)[DOM_ERROR_HANDLER] = (err: unknown) => {
values.pop()
throw err
}
Expand All @@ -31,9 +32,11 @@ export const createContextProviderFunction =

export const createContext = <T>(defaultValue: T): Context<T> => {
const values = [defaultValue]
return {
const context = {
values,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Provider: createContextProviderFunction(values) as any,
}
globalContexts.push(context)
return context
}
33 changes: 33 additions & 0 deletions deno_dist/jsx/dom/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
export {
useState,
useEffect,
useRef,
useCallback,
use,
startTransition,
useTransition,
useDeferredValue,
startViewTransition,
useViewTransition,
useMemo,
useLayoutEffect,
} from '../hooks/index.ts'
export { render } from './render.ts'
export { Suspense, ErrorBoundary } from './components.ts'
export { useContext } from '../context.ts'
export type { Context } from '../context.ts'
export { createContext } from './context.ts'
export { memo, isValidElement } from '../index.ts'

import type { Props, Child, JSXNode } from '../index.ts'
import { jsx } from './jsx-runtime.ts'
export const cloneElement = <T extends JSXNode | JSX.Element>(
element: T,
props: Props,
...children: Child[]
): T => {
return jsx(
(element as JSXNode).tag,
{
...(element as JSXNode).props,
...props,
children: children.length ? children : (element as JSXNode).children,
},
(element as JSXNode).key
) as T
}
51 changes: 36 additions & 15 deletions deno_dist/jsx/dom/render.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import type { FC, Child, Props } from '../index.ts'
import type { JSXNode } from '../index.ts'
import { DOM_RENDERER, DOM_ERROR_HANDLER, DOM_STASH } from '../constants.ts'
import type { Context as JSXContext } from '../context.ts'
import { globalContexts as globalJSXContexts } from '../context.ts'
import type { EffectData } from '../hooks/index.ts'
import { STASH_EFFECT } from '../hooks/index.ts'

const eventAliasMap: Record<string, string> = {
change: 'input',
}

export const RENDER_TO_DOM = Symbol()
export const ERROR_HANDLER = Symbol()
export const STASH = Symbol()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type HasRenderToDom = FC<any> & { [RENDER_TO_DOM]: FC<any> }
export type HasRenderToDom = FC<any> & { [DOM_RENDERER]: FC<any> }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ErrorHandler = (error: any, retry: () => void) => Child | undefined

type Container = HTMLElement | DocumentFragment
type LocalJSXContexts = [JSXContext<unknown>, unknown][] | undefined

export type NodeObject = {
pP: Props | undefined // previous props
Expand All @@ -25,11 +26,18 @@ export type NodeObject = {
s?: Node[] // shadow virtual dom children
c: Container | undefined // container
e: HTMLElement | Text | undefined // rendered element
[STASH]: [
number, // current hook index
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any[][] // stash for hooks
]
[DOM_STASH]:
| [
number, // current hook index
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any[][], // stash for hooks
LocalJSXContexts // context
]
| [
number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any[][]
]
} & JSXNode
type NodeString = [string] & {
e?: Text
Expand Down Expand Up @@ -133,9 +141,9 @@ const invokeTag = (context: Context, node: NodeObject): Child[] => {
return res
}

node[STASH][0] = 0
node[DOM_STASH][0] = 0
buildDataStack.push([context, node])
const func = (node.tag as HasRenderToDom)[RENDER_TO_DOM] || node.tag
const func = (node.tag as HasRenderToDom)[DOM_RENDERER] || node.tag
try {
return [
func.call(null, {
Expand All @@ -157,7 +165,7 @@ const getNextChildren = (
) => {
childrenToRemove.push(...node.vR)
if (typeof node.tag === 'function') {
node[STASH][1][STASH_EFFECT]?.forEach((data: EffectData) => callbacks.push(data))
node[DOM_STASH][1][STASH_EFFECT]?.forEach((data: EffectData) => callbacks.push(data))
}
node.vC.forEach((child) => {
if (isNodeString(child)) {
Expand Down Expand Up @@ -195,7 +203,7 @@ const findInsertBefore = (node: Node | undefined): ChildNode | null => {

const removeNode = (node: Node) => {
if (!isNodeString(node)) {
node[STASH]?.[1][STASH_EFFECT]?.forEach((data: EffectData) => data[2]?.())
node[DOM_STASH]?.[1][STASH_EFFECT]?.forEach((data: EffectData) => data[2]?.())
node.vC?.forEach(removeNode)
}
node.e?.remove()
Expand Down Expand Up @@ -255,6 +263,9 @@ const applyNodeObject = (node: NodeObject, container: Container) => {
}
remove.forEach(removeNode)
callbacks.forEach(([, cb]) => cb?.())
requestAnimationFrame(() => {
callbacks.forEach(([, , , cb]) => cb?.())
})
}

export const build = (
Expand All @@ -271,7 +282,7 @@ export const build = (
children ||= typeof node.tag == 'function' ? invokeTag(context, node) : node.children
if ((children[0] as JSXNode)?.tag === '') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
errorHandler = (children[0] as any)[ERROR_HANDLER] as ErrorHandler
errorHandler = (children[0] as any)[DOM_ERROR_HANDLER] as ErrorHandler
topLevelErrorHandlerNode ||= node
}
const oldVChildren: Node[] = node.vC ? [...node.vC] : []
Expand All @@ -287,6 +298,10 @@ export const build = (
}
prevNode = child

if (typeof child.tag === 'function' && globalJSXContexts.length > 0) {
child[DOM_STASH][2] = globalJSXContexts.map((c) => [c, c.values.at(-1)])
}

let oldChild: Node | undefined
const i = oldVChildren.findIndex((c) => c.key === (child as Node).key)
if (i !== -1) {
Expand Down Expand Up @@ -346,7 +361,7 @@ const buildNode = (node: Child): Node | undefined => {
return [node.toString()] as NodeString
} else {
if (typeof (node as JSXNode).tag === 'function') {
;(node as NodeObject)[STASH] = [0, []]
;(node as NodeObject)[DOM_STASH] = [0, []]
}
return node as NodeObject
}
Expand All @@ -360,7 +375,13 @@ const replaceContainer = (node: NodeObject, from: DocumentFragment, to: Containe
}

const updateSync = (context: Context, node: NodeObject) => {
node[DOM_STASH][2]?.forEach(([c, v]) => {
c.values.push(v)
})
build(context, node, undefined)
node[DOM_STASH][2]?.forEach(([c]) => {
c.values.pop()
})
if (context[0] !== 1 || !context[1]) {
apply(node, node.c as Container)
}
Expand Down

0 comments on commit 6adc806

Please sign in to comment.