React
The @nano_kit/react package integrates @nano_kit/store signals and Dependency Injection with React components.
Installation
Section titled “Installation”Install the package using your favorite package manager:
pnpm add @nano_kit/store @nano_kit/reactyarn add @nano_kit/store @nano_kit/reactnpm install @nano_kit/store @nano_kit/reactSignals
Section titled “Signals”useSignal
Section titled “useSignal”The useSignal hook subscribes a React component to any accessor. The component will re-render automatically whenever the value changes.
import { signal } from '@nano_kit/store'import { useSignal } from '@nano_kit/react'
const $count = signal(0)
export function Counter() { const count = useSignal($count)
return ( <button onClick={() => $count(count => count + 1)}> Count: {count} </button> )}Dependency Injection
Section titled “Dependency Injection”InjectionContextProvider
Section titled “InjectionContextProvider”To use the Dependency Injection system within React, wrap your application (or part of it) in InjectionContextProvider. This component initializes an InjectionContext and makes it available to child components via the React Context API.
You can provide an existing context instance or an array of providers/values.
import { provide } from '@nano_kit/store'import { InjectionContextProvider } from '@nano_kit/react'import { Theme$ } from './tokens'import { App } from './App'
function Root() { return ( <InjectionContextProvider context={[ provide(Theme$, 'dark') ]} > <App /> </InjectionContextProvider> )}useInject
Section titled “useInject”The useInject hook retrieves a dependency from the current injection context. It throws an error if the context is missing, ensuring your dependencies are always resolved.
import { useInject } from '@nano_kit/react'import { Theme$ } from './tokens'
export function ThemedButton() { const theme = useInject(Theme$)
return <button className={`btn-${theme}`}>Click me</button>}injectHook
Section titled “injectHook”injectHook creates a reusable hook for a given injection token. This is useful for encapsulating injection logic away from components.
import { injectHook } from '@nano_kit/react'
/* Define token */function Theme$(): 'light' | 'dark' { return 'light'}
/* Create custom hook */const useTheme = injectHook(Theme$)
export function ThemedButton() { const theme = useTheme() // Type-safe 'light' | 'dark'
return <button className={`btn-${theme}`}>Click me</button>}signalHook
Section titled “signalHook”signalHook creates a hook that subscribes to a signal returned by a getter function. Useful for wrapping signal-valued injection tokens into stable hooks.
import { signalHook, injectHook } from '@nano_kit/react'import { Location$ } from '@nano_kit/router'
/* Creates a hook that reads and subscribes to the location signal */const useLocation = signalHook(injectHook(Location$))
export function ActiveLink({ route }) { const location = useLocation() /* ... */}Isolate
Section titled “Isolate”Isolate renders children with a fresh injection boundary. Use it when a subtree must not inherit dependencies from the parent context.
import { Isolate } from '@nano_kit/react'
export function Preview({ children }) { return ( <Isolate> {children} </Isolate> )}RSC-Side Dehydration
Section titled “RSC-Side Dehydration”Dehydration
Section titled “Dehydration”Dehydration is an async RSC component that dehydrates stores on the server and streams the snapshot to the client via HydrationProvider. It works on both initial page loads and RSC flight requests (client-side navigations), pushing fresh data to the client each time.
Dehydration boundaries can be nested and repeated in the same RSC tree. For example, a layout can dehydrate shared stores, while a page dehydrates page-specific stores. Boundaries in the same RSC request reuse the shared request context and skip stores that were already dehydrated.
import { inject } from '@nano_kit/store'import { Dehydration } from '@nano_kit/react'import { User$ } from './stores'
function Stores$() { const { $user } = inject(User$)
/* Will execute store logic, wait for all async tasks to complete, and extract the dehydrated snapshot */ return [$user]}
export default function Page() { return ( <Dehydration stores={Stores$}> <UserProfile /> </Dehydration> )}If you need to run dehydration manually before rendering, use dehydrate directly and pass the snapshot via dehydrated:
import { inject } from '@nano_kit/store'import { Dehydration, dehydrate, getDehydrationContext} from '@nano_kit/react'import { User$ } from './stores'
function Stores$() { const { $user } = inject(User$)
/* Will execute store logic, wait for all async tasks to complete, and extract the dehydrated snapshot */ return [$user]}
export default async function Page() { const dehydrated = await dehydrate(Stores$) const context = getDehydrationContext()
/* ...You can use `context` or `dehydrated` here to implement some logic before rendering... */
return ( <Dehydration dehydrated={dehydrated}> <UserProfile /> </Dehydration> )}Props:
stores?— function returning an array of accessors to dehydratedehydrated?— pre-dehydrated data (skipsstoresdehydration if provided)context?— additional providers to inject into the server context before dehydrationisolate?— create a newInjectionContexton the client instead of reusing the existing one
StaticDehydration
Section titled “StaticDehydration”StaticDehydration is like Dehydration but performs dehydration only on the initial full-page request and skips it on RSC flight requests (client-side navigations where React fetches only the server component payload, not a full HTML page), detected via FlightDetector. In that case, during client-side navigation, stores will fetch and resolve data in the browser instead of the server.
Like Dehydration, StaticDehydration can be nested and repeated. This makes it valid to place one boundary in a layout and another boundary in a page.
import { inject } from '@nano_kit/store'import { StaticDehydration } from '@nano_kit/react'import { User$ } from './stores'
function Stores$() { const { $user } = inject(User$)
/* Will execute store logic, wait for all async tasks to complete, and extract the dehydrated snapshot */ return [$user]}
export default function Page() { return ( <StaticDehydration stores={Stores$}> <UserProfile /> </StaticDehydration> )}Props:
stores?— function returning an array of accessors to dehydratedehydrated?— pre-dehydrated data (skipsstoresdehydration if provided)context?— additional providers to inject into the server context before dehydrationisolate?— create a newInjectionContexton the client instead of reusing the existing oneflight?— override flight detection; whenfalse, dehydration is always included
FlightDetector
Section titled “FlightDetector”FlightDetector marks the current RSC request as a non-flight render. Place it in the root layout so StaticDehydration knows when to include dehydrated data.
import { FlightDetector } from '@nano_kit/react'
export default function RootLayout({ children }) { return ( <html> <body> <FlightDetector> {children} </FlightDetector> </body> </html> )}dehydrate
Section titled “dehydrate”Low-level RSC helper that runs stores in the shared per-request injection context and returns the dehydrated snapshot. Use it when you need to manually prepare a snapshot before rendering instead of letting Dehydration or StaticDehydration run stores from their stores prop.
After dehydrate(Stores$) resolves, the stores have already run and populated their data. Use getDehydrationContext() when you need request-time logic based on those populated stores, such as redirecting when a loaded user is missing.
import { redirect } from 'next/navigation'import { inject } from '@nano_kit/store'import { Dehydration, dehydrate, getDehydrationContext} from '@nano_kit/react'import { User$ } from './stores'
function Stores$() { const { $user } = inject(User$)
return [$user]}
export default async function Page() { const dehydrated = await dehydrate(Stores$) const context = getDehydrationContext() const { $user } = context.get(User$)
if (!$user()) { redirect('/login') }
return ( <Dehydration dehydrated={dehydrated}> <UserProfile /> </Dehydration> )}Client-Side Hydration
Section titled “Client-Side Hydration”HydrationProvider
Section titled “HydrationProvider”HydrationProvider sets up a reactive hydration context for client-side hydration. It uses ActiveHydrator under the hood, which supports streaming — the dehydrated prop can be updated on re-renders to feed new data.
import { HydrationProvider } from '@nano_kit/react'
function App({ dehydrated }) { return ( <HydrationProvider dehydrated={dehydrated}> <UserProfile /> </HydrationProvider> )}Props:
dehydrated?— dehydrated key-value pairs[string, unknown][], or a falsy value to skip hydrationcontext?— additional injection providersreuse?— reuse an existingInjectionContextinstead of creating a new one;trueby default
StaticHydrationProvider
Section titled “StaticHydrationProvider”StaticHydrationProvider is a simpler one-shot variant using StaticHydrator. It applies the initial dehydrated snapshot once and never updates. Use this for classic SSR where the full snapshot is available at first render.
import { StaticHydrationProvider } from '@nano_kit/react'
function App({ dehydrated }) { return ( <StaticHydrationProvider dehydrated={dehydrated}> <UserProfile /> </StaticHydrationProvider> )}isFlight
Section titled “isFlight”Utility to detect flight requests from HTTP headers. Returns true when the accept header does not include text/html.
import { isFlight } from '@nano_kit/react'import { headers } from 'next/headers'
/* Works with promise-based headers */const flightFromHeaders = await isFlight(headers())/* Or with plain header objects */const flightFromRequest = isFlight(req.headers)