A React island
When a component needs something the native tier cannot express, such as useDeferredValue, useReducer or a component from a package, brust keeps it as real React: rendered on the server, then hydrated in the browser.
1Use a React-only hook
The finder filters Pokémon by the deferred text: useDeferredValue lets React keep the input responsive while the list catches up. Add it to routes.tsx like any page:
import { useDeferredValue, useState } from 'react'
import { POKEMON } from '../lib/pokemon'
export default function PokemonFinder() {
const [q, setQ] = useState('')
// the list filters on the deferred text, so typing never waits for the filter
const deferred = useDeferredValue(q)
const hits = POKEMON.filter((p) => p.name.includes(deferred.toLowerCase()))
return (
<section>
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Find a Pokémon" />
<ul>{hits.map((p) => <li key={p.id}>{p.name}</li>)}</ul>
</section>
)
}{ path: '/finder', Component: PokemonFinder },2Build and read the tier
The native tier has no useDeferredValue, so the compiler falls back to react and says why. The manifest records one ssr job (the server render) and a client chunk (the island).
$ brust build routes.tsx
warning hook-unsupported components/PokemonFinder.tsx:7:20 hook useDeferredValue is not supported
[brust] built dist in 178mspokemonFinder_3e4f4a6c tier: react jobs: [ssr] client: client/react-pokemonFinder_3e4f4a6c-d5dea8c523.jsIn the browser, typing ee narrows the list to eevee. The page works before hydration too: the first paint is the server-rendered list.
The warning is information, not an error. The hooks that keep a component native and the ones that make it an island are listed under What decides the tier.
3Custom hooks stay native
A hook you write yourself is not a reason for an island. If its body uses only hooks the native tier supports, the compiler inlines it into the component, which stays native and ships no React. This debounce hook is one:
import { useEffect, useState } from 'react'
export function useDebounced<T>(value: T, ms: number): T {
const [v, setV] = useState(value)
useEffect(() => {
const t = setTimeout(() => setV(value), ms)
return () => clearTimeout(t)
}, [value, ms])
return v
}The same finder written with useDebounced(q, 200) instead of useDeferredValue builds with no warning and a client/ chunk of a few hundred bytes. The landing page's search box is exactly that pair: the react panel uses useDeferredValue.