typescript_channels.md (3948B)
1 # TypeScript Channels Pattern 2 3 ## What It Does 4 5 `createChannel` creates a pub-sub mechanism that links a websocket listener to a 6 component, allowing updates triggered by network events to propagate to the UI 7 without tight coupling between the two. 8 9 ## The Implementation 10 11 ```typescript 12 /** 13 * Creates a channel/link between a websocket listener and a component. 14 * This allows you to keep listeners separate from components but update 15 * a component's rune whenever a listener event fires. 16 * 17 * This is good for simply linkages between a websocket event and a page 18 * update or rerender. 19 */ 20 export type Unsubscribe = () => void; 21 22 export type Channel<T> = { 23 subscribe(fn: (v: T) => void): Unsubscribe; 24 next(value: T): void; 25 }; 26 27 export function createChannel<T>(): Channel<T> { 28 const subs = new Set<(v: T) => void>(); 29 return { 30 subscribe(fn: (v: T) => void): Unsubscribe { 31 subs.add(fn); 32 return () => subs.delete(fn); 33 }, 34 next(value: T): void { 35 for (const fn of subs) fn(value); 36 }, 37 }; 38 } 39 ``` 40 41 ## How It's Used 42 43 ### 1. Create a channel with your data type 44 45 ```typescript 46 // Create a channel for tracking websocket messages 47 const messageChannel = createChannel<Message>(); 48 49 // Create a channel for connection status 50 const connectionChannel = createChannel<ConnectionStatus>(); 51 ``` 52 53 ### 2. Subscribe in a React component 54 55 ```typescript 56 function MessageDisplay() { 57 const [messages, setMessages] = useState<Message[]>([]); 58 59 const unsubscribe = messageChannel.subscribe((msg) => { 60 setMessages((prev) => [...prev, msg]); 61 }); 62 63 // Cleanup on unmount - Note the inner function! 64 // useEffect accepts an effect function. If that function returns another function, 65 // React treats the inner function as the cleanup for that effect. 66 // • Mount: React runs the outer function (the effect body). 67 // • Unmount: React runs the returned function (the cleanup). 68 useEffect(() => () => unsubscribe(), []); 69 // Equivalent, more explicit: the *effect* returns a *cleanup* function; React runs it on unmount 70 // (and before re-running the effect if deps change — here `[]` means mount once, cleanup on unmount). 71 // useEffect(() => { 72 // return () => { 73 // unsubscribe(); 74 // }; 75 // }, []); 76 77 return <div>{messages.map((m) => <div key={m.id}>{m.text}</div>)}</div>; 78 } 79 ``` 80 81 ### 3. Emit from your websocket handler 82 83 ```typescript 84 // WebSocket connection established 85 ws.onopen = () => { 86 connectionChannel.next({ type: "connected" }); 87 }; 88 89 // New message received 90 ws.onmessage = (event) => { 91 const msg = JSON.parse(event.data); 92 messageChannel.next(msg); 93 }; 94 95 // Connection closed 96 ws.onclose = () => { 97 connectionChannel.next({ type: "disconnected" }); 98 }; 99 ``` 100 101 ### 4. Benefits 102 103 **Decoupling** 104 105 - WebSocket logic knows nothing about React components 106 - Components don't need to import or know about the websocket 107 - Easy to swap implementations (e.g., change backend without touching UI) 108 109 **Testability** 110 111 - Components test with a mock channel instead of real websocket 112 - Handlers test without starting a server 113 114 **Reusability** 115 116 - Same channel used by multiple components 117 - Same handler used with different channels 118 119 --- 120 121 ## Pattern: Channel Listener Pattern 122 123 ``` 124 ┌─────────────┐ Channel Subscribe ┌─────────────┐ 125 │ WebSocket │ ────────────────────────────► │ UI │ 126 │ Handler │ │ Component │ 127 └─────────────┘ └─────────────┘ 128 │ │ 129 ▼ ▼ 130 .next(message) subscribe(fn) 131 ``` 132 133 The pattern decouples event sources from event consumers using a pub-sub 134 channel.