Skip to content

Client Components

hono/jsx supports not only server side but also client side. This means that it is possible to create an interactive UI that runs in the browser. We call it Client Components or hono/jsx/dom.

It is fast and very small. The counter program in hono/jsx/dom is only 2.8KB with Brotli compression. But, 47.8KB for React.

This section introduces Client Components-specific features.

Counter example

Here is an example of a simple counter, the same code works as in React.

tsx
import { useState } from 'hono/jsx'
import { render } from 'hono/jsx/dom'

function Counter() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

function App() {
  return (
    <html>
      <body>
        <Counter />
      </body>
    </html>
  )
}

const root = document.getElementById('root')
render(<App />, root)

render()

You can use render() to insert JSX components within a specified HTML element.

tsx
render(<Component />, container)

Hooks compatible with React

hono/jsx/dom has Hooks that are compatible or partially compatible with React. You can learn about these APIs by looking at the React documentation.

  • useState()
  • useEffect()
  • useRef()
  • useCallback()
  • use()
  • startTransition()
  • useTransition()
  • useDeferredValue()
  • useMemo()
  • useLayoutEffect()
  • useReducer()
  • useDebugValue()
  • createElement()
  • memo()
  • isValidElement()
  • useId()
  • createRef()
  • forwardRef()
  • useImperativeHandle()
  • useSyncExternalStore()
  • useInsertionEffect()
  • useFormStatus()
  • useActionState()
  • useOptimistic()

startViewTransition() family

The startViewTransition() family contains original hooks and functions to handle View Transitions API easily. The followings are examples of how to use them.

1. An easiest example

You can write a transition using the document.startViewTransition shortly with the startViewTransition().

tsx
import { useState, startViewTransition } from 'hono/jsx'
import { css, Style } from 'hono/css'

export default function App() {
  const [showLargeImage, setShowLargeImage] = useState(false)
  return (
    <>
      <Style />
      <button
        onClick={() =>
          startViewTransition(() =>
            setShowLargeImage((state) => !state)
          )
        }
      >
        Click!
      </button>
      <div>
        {!showLargeImage ? (
          <img src='https://hono.dev/images/logo.png' />
        ) : (
          <div
            class={css`
              background: url('https://hono.dev/images/logo-large.png');
              background-size: contain;
              background-repeat: no-repeat;
              background-position: center;
              width: 600px;
              height: 600px;
            `}
          ></div>
        )}
      </div>
    </>
  )
}

2. Using viewTransition() with keyframes()

The viewTransition() function allows you to get the unique view-transition-name.

You can use it with the keyframes(), The ::view-transition-old() is converted to ::view-transition-old(${uniqueName)).

tsx
import { useState, startViewTransition } from 'hono/jsx'
import { viewTransition } from 'hono/jsx/dom/css'
import { css, keyframes, Style } from 'hono/css'

const rotate = keyframes`
  from {
    rotate: 0deg;
  }
  to {
    rotate: 360deg;
  }
`

export default function App() {
  const [showLargeImage, setShowLargeImage] = useState(false)
  const [transitionNameClass] = useState(() =>
    viewTransition(css`
      ::view-transition-old() {
        animation-name: ${rotate};
      }
      ::view-transition-new() {
        animation-name: ${rotate};
      }
    `)
  )
  return (
    <>
      <Style />
      <button
        onClick={() =>
          startViewTransition(() =>
            setShowLargeImage((state) => !state)
          )
        }
      >
        Click!
      </button>
      <div>
        {!showLargeImage ? (
          <img src='https://hono.dev/images/logo.png' />
        ) : (
          <div
            class={css`
              ${transitionNameClass}
              background: url('https://hono.dev/images/logo-large.png');
              background-size: contain;
              background-repeat: no-repeat;
              background-position: center;
              width: 600px;
              height: 600px;
            `}
          ></div>
        )}
      </div>
    </>
  )
}

3. Using useViewTransition

If you want to change the style only during the animation. You can use useViewTransition(). This hook returns the [boolean, (callback: () => void) => void], and they are the isUpdating flag and the startViewTransition() function.

When this hook is used, the Component is evaluated at the following two times.

tsx
import { useState, useViewTransition } from 'hono/jsx'
import { viewTransition } from 'hono/jsx/dom/css'
import { css, keyframes, Style } from 'hono/css'

const rotate = keyframes`
  from {
    rotate: 0deg;
  }
  to {
    rotate: 360deg;
  }
`

export default function App() {
  const [isUpdating, startViewTransition] = useViewTransition()
  const [showLargeImage, setShowLargeImage] = useState(false)
  const [transitionNameClass] = useState(() =>
    viewTransition(css`
      ::view-transition-old() {
        animation-name: ${rotate};
      }
      ::view-transition-new() {
        animation-name: ${rotate};
      }
    `)
  )
  return (
    <>
      <Style />
      <button
        onClick={() =>
          startViewTransition(() =>
            setShowLargeImage((state) => !state)
          )
        }
      >
        Click!
      </button>
      <div>
        {!showLargeImage ? (
          <img src='https://hono.dev/images/logo.png' />
        ) : (
          <div
            class={css`
              ${transitionNameClass}
              background: url('https://hono.dev/images/logo-large.png');
              background-size: contain;
              background-repeat: no-repeat;
              background-position: center;
              width: 600px;
              height: 600px;
              position: relative;
              ${isUpdating &&
              css`
                &:before {
                  content: 'Loading...';
                  position: absolute;
                  top: 50%;
                  left: 50%;
                }
              `}
            `}
          ></div>
        )}
      </div>
    </>
  )
}

The hono/jsx/dom runtime

There is a small JSX Runtime for Client Components. Using this will result in smaller bundled results than using hono/jsx. Specify hono/jsx/dom in tsconfig.json.

json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx/dom"
  }
}

Released under the MIT License.