@nativewindow/react 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Francesco Saverio Cannizzaro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # @nativewindow/react
2
+
3
+ [![npm](https://img.shields.io/npm/v/@nativewindow/react)](https://www.npmjs.com/package/@nativewindow/react)
4
+
5
+ > [!WARNING]
6
+ > This project is in **alpha**. APIs may change without notice.
7
+
8
+ React hooks for [native-window-ipc](https://github.com/nativewindow/webview/tree/main/packages/ipc). Provides type-safe React bindings for the webview side of the IPC channel.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ bun add @nativewindow/react
14
+ # or
15
+ deno add npm:@nativewindow/react
16
+ ```
17
+
18
+ ### Peer Dependencies
19
+
20
+ - `react` ^18.0.0 || ^19.0.0
21
+ - `@nativewindow/ipc`
22
+
23
+ ## Usage
24
+
25
+ ### Factory approach (recommended)
26
+
27
+ Create pre-typed hooks from your schemas:
28
+
29
+ ```ts
30
+ // channel.ts
31
+ import { z } from "zod";
32
+ import { createChannelHooks } from "@nativewindow/react";
33
+
34
+ export const { ChannelProvider, useChannel, useChannelEvent, useSend } = createChannelHooks({
35
+ schemas: {
36
+ counter: z.number(),
37
+ "update-title": z.string(),
38
+ },
39
+ });
40
+ ```
41
+
42
+ ### Provider setup
43
+
44
+ Wrap your app with `ChannelProvider`:
45
+
46
+ ```tsx
47
+ import { ChannelProvider } from "./channel";
48
+
49
+ function App() {
50
+ return (
51
+ <ChannelProvider>
52
+ <Counter />
53
+ </ChannelProvider>
54
+ );
55
+ }
56
+ ```
57
+
58
+ ### Hooks
59
+
60
+ ```tsx
61
+ import { useChannelEvent, useSend } from "./channel";
62
+
63
+ function Counter() {
64
+ const [count, setCount] = useState(0);
65
+ const send = useSend();
66
+
67
+ // Subscribe to events with automatic cleanup
68
+ useChannelEvent("counter", (n) => {
69
+ setCount(n);
70
+ });
71
+
72
+ return <button onClick={() => send("counter", count + 1)}>Count: {count}</button>;
73
+ }
74
+ ```
75
+
76
+ ## API
77
+
78
+ ### `createChannelHooks(options)`
79
+
80
+ Factory that returns pre-typed `{ ChannelProvider, useChannel, useChannelEvent, useSend }`. Each call creates its own React context, supporting multiple independent channels.
81
+
82
+ ### `ChannelProvider`
83
+
84
+ React component that creates a `createChannelClient` instance and provides it via context.
85
+
86
+ ### `useChannel()`
87
+
88
+ Access the typed channel from context. Throws if used outside `ChannelProvider`.
89
+
90
+ ### `useChannelEvent(type, handler)`
91
+
92
+ Subscribe to a specific IPC event type. Automatically cleans up on unmount. Handler is stored in a ref to avoid re-subscriptions on handler identity changes.
93
+
94
+ ### `useSend()`
95
+
96
+ Returns a stable `send` function (memoized via `useCallback`).
97
+
98
+ ## Documentation
99
+
100
+ Full documentation at [nativewindow.fcannizzaro.com](https://nativewindow.fcannizzaro.com)
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,227 @@
1
+ import { ReactNode } from 'react';
2
+ import { EventMap, SendArgs, SchemaMap, InferSchemaMap, TypedChannel, ValidationErrorHandler } from '@nativewindow/ipc';
3
+ /**
4
+ * React hooks for the native-window typed IPC channel (webview-side).
5
+ *
6
+ * Provides lifecycle wrappers around `createChannelClient` from
7
+ * `@nativewindow/ipc/client` for use inside a React app
8
+ * running in a native webview.
9
+ *
10
+ * The recommended approach is to use {@link createChannelHooks} to get
11
+ * a set of pre-typed hooks that infer event types from your schemas:
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { z } from "zod";
16
+ * import { createChannelHooks } from "@nativewindow/react";
17
+ *
18
+ * const { ChannelProvider, useChannel, useChannelEvent, useSend } =
19
+ * createChannelHooks({
20
+ * counter: z.number(),
21
+ * title: z.string(),
22
+ * });
23
+ *
24
+ * function App() {
25
+ * const send = useSend();
26
+ * useChannelEvent("title", (t) => { document.title = t; });
27
+ * return <button onClick={() => send("counter", 1)}>+1</button>;
28
+ * }
29
+ *
30
+ * function Root() {
31
+ * return (
32
+ * <ChannelProvider>
33
+ * <App />
34
+ * </ChannelProvider>
35
+ * );
36
+ * }
37
+ * ```
38
+ *
39
+ * @packageDocumentation
40
+ */
41
+ export type { EventMap, SchemaLike, SchemaMap, InferSchemaMap, InferOutput, SendArgs, ValidationErrorHandler, TypedChannel, } from '@nativewindow/ipc';
42
+ export type { ChannelClientOptions } from '../ipc/client.ts';
43
+ /**
44
+ * Props for {@link ChannelProvider}.
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * <ChannelProvider schemas={schemas}>
49
+ * <App />
50
+ * </ChannelProvider>
51
+ * ```
52
+ */
53
+ export interface ChannelProviderProps<S extends SchemaMap> {
54
+ /** Schemas for each event. Provides both TypeScript types and runtime validation. */
55
+ schemas: S;
56
+ /**
57
+ * Called when an incoming payload fails schema validation.
58
+ * If not provided, failed payloads are silently dropped.
59
+ */
60
+ onValidationError?: ValidationErrorHandler;
61
+ /** React children. */
62
+ children: ReactNode;
63
+ }
64
+ /**
65
+ * Provides a typed IPC channel to the React tree.
66
+ *
67
+ * Creates the channel client exactly once (on initial mount) via
68
+ * `createChannelClient` from `@nativewindow/ipc/client`.
69
+ * The channel instance is stable for the lifetime of the provider.
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * import { z } from "zod";
74
+ * import { ChannelProvider } from "@nativewindow/react";
75
+ *
76
+ * const schemas = {
77
+ * counter: z.number(),
78
+ * title: z.string(),
79
+ * };
80
+ *
81
+ * function Root() {
82
+ * return (
83
+ * <ChannelProvider schemas={schemas}>
84
+ * <App />
85
+ * </ChannelProvider>
86
+ * );
87
+ * }
88
+ * ```
89
+ */
90
+ export declare function ChannelProvider<S extends SchemaMap>(props: ChannelProviderProps<S>): ReactNode;
91
+ /**
92
+ * Access the typed IPC channel from context.
93
+ *
94
+ * Must be called inside a {@link ChannelProvider}. Throws if the
95
+ * provider is missing.
96
+ *
97
+ * @example
98
+ * ```tsx
99
+ * import { useChannel } from "@nativewindow/react";
100
+ *
101
+ * type Events = { counter: number; title: string };
102
+ *
103
+ * function StatusBar() {
104
+ * const channel = useChannel<Events>();
105
+ * channel.send("counter", 1);
106
+ * }
107
+ * ```
108
+ */
109
+ export declare function useChannel<T extends EventMap = EventMap>(): TypedChannel<T>;
110
+ /**
111
+ * Subscribe to a specific IPC event type with automatic cleanup.
112
+ *
113
+ * The handler is stored in a ref to avoid re-subscribing when the
114
+ * handler function identity changes between renders. The subscription
115
+ * itself only re-runs when `type` changes.
116
+ *
117
+ * @example
118
+ * ```tsx
119
+ * import { useChannelEvent } from "@nativewindow/react";
120
+ *
121
+ * type Events = { title: string };
122
+ *
123
+ * function TitleDisplay() {
124
+ * useChannelEvent<Events, "title">("title", (title) => {
125
+ * document.title = title;
126
+ * });
127
+ * return null;
128
+ * }
129
+ * ```
130
+ */
131
+ export declare function useChannelEvent<T extends EventMap = EventMap, K extends keyof T & string = keyof T & string>(type: K, handler: (payload: T[K]) => void): void;
132
+ /**
133
+ * Returns a stable `send` function from the channel.
134
+ *
135
+ * A convenience wrapper around `useChannel().send`. The returned
136
+ * function has a stable identity (does not change between renders).
137
+ *
138
+ * @example
139
+ * ```tsx
140
+ * import { useSend } from "@nativewindow/react";
141
+ *
142
+ * type Events = { counter: number; title: string };
143
+ *
144
+ * function Counter() {
145
+ * const send = useSend<Events>();
146
+ * return <button onClick={() => send("counter", 1)}>Increment</button>;
147
+ * }
148
+ * ```
149
+ */
150
+ export declare function useSend<T extends EventMap = EventMap>(): <K extends keyof T & string>(...args: SendArgs<T, K>) => void;
151
+ /**
152
+ * Options for {@link createChannelHooks}.
153
+ *
154
+ * @example
155
+ * ```tsx
156
+ * createChannelHooks(schemas, {
157
+ * onValidationError: (type, payload) => console.warn(type, payload),
158
+ * });
159
+ * ```
160
+ */
161
+ export interface ChannelHooksOptions {
162
+ /**
163
+ * Called when an incoming payload fails schema validation.
164
+ * If not provided, failed payloads are silently dropped.
165
+ */
166
+ onValidationError?: ValidationErrorHandler;
167
+ }
168
+ /**
169
+ * The set of pre-typed React hooks and provider returned by
170
+ * {@link createChannelHooks}.
171
+ *
172
+ * All hooks are bound to the same internal context and typed to `T`,
173
+ * so event names and payload types are inferred automatically without
174
+ * requiring generic type parameters at the call site.
175
+ */
176
+ export interface TypedChannelHooks<T extends EventMap> {
177
+ /**
178
+ * Context provider that creates the channel client once.
179
+ * Wrap your React app with this at the root.
180
+ */
181
+ ChannelProvider: (props: {
182
+ children: ReactNode;
183
+ }) => ReactNode;
184
+ /** Access the typed channel from context. Throws if outside the provider. */
185
+ useChannel: () => TypedChannel<T>;
186
+ /** Subscribe to a typed event with automatic cleanup. */
187
+ useChannelEvent: <K extends keyof T & string>(type: K, handler: (payload: T[K]) => void) => void;
188
+ /** Returns a stable typed `send` function. */
189
+ useSend: () => <K extends keyof T & string>(...args: SendArgs<T, K>) => void;
190
+ }
191
+ /**
192
+ * Create a set of pre-typed React hooks for the IPC channel.
193
+ *
194
+ * Types are inferred from the `schemas` argument — no need to pass
195
+ * generic type parameters to individual hooks. Each call creates its
196
+ * own React context, so multiple independent channels are supported.
197
+ *
198
+ * @example
199
+ * ```tsx
200
+ * import { z } from "zod";
201
+ * import { createChannelHooks } from "@nativewindow/react";
202
+ *
203
+ * // Types are inferred: { counter: number; title: string }
204
+ * const { ChannelProvider, useChannel, useChannelEvent, useSend } =
205
+ * createChannelHooks({
206
+ * counter: z.number(),
207
+ * title: z.string(),
208
+ * });
209
+ *
210
+ * function App() {
211
+ * const send = useSend(); // fully typed
212
+ * useChannelEvent("title", (t) => { // t: string
213
+ * document.title = t;
214
+ * });
215
+ * return <button onClick={() => send("counter", 1)}>+1</button>;
216
+ * }
217
+ *
218
+ * function Root() {
219
+ * return (
220
+ * <ChannelProvider>
221
+ * <App />
222
+ * </ChannelProvider>
223
+ * );
224
+ * }
225
+ * ```
226
+ */
227
+ export declare function createChannelHooks<S extends SchemaMap>(schemas: S, options?: ChannelHooksOptions): TypedChannelHooks<InferSchemaMap<S>>;
package/dist/index.js ADDED
@@ -0,0 +1,99 @@
1
+ import { createContext, useRef, createElement, useContext, useEffect, useCallback } from "react";
2
+ import { createChannelClient } from "@nativewindow/ipc/client";
3
+ const ChannelContext = createContext(null);
4
+ function ChannelProvider(props) {
5
+ const { schemas, onValidationError, children } = props;
6
+ const channelRef = useRef(null);
7
+ if (channelRef.current === null) {
8
+ channelRef.current = createChannelClient({ schemas, onValidationError });
9
+ }
10
+ return createElement(ChannelContext.Provider, { value: channelRef.current }, children);
11
+ }
12
+ function useChannel() {
13
+ const channel = useContext(ChannelContext);
14
+ if (channel === null) {
15
+ throw new Error("useChannel() must be used inside a <ChannelProvider>.");
16
+ }
17
+ return channel;
18
+ }
19
+ function useChannelEvent(type, handler) {
20
+ const channel = useChannel();
21
+ const handlerRef = useRef(handler);
22
+ handlerRef.current = handler;
23
+ useEffect(() => {
24
+ const stableHandler = (payload) => {
25
+ handlerRef.current(payload);
26
+ };
27
+ channel.on(type, stableHandler);
28
+ return () => {
29
+ channel.off(type, stableHandler);
30
+ };
31
+ }, [channel, type]);
32
+ }
33
+ function useSend() {
34
+ const channel = useChannel();
35
+ return useCallback(
36
+ (...args) => {
37
+ channel.send(...args);
38
+ },
39
+ [channel]
40
+ );
41
+ }
42
+ function createChannelHooks(schemas, options) {
43
+ const HooksContext = createContext(null);
44
+ function HooksProvider(props) {
45
+ const channelRef = useRef(null);
46
+ if (channelRef.current === null) {
47
+ channelRef.current = createChannelClient({
48
+ schemas,
49
+ onValidationError: options?.onValidationError
50
+ });
51
+ }
52
+ return createElement(HooksContext.Provider, { value: channelRef.current }, props.children);
53
+ }
54
+ function hooks_useChannel() {
55
+ const channel = useContext(HooksContext);
56
+ if (channel === null) {
57
+ throw new Error(
58
+ "useChannel() must be used inside the <ChannelProvider> returned by createChannelHooks()."
59
+ );
60
+ }
61
+ return channel;
62
+ }
63
+ function hooks_useChannelEvent(type, handler) {
64
+ const channel = hooks_useChannel();
65
+ const handlerRef = useRef(handler);
66
+ handlerRef.current = handler;
67
+ useEffect(() => {
68
+ const stableHandler = (payload) => {
69
+ handlerRef.current(payload);
70
+ };
71
+ channel.on(type, stableHandler);
72
+ return () => {
73
+ channel.off(type, stableHandler);
74
+ };
75
+ }, [channel, type]);
76
+ }
77
+ function hooks_useSend() {
78
+ const channel = hooks_useChannel();
79
+ return useCallback(
80
+ (...args) => {
81
+ channel.send(...args);
82
+ },
83
+ [channel]
84
+ );
85
+ }
86
+ return {
87
+ ChannelProvider: HooksProvider,
88
+ useChannel: hooks_useChannel,
89
+ useChannelEvent: hooks_useChannelEvent,
90
+ useSend: hooks_useSend
91
+ };
92
+ }
93
+ export {
94
+ ChannelProvider,
95
+ createChannelHooks,
96
+ useChannel,
97
+ useChannelEvent,
98
+ useSend
99
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@nativewindow/react",
3
+ "version": "0.1.1",
4
+ "description": "React bindings for native-window IPC (alpha)",
5
+ "homepage": "https://nativewindow.fcannizzaro.com",
6
+ "bugs": {
7
+ "url": "https://github.com/nativewindow/webview/issues"
8
+ },
9
+ "license": "MIT",
10
+ "author": {
11
+ "name": "Francesco Saverio Cannizzaro (fcannizzaro)",
12
+ "url": "https://fcannizzaro.com"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/nativewindow/webview/tree/main/packages/react"
17
+ },
18
+ "funding": [
19
+ {
20
+ "type": "patreon",
21
+ "url": "https://www.patreon.com/fcannizzaro"
22
+ }
23
+ ],
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "type": "module",
30
+ "main": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js"
36
+ }
37
+ },
38
+ "scripts": {
39
+ "test": "vitest run",
40
+ "build": "vite build",
41
+ "typecheck": "tsc --noEmit"
42
+ },
43
+ "devDependencies": {
44
+ "@testing-library/react": "^16.3.0",
45
+ "@types/react": "^19.1.0",
46
+ "jsdom": "^28.1.0",
47
+ "react": "^19.1.0",
48
+ "react-dom": "^19.1.0",
49
+ "vite": "^7.3.1",
50
+ "vite-plugin-dts": "^4.5.4",
51
+ "vitest": "^4.0.18",
52
+ "zod": "^4.3.6"
53
+ },
54
+ "peerDependencies": {
55
+ "@nativewindow/ipc": "workspace:*",
56
+ "react": "^18.0.0 || ^19.0.0",
57
+ "typescript": "^5"
58
+ }
59
+ }