Skip to content

Commit 4a0b763

Browse files
authored
feat(react): use refs for nodes and channels (#341)
1 parent 401468e commit 4a0b763

File tree

9 files changed

+130
-124
lines changed

9 files changed

+130
-124
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
import {useWindowConnection} from '@sanity/sdk-react'
1+
import {type ComlinkStatus, useWindowConnection} from '@sanity/sdk-react'
22
import {Box, Button, Card, Container, Label, Stack, Text, TextInput} from '@sanity/ui'
33
import {type ReactElement, useState} from 'react'
44

55
import {FromIFrameMessage, ToIFrameMessage} from './types'
66

77
const Framed = (): ReactElement => {
88
const [message, setMessage] = useState('')
9+
const [status, setStatus] = useState<ComlinkStatus>('idle')
910
const [receivedMessages, setReceivedMessages] = useState<string[]>([])
1011

11-
const {sendMessage, status} = useWindowConnection<FromIFrameMessage, ToIFrameMessage>({
12+
const {sendMessage} = useWindowConnection<FromIFrameMessage, ToIFrameMessage>({
1213
name: 'frame',
1314
connectTo: 'main-app',
15+
onStatus: setStatus,
1416
onMessage: {
1517
TO_IFRAME: (data: {message: string}) => {
1618
setReceivedMessages((prev) => [...prev, data.message])

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useFrameConnection} from '@sanity/sdk-react'
1+
import {type ComlinkStatus, useFrameConnection} from '@sanity/sdk-react'
22
import {Box, Button, Card, Label, Stack, Text, TextInput} from '@sanity/ui'
33
import {ReactElement, useEffect, useRef, useState} from 'react'
44

@@ -8,15 +8,17 @@ import {FromIFrameMessage, ToIFrameMessage} from './types'
88
const ParentApp = (): ReactElement => {
99
const [selectedFrame, setSelectedFrame] = useState<number>(1)
1010
const [message, setMessage] = useState('')
11+
const [status, setStatus] = useState<ComlinkStatus>('idle')
1112
const [receivedMessages, setReceivedMessages] = useState<Array<{from: string; message: string}>>(
1213
[],
1314
)
1415
const iframeRef = useRef<HTMLIFrameElement>(null)
1516

16-
const {sendMessage, connect, status} = useFrameConnection<ToIFrameMessage, FromIFrameMessage>({
17+
const {sendMessage, connect} = useFrameConnection<ToIFrameMessage, FromIFrameMessage>({
1718
name: 'main-app',
1819
connectTo: 'frame',
1920
targetOrigin: '*',
21+
onStatus: setStatus,
2022
onMessage: {
2123
FROM_IFRAME: (data: {message: string}) => {
2224
setReceivedMessages((prev) => [

packages/react/src/_exports/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,6 @@ export {useQuery} from '../hooks/query/useQuery'
5959
export {useUsers, type UseUsersParams, type UseUsersResult} from '../hooks/users/useUsers'
6060
export {REACT_SDK_VERSION} from '../version'
6161
export {type DatasetsResponse, type SanityProject, type SanityProjectMember} from '@sanity/client'
62+
export {type Status as ComlinkStatus} from '@sanity/comlink'
6263
export {type CurrentUser, type DocumentHandle} from '@sanity/sdk'
6364
export {type SanityDocument, type SortOrderingItem} from '@sanity/types'

packages/react/src/hooks/comlink/useFrameConnection.test.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,32 +61,42 @@ describe('useFrameController', () => {
6161
vi.mocked(getOrCreateController).mockReturnValue(controller)
6262
})
6363

64-
it('should initialize with idle status', () => {
65-
const {result} = renderHook(() =>
64+
it('should call onStatus callback when status changes', () => {
65+
const onStatusMock = vi.fn()
66+
renderHook(() =>
6667
useFrameConnection({
6768
name: 'test',
6869
connectTo: 'iframe',
6970
targetOrigin: '*',
71+
onStatus: onStatusMock,
7072
}),
7173
)
7274

73-
expect(result.current.status).toBe('idle')
75+
act(() => {
76+
statusCallback?.({status: 'connected', connection: 'test'})
77+
})
78+
expect(onStatusMock).toHaveBeenCalledWith('connected')
79+
80+
act(() => {
81+
statusCallback?.({status: 'disconnected', connection: 'test'})
82+
})
83+
expect(onStatusMock).toHaveBeenCalledWith('disconnected')
7484
})
7585

76-
it('should update status to connected when node connects', async () => {
77-
const {result} = renderHook(() =>
86+
it('should not throw if onStatus is not provided', () => {
87+
renderHook(() =>
7888
useFrameConnection({
7989
name: 'test',
8090
connectTo: 'iframe',
8191
targetOrigin: '*',
8292
}),
8393
)
8494

85-
expect(result.current.status).toBe('idle')
86-
act(() => {
87-
statusCallback?.({status: 'connected', connection: 'test'})
88-
})
89-
expect(result.current.status).toBe('connected')
95+
expect(() => {
96+
act(() => {
97+
statusCallback?.({status: 'connected', connection: 'test'})
98+
})
99+
}).not.toThrow()
90100
})
91101

92102
it('should register and execute message handlers', () => {
Lines changed: 33 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import {type Status} from '@sanity/comlink'
1+
import {type ChannelInstance, type Controller, type Status} from '@sanity/comlink'
22
import {
33
type FrameMessage,
44
getOrCreateChannel,
55
getOrCreateController,
66
releaseChannel,
77
type WindowMessage,
88
} from '@sanity/sdk'
9-
import {useCallback, useEffect, useMemo, useState} from 'react'
9+
import {useCallback, useEffect, useRef} from 'react'
1010

1111
import {useSanityInstance} from '../context/useSanityInstance'
1212

@@ -28,6 +28,7 @@ export interface UseFrameConnectionOptions<TWindowMessage extends WindowMessage>
2828
[K in TWindowMessage['type']]: (data: Extract<TWindowMessage, {type: K}>['data']) => void
2929
}
3030
heartbeat?: boolean
31+
onStatus?: (status: Status) => void
3132
}
3233

3334
/**
@@ -40,7 +41,6 @@ export interface FrameConnection<TFrameMessage extends FrameMessage> {
4041
? [type: T]
4142
: [type: T, data: Extract<TFrameMessage, {type: T}>['data']]
4243
) => void
43-
status: Status
4444
}
4545

4646
/**
@@ -50,81 +50,58 @@ export function useFrameConnection<
5050
TFrameMessage extends FrameMessage,
5151
TWindowMessage extends WindowMessage,
5252
>(options: UseFrameConnectionOptions<TWindowMessage>): FrameConnection<TFrameMessage> {
53-
const {onMessage, targetOrigin, name, connectTo, heartbeat} = options
53+
const {onMessage, targetOrigin, name, connectTo, heartbeat, onStatus} = options
5454
const instance = useSanityInstance()
55-
const [status, setStatus] = useState<Status>('idle')
56-
57-
const controller = useMemo(
58-
() => getOrCreateController(instance, targetOrigin),
59-
[instance, targetOrigin],
60-
)
61-
62-
const channel = useMemo(
63-
() =>
64-
getOrCreateChannel(instance, {
65-
name,
66-
connectTo,
67-
heartbeat,
68-
}),
69-
[instance, name, connectTo, heartbeat],
70-
)
55+
const controllerRef = useRef<Controller | null>(null)
56+
const channelRef = useRef<ChannelInstance<TFrameMessage, TWindowMessage> | null>(null)
7157

7258
useEffect(() => {
73-
if (!channel) return
59+
const controller = getOrCreateController(instance, targetOrigin)
60+
const channel = getOrCreateChannel(instance, {name, connectTo, heartbeat})
61+
controllerRef.current = controller
62+
channelRef.current = channel
7463

75-
const unsubscribe = channel.onStatus((event) => {
76-
setStatus(event.status)
64+
channel.onStatus((event) => {
65+
onStatus?.(event.status)
7766
})
7867

79-
return unsubscribe
80-
}, [channel])
81-
82-
useEffect(() => {
83-
if (!channel || !onMessage) return
84-
85-
const unsubscribers: Array<() => void> = []
68+
const messageUnsubscribers: Array<() => void> = []
8669

87-
Object.entries(onMessage).forEach(([type, handler]) => {
88-
// type assertion, but we've already constrained onMessage to have the correct handler type
89-
const unsubscribe = channel.on(type, handler as FrameMessageHandler<TWindowMessage>)
90-
unsubscribers.push(unsubscribe)
91-
})
70+
if (onMessage) {
71+
Object.entries(onMessage).forEach(([type, handler]) => {
72+
const unsubscribe = channel.on(type, handler as FrameMessageHandler<TWindowMessage>)
73+
messageUnsubscribers.push(unsubscribe)
74+
})
75+
}
9276

9377
return () => {
94-
unsubscribers.forEach((unsub) => unsub())
78+
// Clean up all subscriptions and stop controller/channel
79+
messageUnsubscribers.forEach((unsub) => unsub())
80+
releaseChannel(instance, name)
81+
channelRef.current = null
82+
controllerRef.current = null
9583
}
96-
}, [channel, onMessage])
84+
}, [targetOrigin, name, connectTo, heartbeat, onMessage, instance, onStatus])
9785

98-
const connect = useCallback(
99-
(frameWindow: Window) => {
100-
const removeTarget = controller?.addTarget(frameWindow)
101-
return () => {
102-
removeTarget?.()
103-
}
104-
},
105-
[controller],
106-
)
86+
const connect = useCallback((frameWindow: Window) => {
87+
const removeTarget = controllerRef.current?.addTarget(frameWindow)
88+
return () => {
89+
removeTarget?.()
90+
}
91+
}, [])
10792

10893
const sendMessage = useCallback(
10994
<T extends TFrameMessage['type']>(
11095
type: T,
11196
data?: Extract<TFrameMessage, {type: T}>['data'],
11297
) => {
113-
channel?.post(type, data)
98+
channelRef.current?.post(type, data)
11499
},
115-
[channel],
100+
[],
116101
)
117102

118-
// cleanup channel on unmount
119-
useEffect(() => {
120-
return () => {
121-
releaseChannel(instance, name)
122-
}
123-
}, [name, instance])
124-
125103
return {
126104
connect,
127105
sendMessage,
128-
status,
129106
}
130107
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {type Status} from '@sanity/comlink'
12
import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
23
import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
34
import {useCallback, useState} from 'react'
@@ -48,9 +49,11 @@ interface ManageFavorite {
4849
*/
4950
export function useManageFavorite({_id, _type}: DocumentHandle): ManageFavorite {
5051
const [isFavorited, setIsFavorited] = useState(false) // should load this from a comlink fetch
51-
const {sendMessage, status} = useWindowConnection<Events.FavoriteMessage, FrameMessage>({
52+
const [status, setStatus] = useState<Status>('idle')
53+
const {sendMessage} = useWindowConnection<Events.FavoriteMessage, FrameMessage>({
5254
name: SDK_NODE_NAME,
5355
connectTo: SDK_CHANNEL_NAME,
56+
onStatus: setStatus,
5457
})
5558

5659
const handleFavoriteAction = useCallback(

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import {type Status} from '@sanity/comlink'
12
import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
23
import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
3-
import {useCallback} from 'react'
4+
import {useCallback, useState} from 'react'
45

56
import {useWindowConnection} from './useWindowConnection'
67

@@ -42,9 +43,11 @@ export function useRecordDocumentHistoryEvent({
4243
_id,
4344
_type,
4445
}: DocumentHandle): DocumentInteractionHistory {
45-
const {sendMessage, status} = useWindowConnection<Events.HistoryMessage, FrameMessage>({
46+
const [status, setStatus] = useState<Status>('idle')
47+
const {sendMessage} = useWindowConnection<Events.HistoryMessage, FrameMessage>({
4648
name: SDK_NODE_NAME,
4749
connectTo: SDK_CHANNEL_NAME,
50+
onStatus: setStatus,
4851
})
4952

5053
const recordEvent = useCallback(

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

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,30 +49,40 @@ describe('useWindowConnection', () => {
4949
vi.mocked(getOrCreateNode).mockReturnValue(node as unknown as Node<Message, Message>)
5050
})
5151

52-
it('should initialize with idle status', () => {
53-
const {result} = renderHook(() =>
52+
it('should call onStatus callback when status changes', () => {
53+
const onStatusMock = vi.fn()
54+
renderHook(() =>
5455
useWindowConnection<TestMessages, TestMessages>({
5556
name: 'test',
5657
connectTo: 'window',
58+
onStatus: onStatusMock,
5759
}),
5860
)
5961

60-
expect(result.current.status).toBe('idle')
62+
act(() => {
63+
statusCallback?.('connected')
64+
})
65+
expect(onStatusMock).toHaveBeenCalledWith('connected')
66+
67+
act(() => {
68+
statusCallback?.('disconnected')
69+
})
70+
expect(onStatusMock).toHaveBeenCalledWith('disconnected')
6171
})
6272

63-
it('should update status to connected when node connects', async () => {
64-
const {result} = renderHook(() =>
73+
it('should not throw if onStatus is not provided', () => {
74+
renderHook(() =>
6575
useWindowConnection<TestMessages, TestMessages>({
6676
name: 'test',
6777
connectTo: 'window',
6878
}),
6979
)
7080

71-
expect(result.current.status).toBe('idle')
72-
act(() => {
73-
statusCallback?.('connected')
74-
})
75-
expect(result.current.status).toBe('connected')
81+
expect(() => {
82+
act(() => {
83+
statusCallback?.('connected')
84+
})
85+
}).not.toThrow()
7686
})
7787

7888
it('should register message handlers', () => {

0 commit comments

Comments
 (0)