-
-
Notifications
You must be signed in to change notification settings - Fork 630
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(jsx/dom): introduce react-dom/client APIs and React.version (#2795)
* feat(jsx): export version string from jsx * feat(jsx/dom): introduce react-dom/client APIs * docs(jsx/dom/client): add module description * chore: add jsx/dom/client to jsr.json * refactor(jsx/dom): declare types explicitly.
- Loading branch information
Showing
10 changed files
with
243 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
/** @jsxImportSource ../ */ | ||
import { JSDOM } from 'jsdom' | ||
import { createRoot, hydrateRoot } from './client' | ||
import { useEffect } from '.' | ||
|
||
describe('createRoot', () => { | ||
beforeAll(() => { | ||
global.requestAnimationFrame = (cb) => setTimeout(cb) | ||
}) | ||
|
||
let dom: JSDOM | ||
let rootElement: HTMLElement | ||
beforeEach(() => { | ||
dom = new JSDOM('<html><body><div id="root"></div></body></html>', { | ||
runScripts: 'dangerously', | ||
}) | ||
global.document = dom.window.document | ||
global.HTMLElement = dom.window.HTMLElement | ||
global.SVGElement = dom.window.SVGElement | ||
global.Text = dom.window.Text | ||
rootElement = document.getElementById('root') as HTMLElement | ||
}) | ||
|
||
it('render / unmount', async () => { | ||
const cleanup = vi.fn() | ||
const App = () => { | ||
useEffect(() => cleanup, []) | ||
return <h1>Hello</h1> | ||
} | ||
const root = createRoot(rootElement) | ||
root.render(<App />) | ||
expect(rootElement.innerHTML).toBe('<h1>Hello</h1>') | ||
await new Promise((resolve) => setTimeout(resolve)) | ||
root.unmount() | ||
await Promise.resolve() | ||
expect(rootElement.innerHTML).toBe('') | ||
expect(cleanup).toHaveBeenCalled() | ||
}) | ||
|
||
it('call render twice', async () => { | ||
const App = <h1>Hello</h1> | ||
const App2 = <h1>World</h1> | ||
const root = createRoot(rootElement) | ||
root.render(App) | ||
expect(rootElement.innerHTML).toBe('<h1>Hello</h1>') | ||
|
||
const createElementSpy = vi.spyOn(dom.window.document, 'createElement') | ||
|
||
root.render(App2) | ||
await Promise.resolve() | ||
expect(rootElement.innerHTML).toBe('<h1>World</h1>') | ||
|
||
expect(createElementSpy).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('call render after unmount', async () => { | ||
const App = <h1>Hello</h1> | ||
const App2 = <h1>World</h1> | ||
const root = createRoot(rootElement) | ||
root.render(App) | ||
expect(rootElement.innerHTML).toBe('<h1>Hello</h1>') | ||
root.unmount() | ||
expect(() => root.render(App2)).toThrow('Cannot update an unmounted root') | ||
}) | ||
}) | ||
|
||
describe('hydrateRoot', () => { | ||
let dom: JSDOM | ||
let rootElement: HTMLElement | ||
beforeEach(() => { | ||
dom = new JSDOM('<html><body><div id="root"></div></body></html>', { | ||
runScripts: 'dangerously', | ||
}) | ||
global.document = dom.window.document | ||
global.HTMLElement = dom.window.HTMLElement | ||
global.SVGElement = dom.window.SVGElement | ||
global.Text = dom.window.Text | ||
rootElement = document.getElementById('root') as HTMLElement | ||
}) | ||
|
||
it('should return root object', async () => { | ||
const cleanup = vi.fn() | ||
const App = () => { | ||
useEffect(() => cleanup, []) | ||
return <h1>Hello</h1> | ||
} | ||
const root = hydrateRoot(rootElement, <App />) | ||
expect(rootElement.innerHTML).toBe('<h1>Hello</h1>') | ||
await new Promise((resolve) => setTimeout(resolve)) | ||
root.unmount() | ||
await Promise.resolve() | ||
expect(rootElement.innerHTML).toBe('') | ||
expect(cleanup).toHaveBeenCalled() | ||
}) | ||
|
||
it('call render', async () => { | ||
const App = <h1>Hello</h1> | ||
const App2 = <h1>World</h1> | ||
const root = hydrateRoot(rootElement, App) | ||
expect(rootElement.innerHTML).toBe('<h1>Hello</h1>') | ||
|
||
const createElementSpy = vi.spyOn(dom.window.document, 'createElement') | ||
|
||
root.render(App2) | ||
await Promise.resolve() | ||
expect(rootElement.innerHTML).toBe('<h1>World</h1>') | ||
|
||
expect(createElementSpy).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('call render after unmount', async () => { | ||
const App = <h1>Hello</h1> | ||
const App2 = <h1>World</h1> | ||
const root = hydrateRoot(rootElement, App) | ||
expect(rootElement.innerHTML).toBe('<h1>Hello</h1>') | ||
root.unmount() | ||
expect(() => root.render(App2)).toThrow('Cannot update an unmounted root') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/** | ||
* @module | ||
* This module provides APIs for `hono/jsx/dom/client`, which is compatible with `react-dom/client`. | ||
*/ | ||
|
||
import type { Child } from '../base' | ||
import { useState } from '../hooks' | ||
import { buildNode, renderNode } from './render' | ||
import type { NodeObject } from './render' | ||
|
||
export interface Root { | ||
render(children: Child): void | ||
unmount(): void | ||
} | ||
export type RootOptions = Record<string, unknown> | ||
|
||
/** | ||
* Create a root object for rendering | ||
* @param element Render target | ||
* @param options Options for createRoot (not supported yet) | ||
* @returns Root object has `render` and `unmount` methods | ||
*/ | ||
export const createRoot = ( | ||
element: HTMLElement | DocumentFragment, | ||
options: RootOptions = {} | ||
): Root => { | ||
let setJsxNode: | ||
| undefined // initial state | ||
| ((jsxNode: unknown) => void) // rendered | ||
| null = // unmounted | ||
undefined | ||
|
||
if (Object.keys(options).length > 0) { | ||
console.warn('createRoot options are not supported yet') | ||
} | ||
|
||
return { | ||
render(jsxNode: unknown) { | ||
if (setJsxNode === null) { | ||
// unmounted | ||
throw new Error('Cannot update an unmounted root') | ||
} | ||
if (setJsxNode) { | ||
// rendered | ||
setJsxNode(jsxNode) | ||
} else { | ||
renderNode( | ||
buildNode({ | ||
tag: () => { | ||
const [_jsxNode, _setJsxNode] = useState(jsxNode) | ||
setJsxNode = _setJsxNode | ||
return _jsxNode | ||
}, | ||
props: {}, | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
} as any) as NodeObject, | ||
element | ||
) | ||
} | ||
}, | ||
unmount() { | ||
setJsxNode?.(null) | ||
setJsxNode = null | ||
}, | ||
} | ||
} | ||
|
||
/** | ||
* Create a root object and hydrate app to the target element. | ||
* In hono/jsx/dom, hydrate is equivalent to render. | ||
* @param element Render target | ||
* @param reactNode A JSXNode to render | ||
* @param options Options for createRoot (not supported yet) | ||
* @returns Root object has `render` and `unmount` methods | ||
*/ | ||
export const hydrateRoot = ( | ||
element: HTMLElement | DocumentFragment, | ||
reactNode: Child, | ||
options: RootOptions = {} | ||
): Root => { | ||
const root = createRoot(element, options) | ||
root.render(reactNode) | ||
return root | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters