@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/one-hooks.md
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: one-hooks
|
|
3
|
+
description: One framework hooks guide for routing, navigation, data loading, and focus management. INVOKE WHEN: hooks, useRouter, useParams, usePathname, useLoader, useFocusEffect, useNavigation, navigation hooks, data loading hooks, route params, query params, navigation state, screen focus management.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# one framework: hooks
|
|
7
|
+
|
|
8
|
+
comprehensive guide to hooks in one framework for routing, navigation, data
|
|
9
|
+
loading, and focus management.
|
|
10
|
+
|
|
11
|
+
## route data hooks
|
|
12
|
+
|
|
13
|
+
### useParams
|
|
14
|
+
|
|
15
|
+
returns route segment params. only updates for current route (avoids unnecessary
|
|
16
|
+
re-renders).
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { useParams } from 'one'
|
|
20
|
+
|
|
21
|
+
export default function BlogPost() {
|
|
22
|
+
const params = useParams()
|
|
23
|
+
// params.slug for [slug].tsx routes
|
|
24
|
+
// params.id for [id].tsx routes
|
|
25
|
+
// params.rest for [...rest].tsx routes (array)
|
|
26
|
+
|
|
27
|
+
return <Text>post: {params.slug}</Text>
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**when to use:**
|
|
32
|
+
|
|
33
|
+
- accessing dynamic route segments
|
|
34
|
+
- component needs to know its own route params
|
|
35
|
+
- want to avoid re-renders when other routes change
|
|
36
|
+
|
|
37
|
+
**type safety:** route params are typed based on your file structure.
|
|
38
|
+
|
|
39
|
+
### useActiveParams
|
|
40
|
+
|
|
41
|
+
returns current url path segments. updates even when route isn't focused.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { useActiveParams } from 'one'
|
|
45
|
+
|
|
46
|
+
export default function Analytics() {
|
|
47
|
+
const params = useActiveParams()
|
|
48
|
+
// always reflects current url, even in parent layouts
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
trackPageView(params)
|
|
52
|
+
}, [params])
|
|
53
|
+
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**when to use:**
|
|
59
|
+
|
|
60
|
+
- analytics tracking
|
|
61
|
+
- global state that depends on current route
|
|
62
|
+
- layouts that need to know child route params
|
|
63
|
+
|
|
64
|
+
**difference from useParams:**
|
|
65
|
+
|
|
66
|
+
- `useParams`: only updates when this route is active
|
|
67
|
+
- `useActiveParams`: always updates with current url
|
|
68
|
+
|
|
69
|
+
### usePathname
|
|
70
|
+
|
|
71
|
+
returns current pathname string. updates on every route change.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { usePathname } from 'one'
|
|
75
|
+
|
|
76
|
+
export default function NavBar() {
|
|
77
|
+
const pathname = usePathname()
|
|
78
|
+
// pathname = '/blog/post-slug'
|
|
79
|
+
|
|
80
|
+
const isActive = (path: string) => pathname === path
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Nav>
|
|
84
|
+
<Link href="/" active={isActive('/')}>
|
|
85
|
+
home
|
|
86
|
+
</Link>
|
|
87
|
+
<Link href="/blog" active={isActive('/blog')}>
|
|
88
|
+
blog
|
|
89
|
+
</Link>
|
|
90
|
+
</Nav>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**when to use:**
|
|
96
|
+
|
|
97
|
+
- highlighting active nav items
|
|
98
|
+
- conditional rendering based on current path
|
|
99
|
+
- breadcrumbs
|
|
100
|
+
|
|
101
|
+
### useLoader
|
|
102
|
+
|
|
103
|
+
returns loader data with automatic type safety. only supports loader from same
|
|
104
|
+
file.
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { useLoader } from 'one'
|
|
108
|
+
|
|
109
|
+
export async function loader({ params }) {
|
|
110
|
+
const post = await getPost(params.slug)
|
|
111
|
+
return { post, author: await getAuthor(post.authorId) }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default function BlogPost() {
|
|
115
|
+
const data = useLoader(loader)
|
|
116
|
+
// data is typed: { post: Post, author: Author }
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<>
|
|
120
|
+
<Text>{data.post.title}</Text>
|
|
121
|
+
<Text>by {data.author.name}</Text>
|
|
122
|
+
</>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**when to use:**
|
|
128
|
+
|
|
129
|
+
- accessing server-loaded data
|
|
130
|
+
- ssg/ssr/spa routes with loaders
|
|
131
|
+
|
|
132
|
+
**notes:**
|
|
133
|
+
|
|
134
|
+
- automatically type-safe based on loader return type
|
|
135
|
+
- only works with loader in same file
|
|
136
|
+
- data available after initial render
|
|
137
|
+
|
|
138
|
+
## navigation hooks
|
|
139
|
+
|
|
140
|
+
### useRouter
|
|
141
|
+
|
|
142
|
+
static object for imperative routing. never updates.
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
import { useRouter } from 'one'
|
|
146
|
+
|
|
147
|
+
export default function Page() {
|
|
148
|
+
const router = useRouter()
|
|
149
|
+
|
|
150
|
+
const handleSubmit = async (data) => {
|
|
151
|
+
await saveData(data)
|
|
152
|
+
router.push('/success')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const handleBack = () => {
|
|
156
|
+
if (router.canGoBack()) {
|
|
157
|
+
router.back()
|
|
158
|
+
} else {
|
|
159
|
+
router.push('/')
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<>
|
|
165
|
+
<Form onSubmit={handleSubmit} />
|
|
166
|
+
<Button onPress={handleBack}>back</Button>
|
|
167
|
+
</>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**full api:**
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
type Router = {
|
|
176
|
+
// navigation
|
|
177
|
+
push: (href: Href, options?: LinkToOptions) => void
|
|
178
|
+
navigate: (href: Href, options?: LinkToOptions) => void
|
|
179
|
+
replace: (href: Href, options?: LinkToOptions) => void
|
|
180
|
+
back: () => void
|
|
181
|
+
|
|
182
|
+
// history checks
|
|
183
|
+
canGoBack: () => boolean
|
|
184
|
+
canDismiss: () => boolean
|
|
185
|
+
|
|
186
|
+
// native modals
|
|
187
|
+
dismiss: (count?: number) => void
|
|
188
|
+
dismissAll: () => void
|
|
189
|
+
|
|
190
|
+
// params
|
|
191
|
+
setParams: (params?: Record<string, string | undefined | null>) => void
|
|
192
|
+
|
|
193
|
+
// subscriptions
|
|
194
|
+
subscribe: (listener: RootStateListener) => () => void
|
|
195
|
+
onLoadState: (listener: LoadingStateListener) => () => void
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**methods:**
|
|
200
|
+
|
|
201
|
+
**push / navigate:**
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
router.push('/blog/new-post')
|
|
205
|
+
router.push(`/user/${userId}`)
|
|
206
|
+
router.navigate('/settings') // same as push
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**replace:**
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
router.replace('/login') // replace current history entry
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**back:**
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
router.back() // go back in history
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**canGoBack:**
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
if (router.canGoBack()) {
|
|
225
|
+
router.back()
|
|
226
|
+
} else {
|
|
227
|
+
router.push('/') // go home if can't go back
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**dismiss (native only):**
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
router.dismiss() // close current modal
|
|
235
|
+
router.dismiss(2) // close 2 modals
|
|
236
|
+
router.dismissAll() // close all modals
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**setParams:**
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
// update url params without navigation
|
|
243
|
+
router.setParams({ filter: 'active', sort: 'date' })
|
|
244
|
+
// /posts?filter=active&sort=date
|
|
245
|
+
|
|
246
|
+
router.setParams({ filter: undefined }) // remove param
|
|
247
|
+
// /posts?sort=date
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**subscribe:**
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
const unsubscribe = router.subscribe((state) => {
|
|
255
|
+
console.info('route state changed:', state)
|
|
256
|
+
})
|
|
257
|
+
return unsubscribe
|
|
258
|
+
}, [])
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### useLinkTo
|
|
262
|
+
|
|
263
|
+
creates custom link props. useful for building custom link components.
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
import { useLinkTo } from 'one'
|
|
267
|
+
|
|
268
|
+
type LinkToProps = {
|
|
269
|
+
href: Href
|
|
270
|
+
replace?: boolean
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
type LinkToResult = {
|
|
274
|
+
href: string
|
|
275
|
+
role: 'link'
|
|
276
|
+
onPress: (e?: MouseEvent | GestureResponderEvent) => void
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function CustomLink({ href, replace, children }) {
|
|
280
|
+
const linkProps = useLinkTo({ href, replace })
|
|
281
|
+
|
|
282
|
+
return <Pressable {...linkProps}>{children}</Pressable>
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### useNavigation
|
|
287
|
+
|
|
288
|
+
direct react navigation access. lower-level than useRouter. accepts optional
|
|
289
|
+
parent argument.
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
import { useNavigation } from 'one'
|
|
293
|
+
|
|
294
|
+
export default function Page() {
|
|
295
|
+
const navigation = useNavigation()
|
|
296
|
+
|
|
297
|
+
// access parent navigation
|
|
298
|
+
const parentNav = useNavigation('/(root)')
|
|
299
|
+
const grandparentNav = useNavigation('../../')
|
|
300
|
+
|
|
301
|
+
navigation.setOptions({
|
|
302
|
+
title: 'new title',
|
|
303
|
+
headerRight: () => <Button />,
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
return <Content />
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**when to use:**
|
|
311
|
+
|
|
312
|
+
- need react navigation api directly
|
|
313
|
+
- setting screen options dynamically
|
|
314
|
+
- accessing parent navigators
|
|
315
|
+
|
|
316
|
+
## focus hooks
|
|
317
|
+
|
|
318
|
+
### useFocusEffect
|
|
319
|
+
|
|
320
|
+
like useEffect but only when route is focused. must pass dependency array.
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
import { useFocusEffect } from 'one'
|
|
324
|
+
|
|
325
|
+
export default function Profile({ userId }) {
|
|
326
|
+
const [user, setUser] = useState(null)
|
|
327
|
+
|
|
328
|
+
useFocusEffect(
|
|
329
|
+
() => {
|
|
330
|
+
// runs when screen becomes focused
|
|
331
|
+
const unsubscribe = subscribeToUser(userId, setUser)
|
|
332
|
+
|
|
333
|
+
return () => {
|
|
334
|
+
// cleanup when screen loses focus
|
|
335
|
+
unsubscribe()
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
[userId], // dependencies
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return <ProfileContent user={user} />
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**when to use:**
|
|
346
|
+
|
|
347
|
+
- subscriptions that should only run when screen is visible
|
|
348
|
+
- analytics tracking on screen view
|
|
349
|
+
- refreshing data when returning to screen
|
|
350
|
+
- pausing/resuming animations
|
|
351
|
+
|
|
352
|
+
**common patterns:**
|
|
353
|
+
|
|
354
|
+
**refresh on focus:**
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
useFocusEffect(() => {
|
|
358
|
+
refreshData()
|
|
359
|
+
}, [refreshData])
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**subscription management:**
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
useFocusEffect(() => {
|
|
366
|
+
const subscription = api.subscribe(userId, handleUpdate)
|
|
367
|
+
return () => subscription.unsubscribe()
|
|
368
|
+
}, [userId])
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**analytics:**
|
|
372
|
+
|
|
373
|
+
```tsx
|
|
374
|
+
useFocusEffect(() => {
|
|
375
|
+
trackScreenView('profile', { userId })
|
|
376
|
+
}, [userId])
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### useIsFocused
|
|
380
|
+
|
|
381
|
+
returns boolean. true if current screen is active. re-export from react
|
|
382
|
+
navigation.
|
|
383
|
+
|
|
384
|
+
```tsx
|
|
385
|
+
import { useIsFocused } from 'one'
|
|
386
|
+
|
|
387
|
+
export default function VideoPlayer() {
|
|
388
|
+
const isFocused = useIsFocused()
|
|
389
|
+
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (!isFocused) {
|
|
392
|
+
pauseVideo()
|
|
393
|
+
} else {
|
|
394
|
+
resumeVideo()
|
|
395
|
+
}
|
|
396
|
+
}, [isFocused])
|
|
397
|
+
|
|
398
|
+
return <Video />
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**when to use:**
|
|
403
|
+
|
|
404
|
+
- conditional logic based on focus state
|
|
405
|
+
- pause/resume media playback
|
|
406
|
+
- conditional rendering
|
|
407
|
+
|
|
408
|
+
**vs useFocusEffect:**
|
|
409
|
+
|
|
410
|
+
- `useIsFocused`: reactive boolean value
|
|
411
|
+
- `useFocusEffect`: callback on focus change
|
|
412
|
+
|
|
413
|
+
## practical patterns
|
|
414
|
+
|
|
415
|
+
### protected route with redirect
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
import { useAuth } from '~/hooks/useAuth'
|
|
419
|
+
import { useRouter } from 'one'
|
|
420
|
+
|
|
421
|
+
export default function ProtectedPage() {
|
|
422
|
+
const { isAuthenticated } = useAuth()
|
|
423
|
+
const router = useRouter()
|
|
424
|
+
|
|
425
|
+
useEffect(() => {
|
|
426
|
+
if (!isAuthenticated) {
|
|
427
|
+
router.replace('/login')
|
|
428
|
+
}
|
|
429
|
+
}, [isAuthenticated])
|
|
430
|
+
|
|
431
|
+
if (!isAuthenticated) return null
|
|
432
|
+
|
|
433
|
+
return <Content />
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### active nav link
|
|
438
|
+
|
|
439
|
+
```tsx
|
|
440
|
+
import { usePathname } from 'one'
|
|
441
|
+
import { Link } from 'one'
|
|
442
|
+
|
|
443
|
+
export function NavLink({ href, children }) {
|
|
444
|
+
const pathname = usePathname()
|
|
445
|
+
const isActive = pathname === href
|
|
446
|
+
|
|
447
|
+
return (
|
|
448
|
+
<Link
|
|
449
|
+
href={href}
|
|
450
|
+
style={{
|
|
451
|
+
color: isActive ? '$blue10' : '$gray11',
|
|
452
|
+
fontWeight: isActive ? 'bold' : 'normal',
|
|
453
|
+
}}
|
|
454
|
+
>
|
|
455
|
+
{children}
|
|
456
|
+
</Link>
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### search params with router
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
import { useRouter } from 'one'
|
|
465
|
+
|
|
466
|
+
export function SearchFilters() {
|
|
467
|
+
const router = useRouter()
|
|
468
|
+
|
|
469
|
+
const updateFilter = (key: string, value: string) => {
|
|
470
|
+
router.setParams({ [key]: value })
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return (
|
|
474
|
+
<>
|
|
475
|
+
<Select onValueChange={(v) => updateFilter('category', v)}>
|
|
476
|
+
<option>all</option>
|
|
477
|
+
<option>blog</option>
|
|
478
|
+
</Select>
|
|
479
|
+
<Select onValueChange={(v) => updateFilter('sort', v)}>
|
|
480
|
+
<option>date</option>
|
|
481
|
+
<option>popular</option>
|
|
482
|
+
</Select>
|
|
483
|
+
</>
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### data fetching on focus
|
|
489
|
+
|
|
490
|
+
```tsx
|
|
491
|
+
import { useFocusEffect } from 'one'
|
|
492
|
+
import { useState } from 'react'
|
|
493
|
+
|
|
494
|
+
export default function Feed() {
|
|
495
|
+
const [posts, setPosts] = useState([])
|
|
496
|
+
|
|
497
|
+
useFocusEffect(() => {
|
|
498
|
+
let cancelled = false
|
|
499
|
+
|
|
500
|
+
fetchPosts().then((data) => {
|
|
501
|
+
if (!cancelled) {
|
|
502
|
+
setPosts(data)
|
|
503
|
+
}
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
return () => {
|
|
507
|
+
cancelled = true
|
|
508
|
+
}
|
|
509
|
+
}, [])
|
|
510
|
+
|
|
511
|
+
return <PostList posts={posts} />
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### breadcrumbs from params
|
|
516
|
+
|
|
517
|
+
```tsx
|
|
518
|
+
import { useParams, usePathname } from 'one'
|
|
519
|
+
|
|
520
|
+
export function Breadcrumbs() {
|
|
521
|
+
const pathname = usePathname()
|
|
522
|
+
const params = useParams()
|
|
523
|
+
|
|
524
|
+
const segments = pathname.split('/').filter(Boolean)
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<View>
|
|
528
|
+
{segments.map((segment, i) => (
|
|
529
|
+
<Link key={i} href={`/${segments.slice(0, i + 1).join('/')}`}>
|
|
530
|
+
{params[segment] || segment}
|
|
531
|
+
</Link>
|
|
532
|
+
))}
|
|
533
|
+
</View>
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### conditional loader data
|
|
539
|
+
|
|
540
|
+
```tsx
|
|
541
|
+
import { useLoader } from 'one'
|
|
542
|
+
|
|
543
|
+
export async function loader({ params }) {
|
|
544
|
+
const user = await getUser(params.userId)
|
|
545
|
+
|
|
546
|
+
if (!user) {
|
|
547
|
+
return { error: 'user not found' }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return {
|
|
551
|
+
user,
|
|
552
|
+
posts: await getUserPosts(params.userId),
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export default function UserPage() {
|
|
557
|
+
const data = useLoader(loader)
|
|
558
|
+
|
|
559
|
+
if ('error' in data) {
|
|
560
|
+
return <Text>{data.error}</Text>
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return (
|
|
564
|
+
<>
|
|
565
|
+
<UserHeader user={data.user} />
|
|
566
|
+
<PostList posts={data.posts} />
|
|
567
|
+
</>
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
```
|