React Hooks
React hooks for typed IPC in webview apps
Overview
The @fcannizzaro/native-window-ipc-react package provides React hooks for the Typed IPC channel. It wraps createChannelClient from @fcannizzaro/native-window-ipc/client with idiomatic React lifecycle management — context providers, automatic subscribe/unsubscribe, and stable function references.
Use this package when your webview content is a React app bundled with its own build step (Vite, webpack, etc.). It gives you:
createChannelHooks— factory that returns pre-typed hooks (recommended)ChannelProvider— creates the channel client once and provides it via React contextuseChannel— access the typed channel from any componentuseChannelEvent— subscribe to events with automatic cleanup on unmountuseSend— get a stablesendfunction that won't cause unnecessary re-renders
The package is pure TypeScript with zero runtime dependencies. React and @fcannizzaro/native-window-ipc are peer dependencies.
Installation
bun add @fcannizzaro/native-window-ipc-reactPeer dependencies:
| Package | Version |
|---|---|
react | ^18.0.0 || ^19.0.0 |
@fcannizzaro/native-window-ipc | workspace:* |
typescript | ^5 |
A schema library is also required for runtime validation. See Supported Schema Libraries.
Setup with createChannelHooks (Recommended)
The createChannelHooks factory returns a set of pre-typed hooks. Event names and payload types are inferred from your schemas — no need to pass generic type parameters at every call site:
// channel.ts — define once, import everywhere
import { z } from "zod";
import { createChannelHooks } from "@fcannizzaro/native-window-ipc-react";
export const {
ChannelProvider,
useChannel,
useChannelEvent,
useSend,
} = createChannelHooks({
/** Host -> Webview: update the displayed title */
"update-title": z.string(),
/** Webview -> Host: user clicked a button */
counter: z.number(),
});Wrap your app with the provider at the root:
// main.tsx
import { ChannelProvider } from "./channel";
function Root() {
return (
<ChannelProvider>
<App />
</ChannelProvider>
);
}All hooks returned by the factory are pre-typed — no generics needed:
import { useChannelEvent, useSend } from "./channel";
function Counter() {
const send = useSend();
useChannelEvent("update-title", (title) => {
// title is inferred as string
document.title = title;
});
return (
<button onClick={() => send("counter", 1)}>
Increment
</button>
);
}You can also pass an onValidationError callback as the second argument:
const hooks = createChannelHooks(schemas, {
onValidationError: (type, payload) => {
console.warn(`Invalid "${type}" payload:`, payload);
},
});Each call to createChannelHooks creates its own React context, so multiple independent channels are supported if needed.
Hooks
useChannel
Access the typed channel from any component inside the provider. Throws if called outside the ChannelProvider.
import { useChannel } from "./channel";
function StatusBar() {
const channel = useChannel();
// Full access to send, on, off — all typed
channel.send("counter", 1);
return <div>...</div>;
}useChannelEvent
Subscribe to a specific event type with automatic cleanup. The subscription is created on mount and removed on unmount:
import { useState } from "react";
import { useChannelEvent } from "./channel";
function TitleDisplay() {
const [title, setTitle] = useState("Untitled");
useChannelEvent("update-title", (newTitle) => {
// newTitle is inferred as string
setTitle(newTitle);
});
return <h1>{title}</h1>;
}Handler stability: The handler is stored in a ref internally, so passing a new function on every render does not cause a re-subscription. The underlying on()/off() calls only happen when the event type string changes or the component unmounts.
useSend
Returns a stable send function. The function identity does not change between renders, making it safe to pass as a prop without causing unnecessary re-renders in child components:
import { useSend } from "./channel";
function Counter() {
const send = useSend();
return (
<button onClick={() => send("counter", 1)}>
Increment
</button>
);
}Full Example
A complete React webview app using createChannelHooks:
Tip: Schemas are plain objects with no platform-specific code. Define them once in a shared file and import on both the host (Node.js/native-window-ipc-react
// schemas.ts — shared between host and webview
import { z } from "zod";
export const schemas = {
"update-title": z.string(),
"show-notification": z.object({
message: z.string(),
level: z.enum(["info", "warn", "error"]),
}),
counter: z.number(),
"user-action": z.string(),
};// channel.ts
import { createChannelHooks } from "@fcannizzaro/native-window-ipc";
import { schemas } from "./schemas";
export const {
ChannelProvider,
useChannelEvent,
useSend,
} = createChannelHooks(schemas);// App.tsx
import { useState } from "react";
import { ChannelProvider, useChannelEvent, useSend } from "./channel";
function App() {
const [title, setTitle] = useState("My App");
const [notifications, setNotifications] = useState<string[]>([]);
const send = useSend();
useChannelEvent("update-title", (newTitle) => {
setTitle(newTitle);
});
useChannelEvent("show-notification", (notification) => {
setNotifications((prev) => [...prev, notification.message]);
});
return (
<div>
<h1>{title}</h1>
<button onClick={() => send("counter", 1)}>+1</button>
<button onClick={() => send("user-action", "reset")}>Reset</button>
<ul>
{notifications.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
</div>
);
}
export default function Root() {
return (
<ChannelProvider>
<App />
</ChannelProvider>
);
}On the host side, the corresponding setup would be:
// host.ts (Node.js / Bun)
import { createWindow } from "@fcannizzaro/native-window-ipc";
import { schemas } from "./schemas";
const ch = createWindow(
{ title: "My App", width: 800, height: 600 },
{ schemas },
);
ch.window.loadUrl("http://localhost:5173"); // Vite dev server
ch.on("counter", (n) => console.log("Counter:", n));
ch.on("user-action", (action) => console.log("Action:", action));
ch.send("update-title", "Hello from host!");
ch.send("show-notification", {
message: "Connected to host",
level: "info",
});Standalone Hooks
If you prefer not to use the factory pattern, the package also exports standalone hooks that accept generic type parameters. This approach requires you to define an event map type and pass it to each hook:
import { z } from "zod";
import {
ChannelProvider,
useChannel,
useChannelEvent,
useSend,
} from "@fcannizzaro/native-window-ipc-react";
const schemas = {
counter: z.number(),
title: z.string(),
};
type Events = { counter: number; title: string };
function App() {
const send = useSend<Events>();
useChannelEvent<Events, "title">("title", (t) => {
document.title = t;
});
return <button onClick={() => send("counter", 1)}>+1</button>;
}
function Root() {
return (
<ChannelProvider schemas={schemas}>
<App />
</ChannelProvider>
);
}The standalone ChannelProvider accepts schemas and onValidationError as props:
| Prop | Type | Default | Description |
|---|---|---|---|
schemas | SchemaMap | (required) | Schemas for runtime validation and type inference |
onValidationError | (type, payload) => void | — | Called when an incoming payload fails validation |
children | ReactNode | (required) | React children |
API Reference
Exports
| Export | Kind | Description |
|---|---|---|
createChannelHooks(schemas, options?) | Factory | Returns pre-typed { ChannelProvider, useChannel, useChannelEvent, useSend } |
ChannelProvider | Component | Standalone context provider (requires schemas prop) |
ChannelProviderProps<S> | Type | Props interface for the standalone provider |
useChannel<T>() | Hook | Standalone: access the typed channel from context |
useChannelEvent<T, K>(type, handler) | Hook | Standalone: subscribe to events with automatic cleanup |
useSend<T>() | Hook | Standalone: stable send function |
TypedChannelHooks<T> | Type | Return type of createChannelHooks |
ChannelHooksOptions | Type | Options for createChannelHooks |
Type Re-exports
The package re-exports all core types from @fcannizzaro/native-window-ipc so you don't need to import from two packages:
EventMap, SchemaLike, SchemaMap, InferSchemaMap, InferOutput, ValidationErrorHandler, TypedChannel, ChannelClientOptions