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