@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/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
+ ```