@take-out/docs 0.0.42
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/aggregates.md +584 -0
- package/cloudflare-dev-tunnel.md +41 -0
- package/database.md +229 -0
- package/docs.md +8 -0
- package/emitters.md +562 -0
- package/hot-updater.md +223 -0
- package/native-hot-update.md +252 -0
- package/one-components.md +234 -0
- package/one-hooks.md +570 -0
- package/one-routes.md +660 -0
- package/package-json.md +115 -0
- package/package.json +12 -0
- package/react-native-navigation-flow.md +184 -0
- package/scripts.md +147 -0
- package/sync-prompt.md +208 -0
- package/tamagui.md +478 -0
- package/testing-integration.md +564 -0
- package/triggers.md +450 -0
- package/xcodebuild-mcp.md +127 -0
- package/zero.md +719 -0
package/emitters.md
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: emitters
|
|
3
|
+
description: Event emitters and pub/sub pattern for ephemeral UI state management. INVOKE WHEN: event emitters, pub/sub, ephemeral state, temporary state, transient state, UI events, event coordination, subscribe, emit, listen, @take-out/helpers emitters.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Emitters Guide - Event-Based State Management
|
|
7
|
+
|
|
8
|
+
Emitters are a lightweight event-based state management pattern used throughout
|
|
9
|
+
the codebase for handling ephemeral state and events. They're designed for
|
|
10
|
+
transient state that doesn't need persistence - think UI interactions, temporary
|
|
11
|
+
states, and event coordination.
|
|
12
|
+
|
|
13
|
+
Warning: Emitters should only be used for ephemeral state events. For persistent
|
|
14
|
+
state, use Zero queries. Overusing emitters can lead to hard-to-debug event
|
|
15
|
+
chains and memory leaks.
|
|
16
|
+
|
|
17
|
+
## Core Concepts
|
|
18
|
+
|
|
19
|
+
An emitter is a simple pub-sub pattern with:
|
|
20
|
+
|
|
21
|
+
- A current value
|
|
22
|
+
- Listeners that react to changes
|
|
23
|
+
- Type-safe event emissions
|
|
24
|
+
- Optional comparison logic to prevent unnecessary updates
|
|
25
|
+
|
|
26
|
+
## Basic Usage
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { createEmitter, useEmitterValue, useEmitter } from '@take-out/helpers'
|
|
30
|
+
|
|
31
|
+
// simple value emitter
|
|
32
|
+
const counterEmitter = createEmitter('counter', 0)
|
|
33
|
+
|
|
34
|
+
// emit a new value
|
|
35
|
+
counterEmitter.emit(5)
|
|
36
|
+
|
|
37
|
+
// get current value directly
|
|
38
|
+
console.info(counterEmitter.value) // 5
|
|
39
|
+
|
|
40
|
+
// listen to changes
|
|
41
|
+
const unsubscribe = counterEmitter.listen((value) => {
|
|
42
|
+
console.info('Counter changed:', value)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// clean up listener
|
|
46
|
+
unsubscribe()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Comparison Strategies
|
|
50
|
+
|
|
51
|
+
Comparators control when listeners are notified:
|
|
52
|
+
|
|
53
|
+
- isEqualIdentity: Only emit if value !== previous (default for primitives)
|
|
54
|
+
- isEqualNever: Always emit, even for same values (useful for events)
|
|
55
|
+
- isEqualDeep: Only emit if deeply different (for objects/arrays)
|
|
56
|
+
- Custom: Provide your own comparison function
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { isEqualIdentity, isEqualNever, isEqualDeep } from '@take-out/helpers'
|
|
60
|
+
|
|
61
|
+
// always emit (good for action events)
|
|
62
|
+
const actionEmitter = createEmitter(
|
|
63
|
+
'action',
|
|
64
|
+
{ type: 'idle' },
|
|
65
|
+
{ comparator: isEqualNever },
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// only emit if value changes (identity check)
|
|
69
|
+
const idEmitter = createEmitter('currentId', null as string | null, {
|
|
70
|
+
comparator: isEqualIdentity,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// deep equality check for objects
|
|
74
|
+
const configEmitter = createEmitter(
|
|
75
|
+
'config',
|
|
76
|
+
{ theme: 'dark', fontSize: 14 },
|
|
77
|
+
{ comparator: isEqualDeep },
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## React Hooks
|
|
82
|
+
|
|
83
|
+
### useEmitterValue - Subscribe to value changes
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
function MyComponent() {
|
|
87
|
+
const counter = useEmitterValue(counterEmitter)
|
|
88
|
+
return <div>Count: {counter}</div>
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### useEmitter - React to emissions without storing value
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
function ActionHandler() {
|
|
96
|
+
useEmitter(actionEmitter, (action) => {
|
|
97
|
+
switch (action.type) {
|
|
98
|
+
case 'submit':
|
|
99
|
+
handleSubmit()
|
|
100
|
+
break
|
|
101
|
+
case 'cancel':
|
|
102
|
+
handleCancel()
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### useEmitterSelector - Derive state from emitter value
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
function DerivedState() {
|
|
114
|
+
const isEven = useEmitterSelector(counterEmitter, (count) => count % 2 === 0)
|
|
115
|
+
return <div>Is even: {isEven}</div>
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### useEmittersSelector - Combine multiple emitters
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
function CombinedState() {
|
|
123
|
+
const combined = useEmittersSelector(
|
|
124
|
+
[counterEmitter, idEmitter] as const,
|
|
125
|
+
([count, id]) => ({ count, id }),
|
|
126
|
+
)
|
|
127
|
+
return (
|
|
128
|
+
<div>
|
|
129
|
+
Count: {combined.count}, ID: {combined.id}
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Advanced Patterns
|
|
136
|
+
|
|
137
|
+
### Type-safe action emitters with discriminated unions
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
type MessageInputAction =
|
|
141
|
+
| { type: 'submit'; id: string }
|
|
142
|
+
| { type: 'focus' }
|
|
143
|
+
| { type: 'clear' }
|
|
144
|
+
| { type: 'autocomplete-user'; user: User; value: string }
|
|
145
|
+
| { type: 'autocomplete-emoji'; emoji: string; value: string }
|
|
146
|
+
|
|
147
|
+
const messageInputController = createEmitter<MessageInputAction>(
|
|
148
|
+
'messageInput',
|
|
149
|
+
{ type: 'clear' },
|
|
150
|
+
{ comparator: isEqualNever },
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
// usage with type safety
|
|
154
|
+
messageInputController.emit({ type: 'submit', id: '123' })
|
|
155
|
+
messageInputController.emit({
|
|
156
|
+
type: 'autocomplete-user',
|
|
157
|
+
user: someUser,
|
|
158
|
+
value: '@john',
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Awaiting next value with promises
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
async function waitForConfirmation() {
|
|
166
|
+
const confirmEmitter = createEmitter('confirm', false)
|
|
167
|
+
|
|
168
|
+
showConfirmDialog()
|
|
169
|
+
|
|
170
|
+
const confirmed = await confirmEmitter.nextValue()
|
|
171
|
+
|
|
172
|
+
if (confirmed) {
|
|
173
|
+
performAction()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Global emitters that survive HMR
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
import { createGlobalEmitter } from '@take-out/helpers'
|
|
182
|
+
|
|
183
|
+
const globalStateEmitter = createGlobalEmitter('globalState', {
|
|
184
|
+
theme: 'dark',
|
|
185
|
+
sidebarOpen: true,
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Contextual emitters for component trees
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
import { createContextualEmitter } from '@take-out/helpers'
|
|
193
|
+
|
|
194
|
+
const [useChannelEmitter, ChannelEmitterProvider] =
|
|
195
|
+
createContextualEmitter<string>('channelId', { comparator: isEqualIdentity })
|
|
196
|
+
|
|
197
|
+
// provider at parent level
|
|
198
|
+
function ChannelView({ channelId }) {
|
|
199
|
+
return (
|
|
200
|
+
<ChannelEmitterProvider value={channelId}>
|
|
201
|
+
<ChannelContent />
|
|
202
|
+
</ChannelEmitterProvider>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// consumer in child components
|
|
207
|
+
function ChannelContent() {
|
|
208
|
+
const channelEmitter = useChannelEmitter()
|
|
209
|
+
const channelId = useEmitterValue(channelEmitter)
|
|
210
|
+
// ...
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Silent emitters (no console output)
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
const positionEmitter = createEmitter(
|
|
218
|
+
'position',
|
|
219
|
+
{ x: 0, y: 0 },
|
|
220
|
+
{ silent: true },
|
|
221
|
+
)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Real-World Examples from Codebase
|
|
225
|
+
|
|
226
|
+
### Gallery/Modal Control
|
|
227
|
+
|
|
228
|
+
Shows/hides UI elements with nullable state
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
import { galleryEmitter } from '~/features/gallery/galleryEmitter'
|
|
232
|
+
|
|
233
|
+
// open gallery with items
|
|
234
|
+
galleryEmitter.emit({
|
|
235
|
+
items: attachments,
|
|
236
|
+
firstItem: attachmentId,
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// close gallery
|
|
240
|
+
galleryEmitter.emit(null)
|
|
241
|
+
|
|
242
|
+
// component usage
|
|
243
|
+
function Gallery() {
|
|
244
|
+
const galleryData = useEmitterValue(galleryEmitter)
|
|
245
|
+
if (!galleryData) return null
|
|
246
|
+
return <GalleryView {...galleryData} />
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Error Handling
|
|
251
|
+
|
|
252
|
+
Centralized error reporting
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
import { errorEmitter } from '~/features/errors/errorEmitter'
|
|
256
|
+
|
|
257
|
+
// report error from anywhere
|
|
258
|
+
errorEmitter.emit({
|
|
259
|
+
error: new Error('Something went wrong'),
|
|
260
|
+
context: 'user-action',
|
|
261
|
+
timestamp: Date.now(),
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
// global error handler
|
|
265
|
+
function ErrorBoundary() {
|
|
266
|
+
useEmitter(errorEmitter, (errorReport) => {
|
|
267
|
+
if (errorReport) {
|
|
268
|
+
logToSentry(errorReport)
|
|
269
|
+
showErrorToast(errorReport.error.message)
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Autocomplete System
|
|
276
|
+
|
|
277
|
+
Complex stateful interactions
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
import { autocompleteEmitter } from '~/features/autocomplete/autocompleteEmitter'
|
|
281
|
+
|
|
282
|
+
type AutocompleteEvent =
|
|
283
|
+
| { type: 'open'; query: string; position: DOMRect }
|
|
284
|
+
| { type: 'close' }
|
|
285
|
+
| { type: 'select'; item: AutocompleteItem }
|
|
286
|
+
|
|
287
|
+
// trigger autocomplete
|
|
288
|
+
autocompleteEmitter.emit({
|
|
289
|
+
type: 'open',
|
|
290
|
+
query: '@use',
|
|
291
|
+
position: inputElement.getBoundingClientRect(),
|
|
292
|
+
})
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Message Highlighting
|
|
296
|
+
|
|
297
|
+
Temporary UI states
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
import { messageItemEmitter } from '~/features/message/messageItemEmitter'
|
|
301
|
+
|
|
302
|
+
// highlight a message temporarily
|
|
303
|
+
messageItemEmitter.emit({ type: 'highlight', id: messageId })
|
|
304
|
+
|
|
305
|
+
// in message component
|
|
306
|
+
function MessageItem({ message }) {
|
|
307
|
+
const [isHighlighted, setIsHighlighted] = useState(false)
|
|
308
|
+
|
|
309
|
+
useEmitter(messageItemEmitter, (event) => {
|
|
310
|
+
if (event.type === 'highlight' && event.id === message.id) {
|
|
311
|
+
setIsHighlighted(true)
|
|
312
|
+
setTimeout(() => setIsHighlighted(false), 1000)
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
return <div className={isHighlighted ? 'highlight' : ''}>{message.text}</div>
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Toast/Notification Queue
|
|
321
|
+
|
|
322
|
+
Event-driven notifications
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
const notificationEmitter = createEmitter<
|
|
326
|
+
| { type: 'show'; notification: Notification }
|
|
327
|
+
| { type: 'hide'; id: string }
|
|
328
|
+
| { type: 'hide_all' }
|
|
329
|
+
>('notifications', { type: 'hide_all' })
|
|
330
|
+
|
|
331
|
+
// show notification
|
|
332
|
+
notificationEmitter.emit({
|
|
333
|
+
type: 'show',
|
|
334
|
+
notification: {
|
|
335
|
+
id: Date.now().toString(),
|
|
336
|
+
title: 'New message',
|
|
337
|
+
body: 'You have a new message from John',
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Dialog Management
|
|
343
|
+
|
|
344
|
+
Coordinating async UI flows
|
|
345
|
+
|
|
346
|
+
```tsx
|
|
347
|
+
import { confirmEmitter, dialogEmit } from '~/interface/dialogs/actions'
|
|
348
|
+
|
|
349
|
+
async function deleteMessage(messageId: string) {
|
|
350
|
+
dialogEmit({
|
|
351
|
+
type: 'confirm',
|
|
352
|
+
title: 'Delete Message?',
|
|
353
|
+
description: 'This action cannot be undone.',
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
const confirmed = await confirmEmitter.nextValue()
|
|
357
|
+
|
|
358
|
+
if (confirmed) {
|
|
359
|
+
await api.deleteMessage(messageId)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Server Tint Color
|
|
365
|
+
|
|
366
|
+
Propagating theme changes
|
|
367
|
+
|
|
368
|
+
```tsx
|
|
369
|
+
import { serverTintEmitter } from '~/features/server/serverTintEmitter'
|
|
370
|
+
|
|
371
|
+
// provider updates tint when server changes
|
|
372
|
+
function ServerTintProvider() {
|
|
373
|
+
const { server } = useCurrentServer()
|
|
374
|
+
|
|
375
|
+
useLayoutEffect(() => {
|
|
376
|
+
serverTintEmitter.emit(server?.tint || 0)
|
|
377
|
+
}, [server?.tint])
|
|
378
|
+
|
|
379
|
+
return null
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// consumer components react to tint changes
|
|
383
|
+
function ThemedButton() {
|
|
384
|
+
const tint = useEmitterValue(serverTintEmitter)
|
|
385
|
+
const color = tint > 0 ? colors[tint] : 'default'
|
|
386
|
+
return <Button color={color} />
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Best Practices
|
|
391
|
+
|
|
392
|
+
DO:
|
|
393
|
+
|
|
394
|
+
- Use for ephemeral UI state (modals, tooltips, highlights)
|
|
395
|
+
- Use for event coordination between distant components
|
|
396
|
+
- Use for temporary user interactions
|
|
397
|
+
- Use for one-time events and notifications
|
|
398
|
+
- Clean up listeners in useEffect/useLayoutEffect
|
|
399
|
+
- Use appropriate comparators for your data type
|
|
400
|
+
- Name emitters clearly and consistently
|
|
401
|
+
|
|
402
|
+
DON'T:
|
|
403
|
+
|
|
404
|
+
- Use for persistent application state (use Zero/Jotai instead)
|
|
405
|
+
- Use for data that needs to survive page refreshes
|
|
406
|
+
- Create complex chains of emitters triggering each other
|
|
407
|
+
- Forget to unsubscribe listeners (memory leaks)
|
|
408
|
+
- Use for frequently changing values like scroll position without throttling
|
|
409
|
+
- Store large objects that could be in proper state management
|
|
410
|
+
- Use when simple React state or props would suffice
|
|
411
|
+
|
|
412
|
+
## Debugging
|
|
413
|
+
|
|
414
|
+
Emitters support debug logging when DEBUG_LEVEL > 1:
|
|
415
|
+
|
|
416
|
+
1. Set DEBUG_LEVEL in environment/devtools
|
|
417
|
+
2. Non-silent emitters will log emissions with stack traces
|
|
418
|
+
3. Use browser DevTools to filter by emitter name
|
|
419
|
+
|
|
420
|
+
Example console output:
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
📣 messageInput
|
|
424
|
+
{ type: 'submit', id: '123' }
|
|
425
|
+
trace > [stack trace]
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Memory Management
|
|
429
|
+
|
|
430
|
+
### Preventing memory leaks:
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
// bad: listener never cleaned up
|
|
434
|
+
function LeakyComponent() {
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
myEmitter.listen((value) => {
|
|
437
|
+
doSomething(value)
|
|
438
|
+
})
|
|
439
|
+
}, [])
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// good: proper cleanup
|
|
443
|
+
function CleanComponent() {
|
|
444
|
+
useEffect(() => {
|
|
445
|
+
const unsubscribe = myEmitter.listen((value) => {
|
|
446
|
+
doSomething(value)
|
|
447
|
+
})
|
|
448
|
+
return unsubscribe
|
|
449
|
+
}, [])
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// better: use the hook (handles cleanup automatically)
|
|
453
|
+
function BestComponent() {
|
|
454
|
+
useEmitter(myEmitter, (value) => {
|
|
455
|
+
doSomething(value)
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Testing
|
|
461
|
+
|
|
462
|
+
Emitters are easy to test:
|
|
463
|
+
|
|
464
|
+
```tsx
|
|
465
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
466
|
+
|
|
467
|
+
describe('Emitter Testing', () => {
|
|
468
|
+
it('should emit values to listeners', () => {
|
|
469
|
+
const emitter = createEmitter('test', 0)
|
|
470
|
+
const listener = vi.fn()
|
|
471
|
+
|
|
472
|
+
const unsubscribe = emitter.listen(listener)
|
|
473
|
+
emitter.emit(5)
|
|
474
|
+
|
|
475
|
+
expect(listener).toHaveBeenCalledWith(5)
|
|
476
|
+
expect(emitter.value).toBe(5)
|
|
477
|
+
|
|
478
|
+
unsubscribe()
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
it('should handle async nextValue', async () => {
|
|
482
|
+
const emitter = createEmitter('async', false)
|
|
483
|
+
|
|
484
|
+
setTimeout(() => emitter.emit(true), 100)
|
|
485
|
+
|
|
486
|
+
const value = await emitter.nextValue()
|
|
487
|
+
expect(value).toBe(true)
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Common Pitfalls & Solutions
|
|
493
|
+
|
|
494
|
+
### Infinite loops with emitters in effects
|
|
495
|
+
|
|
496
|
+
```tsx
|
|
497
|
+
// bad: creates infinite loop
|
|
498
|
+
function InfiniteLoop() {
|
|
499
|
+
useEmitter(emitterA, (value) => {
|
|
500
|
+
emitterB.emit(value)
|
|
501
|
+
})
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// good: break circular dependencies
|
|
505
|
+
function SafeEmission() {
|
|
506
|
+
useEmitter(emitterA, (value) => {
|
|
507
|
+
if (shouldUpdate(value)) {
|
|
508
|
+
emitterB.emit(transform(value))
|
|
509
|
+
}
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Using emitters for computed values
|
|
515
|
+
|
|
516
|
+
```tsx
|
|
517
|
+
// bad: unnecessary emitter for derived state
|
|
518
|
+
const doubledEmitter = createEmitter('doubled', 0)
|
|
519
|
+
useEmitter(counterEmitter, (count) => {
|
|
520
|
+
doubledEmitter.emit(count * 2)
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
// good: use selector for derived values
|
|
524
|
+
const doubled = useEmitterSelector(counterEmitter, (count) => count * 2)
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Forgetting type safety
|
|
528
|
+
|
|
529
|
+
```tsx
|
|
530
|
+
// bad: loose typing
|
|
531
|
+
const emitter = createEmitter('data', {} as any)
|
|
532
|
+
|
|
533
|
+
// good: proper typing
|
|
534
|
+
type DataState = {
|
|
535
|
+
user: User | null
|
|
536
|
+
loading: boolean
|
|
537
|
+
error: Error | null
|
|
538
|
+
}
|
|
539
|
+
const emitter = createEmitter<DataState>('data', {
|
|
540
|
+
user: null,
|
|
541
|
+
loading: false,
|
|
542
|
+
error: null,
|
|
543
|
+
})
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
## Summary
|
|
547
|
+
|
|
548
|
+
Emitters are powerful for ephemeral UI state, event coordination, decoupled
|
|
549
|
+
communication, and temporary interactions.
|
|
550
|
+
|
|
551
|
+
But remember:
|
|
552
|
+
|
|
553
|
+
- They're not for persistent state
|
|
554
|
+
- Always clean up listeners
|
|
555
|
+
- Use appropriate comparators
|
|
556
|
+
- Don't overuse them
|
|
557
|
+
|
|
558
|
+
When in doubt, ask yourself:
|
|
559
|
+
|
|
560
|
+
- "Does this state need to persist?" → Use Zero/Jotai
|
|
561
|
+
- "Is this a temporary UI event?" → Use an emitter
|
|
562
|
+
- "Can I use simple React state?" → Use useState
|