@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.
- package/README.md +10 -7
- package/dist/index.js +200 -88
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@nativewindow/react)
|
|
4
4
|
|
|
5
|
-
> [!
|
|
6
|
-
> This project is in **
|
|
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
|
-
|
|
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("
|
|
69
|
-
|
|
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,
|
|
1
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useRef } from "react";
|
|
2
2
|
import { createChannelClient } from "@nativewindow/ipc/client";
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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.
|
|
4
|
-
"description": "React bindings for native-window IPC (
|
|
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": "^
|
|
46
|
+
"jsdom": "^29.0.1",
|
|
47
47
|
"react": "^19.1.0",
|
|
48
48
|
"react-dom": "^19.1.0",
|
|
49
|
-
"vite": "^
|
|
49
|
+
"vite": "^8.0.3",
|
|
50
50
|
"vite-plugin-dts": "^4.5.4",
|
|
51
|
-
"vitest": "^4.
|
|
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": "^
|
|
57
|
+
"typescript": "^6.0.2"
|
|
58
58
|
}
|
|
59
59
|
}
|