@nativewindow/react 1.0.0 → 1.0.2

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.
Files changed (3) hide show
  1. package/README.md +10 -7
  2. package/dist/index.js +200 -88
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@nativewindow/react)](https://www.npmjs.com/package/@nativewindow/react)
4
4
 
5
- > [!WARNING]
6
- > This project is in **alpha**. APIs may change without notice.
5
+ > [!NOTE]
6
+ > This project is in **beta**. APIs may change without notice.
7
7
 
8
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
9
 
@@ -32,10 +32,12 @@ import { z } from "zod";
32
32
  import { createChannelHooks } from "@nativewindow/react";
33
33
 
34
34
  export const { ChannelProvider, useChannel, useChannelEvent, useSend } = createChannelHooks({
35
- schemas: {
36
- counter: z.number(),
35
+ host: {
37
36
  "update-title": z.string(),
38
37
  },
38
+ client: {
39
+ counter: z.number(),
40
+ },
39
41
  });
40
42
  ```
41
43
 
@@ -64,11 +66,12 @@ function Counter() {
64
66
  const [count, setCount] = useState(0);
65
67
  const send = useSend();
66
68
 
67
- // Subscribe to events with automatic cleanup
68
- useChannelEvent("counter", (n) => {
69
- setCount(n);
69
+ // Subscribe to host events with automatic cleanup
70
+ useChannelEvent("update-title", (title) => {
71
+ document.title = title;
70
72
  });
71
73
 
74
+ // Send client events to the host
72
75
  return <button onClick={() => send("counter", count + 1)}>Count: {count}</button>;
73
76
  }
74
77
  ```
package/dist/index.js CHANGED
@@ -1,99 +1,211 @@
1
- import { createContext, useRef, createElement, useContext, useEffect, useCallback } from "react";
1
+ import { createContext, createElement, useCallback, useContext, useEffect, useRef } from "react";
2
2
  import { createChannelClient } from "@nativewindow/ipc/client";
3
- const ChannelContext = createContext(null);
3
+ //#region index.ts
4
+ /** @internal React context holding the channel instance. */
5
+ var ChannelContext = createContext(null);
6
+ /**
7
+ * Provides a typed IPC channel to the React tree.
8
+ *
9
+ * Creates the channel client exactly once (on initial mount) via
10
+ * `createChannelClient` from `@nativewindow/ipc/client`.
11
+ * The channel instance is stable for the lifetime of the provider.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { z } from "zod";
16
+ * import { ChannelProvider } from "@nativewindow/react";
17
+ *
18
+ * const schemas = {
19
+ * host: { title: z.string() },
20
+ * client: { counter: z.number() },
21
+ * };
22
+ *
23
+ * function Root() {
24
+ * return (
25
+ * <ChannelProvider schemas={schemas}>
26
+ * <App />
27
+ * </ChannelProvider>
28
+ * );
29
+ * }
30
+ * ```
31
+ */
4
32
  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);
33
+ const { schemas, onValidationError, children } = props;
34
+ const channelRef = useRef(null);
35
+ if (channelRef.current === null) channelRef.current = createChannelClient({
36
+ schemas,
37
+ onValidationError
38
+ });
39
+ return createElement(ChannelContext.Provider, { value: channelRef.current }, children);
11
40
  }
41
+ /**
42
+ * Access the typed IPC channel from context.
43
+ *
44
+ * Must be called inside a {@link ChannelProvider}. Throws if the
45
+ * provider is missing.
46
+ *
47
+ * @typeParam Send - Events this side can send (client events).
48
+ * @typeParam Receive - Events this side can receive (host events).
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * import { useChannel } from "@nativewindow/react";
53
+ *
54
+ * type ClientEvents = { counter: number };
55
+ * type HostEvents = { title: string };
56
+ *
57
+ * function StatusBar() {
58
+ * const channel = useChannel<ClientEvents, HostEvents>();
59
+ * channel.send("counter", 1);
60
+ * }
61
+ * ```
62
+ */
12
63
  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;
64
+ const channel = useContext(ChannelContext);
65
+ if (channel === null) throw new Error("useChannel() must be used inside a <ChannelProvider>.");
66
+ return channel;
18
67
  }
68
+ /**
69
+ * Subscribe to a specific incoming IPC event type with automatic cleanup.
70
+ *
71
+ * The handler is stored in a ref to avoid re-subscribing when the
72
+ * handler function identity changes between renders. The subscription
73
+ * itself only re-runs when `type` changes.
74
+ *
75
+ * @typeParam Receive - The event map for incoming (receivable) events.
76
+ * @typeParam K - The specific event key to subscribe to.
77
+ *
78
+ * @example
79
+ * ```tsx
80
+ * import { useChannelEvent } from "@nativewindow/react";
81
+ *
82
+ * type HostEvents = { title: string };
83
+ *
84
+ * function TitleDisplay() {
85
+ * useChannelEvent<HostEvents, "title">("title", (title) => {
86
+ * document.title = title;
87
+ * });
88
+ * return null;
89
+ * }
90
+ * ```
91
+ */
19
92
  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]);
93
+ const channel = useChannel();
94
+ const handlerRef = useRef(handler);
95
+ handlerRef.current = handler;
96
+ useEffect(() => {
97
+ const stableHandler = (payload) => {
98
+ handlerRef.current(payload);
99
+ };
100
+ channel.on(type, stableHandler);
101
+ return () => {
102
+ channel.off(type, stableHandler);
103
+ };
104
+ }, [channel, type]);
32
105
  }
106
+ /**
107
+ * Returns a stable `send` function from the channel.
108
+ *
109
+ * A convenience wrapper around `useChannel().send`. The returned
110
+ * function has a stable identity (does not change between renders).
111
+ *
112
+ * @typeParam Send - The event map for outgoing (sendable) events.
113
+ *
114
+ * @example
115
+ * ```tsx
116
+ * import { useSend } from "@nativewindow/react";
117
+ *
118
+ * type ClientEvents = { counter: number };
119
+ *
120
+ * function Counter() {
121
+ * const send = useSend<ClientEvents>();
122
+ * return <button onClick={() => send("counter", 1)}>Increment</button>;
123
+ * }
124
+ * ```
125
+ */
33
126
  function useSend() {
34
- const channel = useChannel();
35
- return useCallback(
36
- (...args) => {
37
- channel.send(...args);
38
- },
39
- [channel]
40
- );
127
+ const channel = useChannel();
128
+ return useCallback((...args) => {
129
+ channel.send(...args);
130
+ }, [channel]);
41
131
  }
132
+ /**
133
+ * Create a set of pre-typed React hooks for the IPC channel.
134
+ *
135
+ * Types are inferred from the `schemas` argument — no need to pass
136
+ * generic type parameters to individual hooks. Each call creates its
137
+ * own React context, so multiple independent channels are supported.
138
+ *
139
+ * @example
140
+ * ```tsx
141
+ * import { z } from "zod";
142
+ * import { createChannelHooks } from "@nativewindow/react";
143
+ *
144
+ * // Types are inferred from directional schemas
145
+ * const { ChannelProvider, useChannel, useChannelEvent, useSend } =
146
+ * createChannelHooks({
147
+ * host: { title: z.string() },
148
+ * client: { counter: z.number() },
149
+ * });
150
+ *
151
+ * function App() {
152
+ * const send = useSend(); // fully typed (client events)
153
+ * useChannelEvent("title", (t) => { // t: string (host events)
154
+ * document.title = t;
155
+ * });
156
+ * return <button onClick={() => send("counter", 1)}>+1</button>;
157
+ * }
158
+ *
159
+ * function Root() {
160
+ * return (
161
+ * <ChannelProvider>
162
+ * <App />
163
+ * </ChannelProvider>
164
+ * );
165
+ * }
166
+ * ```
167
+ */
42
168
  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
- };
169
+ const HooksContext = createContext(null);
170
+ function HooksProvider(props) {
171
+ const channelRef = useRef(null);
172
+ if (channelRef.current === null) channelRef.current = createChannelClient({
173
+ schemas,
174
+ onValidationError: options?.onValidationError
175
+ });
176
+ return createElement(HooksContext.Provider, { value: channelRef.current }, props.children);
177
+ }
178
+ function hooks_useChannel() {
179
+ const channel = useContext(HooksContext);
180
+ if (channel === null) throw new Error("useChannel() must be used inside the <ChannelProvider> returned by createChannelHooks().");
181
+ return channel;
182
+ }
183
+ function hooks_useChannelEvent(type, handler) {
184
+ const channel = hooks_useChannel();
185
+ const handlerRef = useRef(handler);
186
+ handlerRef.current = handler;
187
+ useEffect(() => {
188
+ const stableHandler = (payload) => {
189
+ handlerRef.current(payload);
190
+ };
191
+ channel.on(type, stableHandler);
192
+ return () => {
193
+ channel.off(type, stableHandler);
194
+ };
195
+ }, [channel, type]);
196
+ }
197
+ function hooks_useSend() {
198
+ const channel = hooks_useChannel();
199
+ return useCallback((...args) => {
200
+ channel.send(...args);
201
+ }, [channel]);
202
+ }
203
+ return {
204
+ ChannelProvider: HooksProvider,
205
+ useChannel: hooks_useChannel,
206
+ useChannelEvent: hooks_useChannelEvent,
207
+ useSend: hooks_useSend
208
+ };
92
209
  }
93
- export {
94
- ChannelProvider,
95
- createChannelHooks,
96
- useChannel,
97
- useChannelEvent,
98
- useSend
99
- };
210
+ //#endregion
211
+ export { ChannelProvider, createChannelHooks, useChannel, useChannelEvent, useSend };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nativewindow/react",
3
- "version": "1.0.0",
4
- "description": "React bindings for native-window IPC (alpha)",
3
+ "version": "1.0.2",
4
+ "description": "React bindings for native-window IPC (beta)",
5
5
  "homepage": "https://nativewindow.fcannizzaro.com",
6
6
  "bugs": {
7
7
  "url": "https://github.com/nativewindow/webview/issues"
@@ -43,17 +43,17 @@
43
43
  "devDependencies": {
44
44
  "@testing-library/react": "^16.3.0",
45
45
  "@types/react": "^19.1.0",
46
- "jsdom": "^28.1.0",
46
+ "jsdom": "^29.0.1",
47
47
  "react": "^19.1.0",
48
48
  "react-dom": "^19.1.0",
49
- "vite": "^7.3.1",
49
+ "vite": "^8.0.3",
50
50
  "vite-plugin-dts": "^4.5.4",
51
- "vitest": "^4.0.18",
51
+ "vitest": "^4.1.2",
52
52
  "zod": "^4.3.6"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@nativewindow/ipc": "workspace:*",
56
56
  "react": "^18.0.0 || ^19.0.0",
57
- "typescript": "^5"
57
+ "typescript": "^6.0.2"
58
58
  }
59
59
  }