Skip to content

Commit

Permalink
fix: add "key" attribute to suppress react "key" warnings (#238)
Browse files Browse the repository at this point in the history
Fixes #233

* fix: add key attribute to placeholder template element

To suppress react key warnings in SSR.

* fix(client): add key attribute to each nested element in runtime

To suppress react key warnings in runtime.

* refactor: use `createElement` instead of `jsx` for compatibility with react
  • Loading branch information
usualoma authored Nov 24, 2024
1 parent 034cd20 commit 3d783f1
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 7 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"@types/node": "^20.10.5",
"eslint": "^9.10.0",
"glob": "^10.3.10",
"happy-dom": "^15.11.6",
"hono": "4.4.13",
"np": "7.7.0",
"prettier": "^3.1.1",
Expand Down
5 changes: 2 additions & 3 deletions src/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { render } from 'hono/jsx/dom'
import { jsx as jsxFn } from 'hono/jsx/dom/jsx-runtime'
import { render, createElement as createElementHono } from 'hono/jsx/dom'
import {
COMPONENT_EXPORT,
COMPONENT_NAME,
Expand Down Expand Up @@ -61,7 +60,7 @@ export const createClient = async (options?: ClientOptions) => {
const props = JSON.parse(serializedProps ?? '{}') as Record<string, unknown>

const hydrate = options?.hydrate ?? render
const createElement = options?.createElement ?? jsxFn
const createElement = options?.createElement ?? createElementHono

let maybeTemplate = element.childNodes[element.childNodes.length - 1]
while (maybeTemplate?.nodeName === 'TEMPLATE') {
Expand Down
21 changes: 21 additions & 0 deletions src/client/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { buildCreateChildrenFn } from './runtime'

describe('buildCreateChildrenFn', () => {
it('should set key for children', async () => {
const createElement = vi.fn()
const importComponent = vi.fn()
const createChildren = buildCreateChildrenFn(createElement, importComponent)

const div = document.createElement('div')
div.innerHTML = '<span>test</span><div>test2</div>'
const result = await createChildren(div.childNodes)
expect(createElement).toHaveBeenNthCalledWith(1, 'SPAN', {
children: ['test'],
key: 1,
})
expect(createElement).toHaveBeenNthCalledWith(2, 'DIV', {
children: ['test2'],
key: 2,
})
})
})
13 changes: 11 additions & 2 deletions src/client/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const buildCreateChildrenFn = (
createElement: CreateElement,
importComponent: ImportComponent
): CreateChildren => {
let keyIndex = 0
const setChildrenFromTemplate = async (props: { children?: Node[] }, element: HTMLElement) => {
const maybeTemplate = element.childNodes[element.childNodes.length - 1]
if (
Expand All @@ -26,7 +27,10 @@ export const buildCreateChildrenFn = (
for (let i = 0; i < attributes.length; i++) {
props[attributes[i].name] = attributes[i].value
}
return createElement(element.nodeName, props)
return createElement(element.nodeName, {
key: ++keyIndex,
...props,
})
}
const createChildren = async (childNodes: NodeListOf<ChildNode>): Promise<Node[]> => {
const children = []
Expand Down Expand Up @@ -117,7 +121,12 @@ export const buildCreateChildrenFn = (
if (component) {
const props = JSON.parse(child.getAttribute(DATA_SERIALIZED_PROPS) || '{}')
await setChildrenFromTemplate(props, child)
children.push(await createElement(component, props))
children.push(
await createElement(component, {
key: ++keyIndex,
...props,
})
)
} else {
children.push(await createElementFromHTMLElement(child))
}
Expand Down
18 changes: 18 additions & 0 deletions src/vite/components/honox-island.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { HonoXIsland } from './honox-island'

const TestComponent = () => <div>Test</div>

describe('HonoXIsland', () => {
it('should set key for children', () => {
const element = HonoXIsland({
componentName: 'Test',
componentExport: 'Test',
Component: () => <div>Test</div>,
props: {
children: <TestComponent />,
},
})
// XXX: tested by internal implementation
expect((element as any).children[1][0].key).toBe('children')
})
})
2 changes: 1 addition & 1 deletion src/vite/components/honox-island.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const HonoXIsland = ({
<Component {...props} />
</IslandContext.Provider>
{Object.entries(elementProps).map(([key, children]) => (
<template {...{ [DATA_HONO_TEMPLATE]: key }}>
<template {...{ [DATA_HONO_TEMPLATE]: key }} key={key}>
<IslandContext.Provider value={{ ...islandState, [inChildren]: true }}>
{children}
</IslandContext.Provider>
Expand Down
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'happy-dom',
exclude: ['node_modules', 'dist', '.git', '.cache', 'test-presets', 'sandbox', 'examples'],
},
})
21 changes: 20 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
# bun ./bun.lockb --hash: 543A293A66C80C95-f7ab18b32b9678e2-D7579C357F066BE3-94c80f775e73166d
# bun ./bun.lockb --hash: 4D4CD2136DCE3975-f38db2e954b12206-5D94700DFFA576F1-3e4dda30ab363703


"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.24.7":
Expand Down Expand Up @@ -2521,6 +2521,15 @@ graphemer@^1.4.0:
resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==

happy-dom@^15.11.6:
version "15.11.6"
resolved "https://registry.npmjs.org/happy-dom/-/happy-dom-15.11.6.tgz"
integrity sha512-elX7iUTu+5+3b2+NGQc0L3eWyq9jKhuJJ4GpOMxxT/c2pg9O3L5H3ty2VECX0XXZgRmmRqXyOK8brA2hDI6LsQ==
dependencies:
entities "^4.5.0"
webidl-conversions "^7.0.0"
whatwg-mimetype "^3.0.0"

hard-rejection@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz"
Expand Down Expand Up @@ -5491,6 +5500,16 @@ webidl-conversions@^4.0.2:
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz"
integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==

webidl-conversions@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz"
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==

whatwg-mimetype@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz"
integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==

whatwg-url@^7.0.0:
version "7.1.0"
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz"
Expand Down

0 comments on commit 3d783f1

Please sign in to comment.