@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-routes.md ADDED
@@ -0,0 +1,660 @@
1
+ ---
2
+ name: one-routes
3
+ description: One framework routing guide. Use when working with routing, routes, pages, navigation, app/ directory structure, file-based routing, dynamic routes [id], params, layouts, _layout.tsx, SSG, SSR, SPA static generation, loaders, data loading, Link component, or navigate.
4
+ ---
5
+
6
+ # one framework: routes & routing
7
+
8
+ comprehensive routing guide for one framework covering file-system routing,
9
+ render modes, navigation, loaders, and api routes.
10
+
11
+ ## file system routing
12
+
13
+ all routes live in `app/` directory. files export a react component (prefer
14
+ named exports for better hot reloading).
15
+
16
+ **important:** avoid intermediate imports in route files. instead of importing
17
+ and re-exporting, use inline re-export syntax:
18
+ `export { ComponentName as default } from '~/features/...'`
19
+
20
+ ### route types
21
+
22
+ **simple routes:**
23
+
24
+ - `app/index.tsx` → `/`
25
+ - `app/about.tsx` → `/about`
26
+ - `app/blog/index.tsx` → `/blog`
27
+
28
+ **dynamic params:**
29
+
30
+ - `app/blog/[slug].tsx` → `/blog/post-one`
31
+ - access via `useParams()` → `params.slug`
32
+
33
+ **rest params:**
34
+
35
+ - `app/catalog/[...rest].tsx` → `/catalog/a/b/c`
36
+ - `params.rest = ['a', 'b', 'c']`
37
+
38
+ **not found:**
39
+
40
+ - `app/+not-found.tsx` → custom 404 pages
41
+
42
+ **groups (invisible in url):**
43
+
44
+ - `app/(blog)/` → organize without creating url segments
45
+ - useful for adding layouts without affecting routes
46
+
47
+ **platform-specific:**
48
+
49
+ - `.web.tsx`, `.native.tsx`, `.ios.tsx`, `.android.tsx`
50
+ - example: `index.web.tsx` only matches on web
51
+
52
+ ### accessing params
53
+
54
+ ```tsx
55
+ import { useParams } from 'one'
56
+
57
+ export function loader({ params }) {
58
+ // server-side: params.slug available
59
+ }
60
+
61
+ export default function Page() {
62
+ const params = useParams()
63
+ // client-side: params.slug available
64
+ }
65
+ ```
66
+
67
+ ### route types generation
68
+
69
+ route types auto-generated to `app/routes.d.ts` for type-safe navigation.
70
+
71
+ ## render modes
72
+
73
+ four main modes controlled by filename suffix:
74
+
75
+ ### ssg (static site generation)
76
+
77
+ **suffix:** `route+ssg.tsx` **behavior:** pre-renders html/css at build time
78
+ **served from:** cdn **best for:** marketing pages, blogs, mostly static content
79
+ **notes:** can still add dynamic content after hydration
80
+
81
+ ### spa (single page app)
82
+
83
+ **suffix:** `route+spa.tsx` **behavior:** no server rendering, client-only js
84
+ **best for:** dashboards, highly dynamic apps (linear, figma-style) **notes:**
85
+ simpler to build, slower initial load, worse seo
86
+
87
+ ### ssr (server side rendered)
88
+
89
+ **suffix:** `route+ssr.tsx` **behavior:** renders on each request **best for:**
90
+ dynamic content needing seo (github issues-style) **notes:** most complex and
91
+ expensive, can cache on cdn with invalidation
92
+
93
+ ### api routes
94
+
95
+ **suffix:** `route+api.tsx` **behavior:** creates api endpoints using web
96
+ standard request/response
97
+
98
+ ```tsx
99
+ import { Endpoint } from 'one'
100
+
101
+ export const GET: Endpoint = (request) => {
102
+ return Response.json({ hello: 'world' })
103
+ }
104
+
105
+ export const POST: Endpoint = async (request) => {
106
+ const data = await request.json()
107
+ return Response.json({ received: data })
108
+ }
109
+
110
+ // or catch-all default export
111
+ export default (request: Request): Response => {
112
+ return Response.json({ hello: 'world' })
113
+ }
114
+ ```
115
+
116
+ ## navigation
117
+
118
+ ### link component
119
+
120
+ ```tsx
121
+ import { Link } from 'one'
122
+
123
+ <Link href="/blog">go to blog</Link>
124
+ <Link href="/blog/post" replace>replace history</Link>
125
+ <Link href="https://example.com" target="_blank">external</Link>
126
+ ```
127
+
128
+ **link props:**
129
+
130
+ - `href`: typed route path
131
+ - `asChild`: forward props to child
132
+ - `replace`: replace history instead of push
133
+ - `push`: explicitly push to history
134
+ - `className`: web class, native css interop
135
+ - `target`: web-only (\_blank, \_self, etc.)
136
+ - `rel`: web-only (nofollow, noopener, etc.)
137
+ - `download`: web-only download attribute
138
+
139
+ ### useRouter hook
140
+
141
+ ```tsx
142
+ const router = useRouter()
143
+
144
+ router.push('/path') // navigate
145
+ router.replace('/path') // replace
146
+ router.back() // go back
147
+ router.canGoBack() // check history
148
+ router.setParams({ id: 5 }) // update params
149
+ router.dismiss() // native modal dismiss
150
+ ```
151
+
152
+ **full api:**
153
+
154
+ ```tsx
155
+ type Router = {
156
+ back: () => void
157
+ canGoBack: () => boolean
158
+ push: (href: Href, options?: LinkToOptions) => void
159
+ navigate: (href: Href, options?: LinkToOptions) => void
160
+ replace: (href: Href, options?: LinkToOptions) => void
161
+ dismiss: (count?: number) => void
162
+ dismissAll: () => void
163
+ canDismiss: () => boolean
164
+ setParams: <T>(params?: Record<string, string | undefined | null>) => void
165
+ subscribe: (listener: RootStateListener) => () => void
166
+ onLoadState: (listener: LoadingStateListener) => () => void
167
+ }
168
+ ```
169
+
170
+ ## loaders
171
+
172
+ server-side data loading that runs at build-time (ssg), request-time (ssr), or
173
+ load-time (spa). tree-shaken from client bundles.
174
+
175
+ ### basic usage
176
+
177
+ ```tsx
178
+ import { useLoader } from 'one'
179
+
180
+ export async function loader({ params, path, request }) {
181
+ // server-only code - can access secrets
182
+ const user = await getUser(params.id)
183
+ return { greet: `Hello ${user.name}` }
184
+ }
185
+
186
+ export default function Page() {
187
+ const data = useLoader(loader) // automatically type-safe
188
+ return <p>{data.greet}</p>
189
+ }
190
+ ```
191
+
192
+ ### loader arguments
193
+
194
+ - `params`: dynamic route segments
195
+ - `path`: full pathname
196
+ - `request`: web request object (ssr only)
197
+
198
+ ### return types
199
+
200
+ - json-serializable objects
201
+ - response objects
202
+ - can throw response for early exit
203
+
204
+ ### patterns
205
+
206
+ **redirect if not found:**
207
+
208
+ ```tsx
209
+ export async function loader({ params: { id } }) {
210
+ const user = await db.users.findOne({ id })
211
+ if (!user) {
212
+ throw redirect('/login')
213
+ }
214
+ return { user }
215
+ }
216
+ ```
217
+
218
+ **custom response:**
219
+
220
+ ```tsx
221
+ export async function loader() {
222
+ return new Response(JSON.stringify(data), {
223
+ headers: { 'Content-Type': 'application/json' },
224
+ })
225
+ }
226
+ ```
227
+
228
+ ## routing exports
229
+
230
+ ### generateStaticParams
231
+
232
+ required for ssg routes with dynamic segments. returns array of param objects.
233
+
234
+ ```tsx
235
+ // app/blog/[month]/[year]/[slug]+ssg.tsx
236
+ export async function generateStaticParams() {
237
+ const posts = await getAllBlogPosts()
238
+ return posts.map((post) => ({
239
+ month: post.month,
240
+ year: post.year,
241
+ slug: post.slug,
242
+ }))
243
+ }
244
+ ```
245
+
246
+ ## middlewares
247
+
248
+ **status:** developing
249
+
250
+ place `_middleware.ts` anywhere in `app/`. middlewares nest and run top to
251
+ bottom.
252
+
253
+ ```tsx
254
+ import { createMiddleware } from 'one'
255
+
256
+ export default createMiddleware(async ({ request, next, context }) => {
257
+ // before route
258
+ if (request.url.includes('test')) {
259
+ return Response.json({ middleware: 'works' })
260
+ }
261
+
262
+ const response = await next() // run rest of middlewares + route
263
+
264
+ // after route
265
+ if (!response && request.url.endsWith('/missing')) {
266
+ return Response.json({ notFound: true })
267
+ }
268
+
269
+ return response
270
+ })
271
+ ```
272
+
273
+ **arguments:**
274
+
275
+ - `request`: web request object
276
+ - `next`: function to run rest of chain
277
+ - `context`: mutable object for passing data
278
+
279
+ ## helper functions
280
+
281
+ ### redirect
282
+
283
+ ```tsx
284
+ import { redirect } from 'one'
285
+
286
+ export function redirectToLogin() {
287
+ return redirect('/login')
288
+ }
289
+
290
+ // in loader
291
+ export async function loader({ params }) {
292
+ const user = await db.users.findOne({ id: params.id })
293
+ if (!user) throw redirect('/login')
294
+ }
295
+ ```
296
+
297
+ - server: returns response.redirect
298
+ - client: calls router.navigate
299
+
300
+ ### getURL
301
+
302
+ ```tsx
303
+ import { getURL } from 'one'
304
+
305
+ const url = getURL() // http://127.0.0.1:8081
306
+ ```
307
+
308
+ returns current app url, uses `ONE_SERVER_URL` in production
309
+
310
+ ### href
311
+
312
+ ```tsx
313
+ import { href } from 'one'
314
+
315
+ const link = href('/post/hello-world') // type-checked
316
+ ```
317
+
318
+ type-level validation only
319
+
320
+ ## layouts
321
+
322
+ layouts frame routes in a directory and can nest inside each other. must render
323
+ one of: `Slot`, `Stack`, `Tabs`, or `Drawer`.
324
+
325
+ ### root layout
326
+
327
+ ```tsx
328
+ // app/_layout.tsx
329
+ import { Slot } from 'one'
330
+
331
+ export default function Layout() {
332
+ return (
333
+ <html lang="en-US">
334
+ <head>
335
+ <meta charSet="utf-8" />
336
+ </head>
337
+ <body>
338
+ <Slot />
339
+ </body>
340
+ </html>
341
+ )
342
+ }
343
+ ```
344
+
345
+ ### useServerHeadInsertion
346
+
347
+ root layout only hook for inserting tags into `<head>` after ssr. useful for
348
+ css-in-js.
349
+
350
+ ```tsx
351
+ import { Slot, useServerHeadInsertion } from 'one'
352
+
353
+ export default function Layout() {
354
+ useServerHeadInsertion(() => {
355
+ return <style>{renderCSS()}</style>
356
+ })
357
+ return (
358
+ <html>
359
+ <Slot />
360
+ </html>
361
+ )
362
+ }
363
+ ```
364
+
365
+ ### slot
366
+
367
+ renders children directly without frame. simplest layout option.
368
+
369
+ ```tsx
370
+ import { Slot } from 'one'
371
+
372
+ export default function Layout() {
373
+ return <Slot />
374
+ }
375
+ ```
376
+
377
+ ### stack
378
+
379
+ react navigation native stack. can configure per-screen.
380
+
381
+ ```tsx
382
+ import { Stack } from 'one'
383
+
384
+ export default function Layout() {
385
+ return (
386
+ <Stack screenOptions={{ headerRight: () => <Button label="Settings" /> }}>
387
+ <Stack.Screen name="index" options={{ title: 'Feed' }} />
388
+ <Stack.Screen name="[id]" options={{ title: 'Post' }} />
389
+ <Stack.Screen
390
+ name="sheet"
391
+ options={{
392
+ presentation: 'formSheet',
393
+ animation: 'slide_from_bottom',
394
+ headerShown: false,
395
+ }}
396
+ />
397
+ </Stack>
398
+ )
399
+ }
400
+ ```
401
+
402
+ **common options:**
403
+
404
+ - `presentation`: 'card' | 'modal' | 'transparentModal' | 'containedModal' |
405
+ 'containedTransparentModal' | 'fullScreenModal' | 'formSheet'
406
+ - `animation`: 'default' | 'fade' | 'fade_from_bottom' | 'flip' | 'simple_push'
407
+ | 'slide_from_bottom' | 'slide_from_right' | 'slide_from_left' | 'none'
408
+ - `headerShown`: boolean
409
+ - `title`: string
410
+ - `headerRight`: () => ReactElement
411
+ - `headerLeft`: () => ReactElement
412
+
413
+ ### tabs
414
+
415
+ react navigation bottom tabs. must set `href` on each screen.
416
+
417
+ ```tsx
418
+ import { Tabs } from 'one'
419
+
420
+ export default function Layout() {
421
+ return (
422
+ <Tabs>
423
+ <Tabs.Screen
424
+ name="explore"
425
+ options={{
426
+ title: 'Explore',
427
+ href: '/explore',
428
+ }}
429
+ />
430
+ <Tabs.Screen
431
+ name="profile"
432
+ options={{
433
+ title: 'Profile',
434
+ href: '/home/profile',
435
+ }}
436
+ />
437
+ </Tabs>
438
+ )
439
+ }
440
+ ```
441
+
442
+ **note:** new tab routes may need `--clean` flag to show up
443
+
444
+ ### drawer
445
+
446
+ **status:** early (currently disabled due to react-native-gesture-handler issue)
447
+
448
+ ### nested layouts
449
+
450
+ **twitter/x pattern example:**
451
+
452
+ ```
453
+ app/
454
+ _layout.tsx → tabs (feed, notifications, profile)
455
+ home/
456
+ _layout.tsx → stack (inside feed tab)
457
+ index.tsx → feed list
458
+ post-[id].tsx → individual post
459
+ notifications.tsx
460
+ profile.tsx
461
+ ```
462
+
463
+ **tabs layout:**
464
+
465
+ ```tsx
466
+ // app/_layout.tsx
467
+ import { Tabs } from 'one'
468
+
469
+ export default function RootLayout() {
470
+ return (
471
+ <Tabs screenOptions={{ headerShown: false }}>
472
+ <Tabs.Screen name="home" options={{ title: 'Feed', href: '/' }} />
473
+ <Tabs.Screen
474
+ name="notifications"
475
+ options={{ title: 'Notifications', href: '/notifications' }}
476
+ />
477
+ <Tabs.Screen
478
+ name="profile"
479
+ options={{ title: 'Profile', href: '/home/profile' }}
480
+ />
481
+ </Tabs>
482
+ )
483
+ }
484
+ ```
485
+
486
+ **stack inside feed tab:**
487
+
488
+ ```tsx
489
+ // app/home/_layout.tsx
490
+ import { Stack, Slot } from 'one'
491
+
492
+ export default function FeedLayout() {
493
+ return (
494
+ <>
495
+ {typeof window !== 'undefined' ? (
496
+ <Slot /> // web uses slot (browser back button is stack)
497
+ ) : (
498
+ <Stack>
499
+ <Stack.Screen name="index" options={{ title: 'Feed' }} />
500
+ <Stack.Screen name="post-[id]" options={{ title: 'Post' }} />
501
+ </Stack>
502
+ )}
503
+ </>
504
+ )
505
+ }
506
+ ```
507
+
508
+ ### platform-specific layouts
509
+
510
+ ```tsx
511
+ import { Stack, Slot } from 'one'
512
+
513
+ export default function Layout() {
514
+ if (typeof window !== 'undefined') {
515
+ return <Slot /> // web: browser navigation
516
+ }
517
+ return (
518
+ <Stack>
519
+ <Stack.Screen name="index" />
520
+ <Stack.Screen name="[id]" />
521
+ </Stack>
522
+ )
523
+ }
524
+ ```
525
+
526
+ ### custom layouts with withLayoutContext
527
+
528
+ ```tsx
529
+ import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'
530
+ import { withLayoutContext } from 'one'
531
+
532
+ const NativeTabsNavigator = createNativeBottomTabNavigator().Navigator
533
+ export const NativeTabs = withLayoutContext(NativeTabsNavigator)
534
+ ```
535
+
536
+ ### layout limitations
537
+
538
+ - layouts don't support loaders (yet)
539
+ - `useParams` won't work in layouts
540
+ - use `useActiveParams` instead for accessing url params in layouts
541
+
542
+ ## practical patterns
543
+
544
+ ### modal presentation
545
+
546
+ ```tsx
547
+ // app/_layout.tsx
548
+ import { Stack } from 'one'
549
+
550
+ export default function Layout() {
551
+ return (
552
+ <Stack>
553
+ <Stack.Screen name="index" />
554
+ <Stack.Screen
555
+ name="modal"
556
+ options={{
557
+ presentation: 'modal',
558
+ headerShown: false,
559
+ }}
560
+ />
561
+ </Stack>
562
+ )
563
+ }
564
+ ```
565
+
566
+ ### shared header across routes
567
+
568
+ ```tsx
569
+ // app/(app)/_layout.tsx
570
+ import { Stack } from 'one'
571
+
572
+ export default function AppLayout() {
573
+ return (
574
+ <Stack
575
+ screenOptions={{
576
+ headerStyle: { backgroundColor: '#000' },
577
+ headerTintColor: '#fff',
578
+ headerRight: () => <ProfileButton />,
579
+ }}
580
+ >
581
+ <Stack.Screen name="index" options={{ title: 'Home' }} />
582
+ <Stack.Screen name="settings" options={{ title: 'Settings' }} />
583
+ </Stack>
584
+ )
585
+ }
586
+ ```
587
+
588
+ ### bottom sheet pattern
589
+
590
+ ```tsx
591
+ // app/_layout.tsx
592
+ import { Stack } from 'one'
593
+
594
+ export default function Layout() {
595
+ return (
596
+ <Stack>
597
+ <Stack.Screen name="index" />
598
+ <Stack.Screen
599
+ name="sheet"
600
+ options={{
601
+ presentation: 'formSheet',
602
+ sheetAllowedDetents: [0.5, 1],
603
+ sheetGrabberVisible: true,
604
+ }}
605
+ />
606
+ </Stack>
607
+ )
608
+ }
609
+ ```
610
+
611
+ ### basic route with data
612
+
613
+ ```tsx
614
+ // app/user/[id].tsx
615
+ export async function loader({ params }) {
616
+ const user = await db.users.find(params.id)
617
+ if (!user) throw redirect('/404')
618
+ return { user }
619
+ }
620
+
621
+ export default function UserPage() {
622
+ const { user } = useLoader(loader)
623
+ return <Text>{user.name}</Text>
624
+ }
625
+ ```
626
+
627
+ ### ssg with static params
628
+
629
+ ```tsx
630
+ // app/blog/[slug]+ssg.tsx
631
+ export async function generateStaticParams() {
632
+ const posts = await getAllPosts()
633
+ return posts.map((p) => ({ slug: p.slug }))
634
+ }
635
+
636
+ export async function loader({ params }) {
637
+ return { post: await getPost(params.slug) }
638
+ }
639
+
640
+ export default function Post() {
641
+ const { post } = useLoader(loader)
642
+ return <Markdown>{post.content}</Markdown>
643
+ }
644
+ ```
645
+
646
+ ### api routes
647
+
648
+ ```tsx
649
+ // app/api/users+api.tsx
650
+ export const GET = async (request: Request) => {
651
+ const users = await db.users.findAll()
652
+ return Response.json(users)
653
+ }
654
+
655
+ export const POST = async (request: Request) => {
656
+ const data = await request.json()
657
+ const user = await db.users.create(data)
658
+ return Response.json(user, { status: 201 })
659
+ }
660
+ ```