Skip to content

Commit 1a78882

Browse files
authored
feat(react): add comlink fetch hook (#338)
1 parent 8c94c29 commit 1a78882

File tree

4 files changed

+144
-27
lines changed

4 files changed

+144
-27
lines changed

apps/kitchensink-react/src/Comlink/Framed.tsx

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import {type ComlinkStatus, useWindowConnection} from '@sanity/sdk-react'
22
import {Box, Button, Card, Container, Label, Stack, Text, TextInput} from '@sanity/ui'
3-
import {type ReactElement, useState} from 'react'
3+
import {type ReactElement, useEffect, useRef, useState} from 'react'
44

5-
import {FromIFrameMessage, ToIFrameMessage} from './types'
5+
import {FromIFrameMessage, ToIFrameMessage, UserData} from './types'
66

77
const Framed = (): ReactElement => {
8-
const [message, setMessage] = useState('')
98
const [status, setStatus] = useState<ComlinkStatus>('idle')
109
const [receivedMessages, setReceivedMessages] = useState<string[]>([])
10+
const [users, setUsers] = useState<UserData[]>([])
11+
const [error, setError] = useState<string | null>(null)
1112

12-
const {sendMessage} = useWindowConnection<FromIFrameMessage, ToIFrameMessage>({
13+
const messageInputRef = useRef<HTMLInputElement>(null)
14+
15+
const {sendMessage, fetch} = useWindowConnection<FromIFrameMessage, ToIFrameMessage>({
1316
name: 'frame',
1417
connectTo: 'main-app',
1518
onStatus: setStatus,
@@ -20,15 +23,44 @@ const Framed = (): ReactElement => {
2023
},
2124
})
2225

26+
// Fetch all users when connected
27+
useEffect(() => {
28+
if (!fetch || status !== 'connected') return
29+
30+
async function fetchUsers(signal: AbortSignal) {
31+
try {
32+
const data = await fetch<UserData[]>('FETCH_USERS', undefined, {signal})
33+
setUsers(data)
34+
setError(null)
35+
} catch (err) {
36+
if (err?.name !== 'AbortError') {
37+
setError('Failed to fetch users')
38+
}
39+
}
40+
}
41+
42+
const controller = new AbortController()
43+
fetchUsers(controller.signal)
44+
45+
return () => {
46+
controller.abort()
47+
}
48+
}, [fetch, status])
49+
2350
const sendMessageToParent = () => {
51+
const message = messageInputRef.current?.value || ''
2452
if (message.trim()) {
2553
sendMessage('FROM_IFRAME', {message})
26-
setMessage('')
54+
if (messageInputRef.current) {
55+
messageInputRef.current.value = ''
56+
}
2757
}
2858
}
2959

60+
const isConnected = status === 'connected'
61+
3062
return (
31-
<Container height={'fill'}>
63+
<Container height="fill">
3264
<Card tone="transparent">
3365
<Stack padding={4} space={4}>
3466
<Text weight="semibold" size={2}>
@@ -41,21 +73,49 @@ const Framed = (): ReactElement => {
4173
<Box display="flex">
4274
<Box flex={1}>
4375
<TextInput
44-
value={message}
45-
onChange={(event) => setMessage(event.currentTarget.value)}
76+
ref={messageInputRef}
4677
onKeyDown={(e) => e.key === 'Enter' && sendMessageToParent()}
47-
disabled={status !== 'connected'}
78+
disabled={!isConnected}
4879
/>
4980
</Box>
5081
<Button
5182
text="Send"
5283
tone="primary"
5384
onClick={sendMessageToParent}
54-
disabled={status !== 'connected'}
85+
disabled={!isConnected}
5586
/>
5687
</Box>
5788
</Stack>
5889

90+
{/* Users section */}
91+
<Card padding={3} border radius={2}>
92+
<Stack space={3}>
93+
<Label>Users</Label>
94+
{users.length > 0 ? (
95+
<Stack space={2}>
96+
{users.map((user) => (
97+
<Card key={user.id} padding={3} tone="positive" radius={2}>
98+
<Stack space={2}>
99+
<Text size={1} weight="semibold">
100+
{user.name}
101+
</Text>
102+
<Text size={1}>{user.email}</Text>
103+
</Stack>
104+
</Card>
105+
))}
106+
</Stack>
107+
) : error ? (
108+
<Card padding={3} tone="critical" radius={2}>
109+
<Text size={1}>{error}</Text>
110+
</Card>
111+
) : (
112+
<Card padding={3} tone="default" radius={2}>
113+
<Text size={1}>Loading users...</Text>
114+
</Card>
115+
)}
116+
</Stack>
117+
</Card>
118+
59119
{/* Received messages */}
60120
<Box flex={1} style={{height: '500px'}}>
61121
<Stack space={3}>

apps/kitchensink-react/src/Comlink/ParentApp.tsx

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,57 @@ import {Box, Button, Card, Label, Stack, Text, TextInput} from '@sanity/ui'
33
import {ReactElement, useEffect, useRef, useState} from 'react'
44

55
import {PageLayout} from '../components/PageLayout'
6-
import {FromIFrameMessage, ToIFrameMessage} from './types'
6+
import {FetchUsersRequest, FromIFrameMessage, ToIFrameMessage, UserData} from './types'
7+
8+
// Add this mock data
9+
const MOCK_USERS: Record<string, UserData> = {
10+
1: {id: '1', name: 'Alice Johnson', email: '[email protected]'},
11+
2: {id: '2', name: 'Bob Smith', email: '[email protected]'},
12+
3: {id: '3', name: 'Carol Williams', email: '[email protected]'},
13+
}
714

815
const ParentApp = (): ReactElement => {
916
const [selectedFrame, setSelectedFrame] = useState<number>(1)
10-
const [message, setMessage] = useState('')
1117
const [status, setStatus] = useState<ComlinkStatus>('idle')
1218
const [receivedMessages, setReceivedMessages] = useState<Array<{from: string; message: string}>>(
1319
[],
1420
)
21+
22+
const messageInputRef = useRef<HTMLInputElement>(null)
1523
const iframeRef = useRef<HTMLIFrameElement>(null)
1624

17-
const {sendMessage, connect} = useFrameConnection<ToIFrameMessage, FromIFrameMessage>({
25+
const {sendMessage, connect} = useFrameConnection<
26+
ToIFrameMessage,
27+
FromIFrameMessage | FetchUsersRequest
28+
>({
1829
name: 'main-app',
1930
connectTo: 'frame',
2031
targetOrigin: '*',
2132
onStatus: setStatus,
33+
heartbeat: true,
2234
onMessage: {
2335
FROM_IFRAME: (data: {message: string}) => {
2436
setReceivedMessages((prev) => [
2537
...prev,
2638
{from: `Frame ${selectedFrame}`, message: data.message},
2739
])
2840
},
41+
FETCH_USERS: () => {
42+
return Object.values(MOCK_USERS)
43+
},
2944
},
3045
})
3146

47+
const sendMessageToFramedApp = () => {
48+
const message = messageInputRef.current?.value || ''
49+
if (message.trim()) {
50+
sendMessage('TO_IFRAME', {message})
51+
if (messageInputRef.current) {
52+
messageInputRef.current.value = ''
53+
}
54+
}
55+
}
56+
3257
useEffect(() => {
3358
let cleanupIframeConnection: (() => void) | undefined
3459

@@ -57,19 +82,12 @@ const ParentApp = (): ReactElement => {
5782
}
5883
}, [connect, selectedFrame])
5984

60-
const sendMessageToFramedApp = () => {
61-
if (message.trim()) {
62-
sendMessage('TO_IFRAME', {message})
63-
setMessage('')
64-
}
65-
}
66-
6785
const frames = [1, 2, 3]
6886

6987
return (
7088
<PageLayout
7189
title="Comlink demo"
72-
subtitle="Explore comlink connections"
90+
subtitle="Explore comlink connections and fetch operations"
7391
homePath="/comlink-demo"
7492
homeText="Comlink Demo Home"
7593
>
@@ -107,9 +125,8 @@ const ParentApp = (): ReactElement => {
107125
<Box display="flex">
108126
<Box flex={1}>
109127
<TextInput
110-
value={message}
111-
onChange={(event) => setMessage(event.currentTarget.value)}
112-
onKeyPress={(e) => e.key === 'Enter' && sendMessageToFramedApp()}
128+
ref={messageInputRef}
129+
onKeyDown={(e) => e.key === 'Enter' && sendMessageToFramedApp()}
113130
disabled={status !== 'connected'}
114131
/>
115132
</Box>
Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1-
export type ToIFrameMessage = {
1+
export interface UserData {
2+
id: string
3+
name: string
4+
email: string
5+
}
6+
7+
export interface ToIFrameMessage {
28
type: 'TO_IFRAME'
39
data: {message: string}
410
}
511

6-
export type FromIFrameMessage = {
12+
export interface FromIFrameMessage {
713
type: 'FROM_IFRAME'
814
data: {message: string}
915
}
16+
17+
export interface FetchUsersRequest {
18+
type: 'FETCH_USERS'
19+
}

packages/react/src/hooks/comlink/useWindowConnection.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {type Node, type Status} from '@sanity/comlink'
1+
import {type MessageData, type Node, type Status} from '@sanity/comlink'
22
import {type FrameMessage, getOrCreateNode, releaseNode, type WindowMessage} from '@sanity/sdk'
33
import {useCallback, useEffect, useRef} from 'react'
44

@@ -29,10 +29,25 @@ export interface WindowConnection<TMessage extends WindowMessage> {
2929
type: TType,
3030
data?: Extract<TMessage, {type: TType}>['data'],
3131
) => void
32+
fetch: <TResponse>(
33+
type: string,
34+
data?: MessageData,
35+
options?: {
36+
signal?: AbortSignal
37+
suppressWarnings?: boolean
38+
responseTimeout?: number
39+
},
40+
) => Promise<TResponse>
3241
}
3342

3443
/**
3544
* @internal
45+
* Hook to wrap a Comlink node in a React hook.
46+
* Our store functionality takes care of the lifecycle of the node,
47+
* as well as sharing a single node between invocations if they share the same name.
48+
*
49+
* Generally not to be used directly, but to be used as a dependency of
50+
* Comlink-powered hooks like `useManageFavorite`.
3651
*/
3752
export function useWindowConnection<
3853
TWindowMessage extends WindowMessage,
@@ -86,7 +101,22 @@ export function useWindowConnection<
86101
[],
87102
)
88103

104+
const fetch = useCallback(
105+
<TResponse>(
106+
type: string,
107+
data?: MessageData,
108+
fetchOptions?: {
109+
responseTimeout?: number
110+
signal?: AbortSignal
111+
suppressWarnings?: boolean
112+
},
113+
): Promise<TResponse> => {
114+
return nodeRef.current?.fetch(type, data, fetchOptions ?? {}) as Promise<TResponse>
115+
},
116+
[],
117+
)
89118
return {
90119
sendMessage,
120+
fetch,
91121
}
92122
}

0 commit comments

Comments
 (0)