Skip to content

Commit

Permalink
fix(jsx): allow null, undefined, and boolean to be returned from func…
Browse files Browse the repository at this point in the history
…tion component (honojs#3241)

* fix(jsx): allow `null`, `undefined`, and `boolean` to be returned from function component

* fix(jsx): allow `null` to be returned from `FC` type

* test: add test for empty fragment in `"jsx": "precompile"`

Empty Fragment is converted to null in `"jsx": "precompile"`, so add a test for that pattern
  • Loading branch information
usualoma authored Aug 8, 2024
1 parent e123546 commit fbed2df
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 5 deletions.
6 changes: 6 additions & 0 deletions runtime_tests/deno-jsx/jsx.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ Deno.test('JSX: Fragment', () => {
assertEquals(fragment.toString(), '<p>1</p><p>2</p>')
})

Deno.test('JSX: Empty Fragment', () => {
const Component = () => <></>
const html = <Component />
assertEquals(html.toString(), '')
})

Deno.test('JSX: Async Component', async () => {
const Component = async ({ name }: { name: string }) =>
new Promise<HtmlEscapedString>((resolve) => setTimeout(() => resolve(<span>{name}</span>), 10))
Expand Down
13 changes: 8 additions & 5 deletions src/jsx/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { domRenderers } from './intrinsic-element/common'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Props = Record<string, any>
export type FC<P = Props> = {
(props: P): HtmlEscapedString | Promise<HtmlEscapedString>
(props: P): HtmlEscapedString | Promise<HtmlEscapedString> | null
defaultProps?: Partial<P> | undefined
displayName?: string | undefined
}
Expand Down Expand Up @@ -247,7 +247,10 @@ class JSXFunctionNode extends JSXNode {
children: children.length <= 1 ? children[0] : children,
})

if (res instanceof Promise) {
if (typeof res === 'boolean' || res == null) {
// boolean or null or undefined
return
} else if (res instanceof Promise) {
if (globalContexts.length === 0) {
buffer.unshift('', res)
} else {
Expand Down Expand Up @@ -371,11 +374,11 @@ export const memo = <T>(
component: FC<T>,
propsAreEqual: (prevProps: Readonly<T>, nextProps: Readonly<T>) => boolean = shallowEqual
): FC<T> => {
let computed: HtmlEscapedString | Promise<HtmlEscapedString> | undefined = undefined
let computed: ReturnType<FC<T>> = null
let prevProps: T | undefined = undefined
return ((props: T & { children?: Child }): HtmlEscapedString | Promise<HtmlEscapedString> => {
return ((props) => {
if (prevProps && !propsAreEqual(prevProps, props)) {
computed = undefined
computed = null
}
prevProps = props
return (computed ||= component(props))
Expand Down
18 changes: 18 additions & 0 deletions src/jsx/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,24 @@ describe('render to string', () => {
'<html><head><title>Home page</title></head><body><h1>Hono</h1><p>Hono is great</p></body></html>'
)
})

describe('Booleans, Null, and Undefined Are Ignored', () => {
it.each([true, false, undefined, null])('%s', (item) => {
const Component: FC = (() => {
return item
}) as FC
const template = <Component />
expect(template.toString()).toBe('')
})

it('falsy value', () => {
const Component: FC = (() => {
return 0
}) as unknown as FC
const template = <Component />
expect(template.toString()).toBe('0')
})
})
})

describe('style attribute', () => {
Expand Down

0 comments on commit fbed2df

Please sign in to comment.