Skip to content

Commit a537be6

Browse files
committed
support layout component
1 parent 7cd7e17 commit a537be6

File tree

4 files changed

+53
-36
lines changed

4 files changed

+53
-36
lines changed

app/root.tsx

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ import {
3636
} from './components/ui/dropdown-menu.tsx'
3737
import { Icon, href as iconsHref } from './components/ui/icon.tsx'
3838
import { EpicToaster } from './components/ui/sonner.tsx'
39-
import { ThemeSwitch, useTheme } from './routes/resources+/theme-switch.tsx'
39+
import {
40+
ThemeSwitch,
41+
useOptionalTheme,
42+
useTheme,
43+
} from './routes/resources+/theme-switch.tsx'
4044
import tailwindStyleSheetUrl from './styles/tailwind.css?url'
4145
import { getUserId, logout } from './utils/auth.server.ts'
4246
import { ClientHintCheck, getHints } from './utils/client-hints.tsx'
@@ -153,14 +157,13 @@ function Document({
153157
nonce,
154158
theme = 'light',
155159
env = {},
156-
allowIndexing = true,
157160
}: {
158161
children: React.ReactNode
159162
nonce: string
160163
theme?: Theme
161164
env?: Record<string, string>
162-
allowIndexing?: boolean
163165
}) {
166+
const allowIndexing = ENV.ALLOW_INDEXING !== 'false'
164167
return (
165168
<html lang="en" className={`${theme} h-full overflow-x-hidden`}>
166169
<head>
@@ -188,24 +191,29 @@ function Document({
188191
)
189192
}
190193

194+
export function Layout({ children }: { children: React.ReactNode }) {
195+
// if there was an error running the loader, data could be missing
196+
const data = useLoaderData<typeof loader | null>()
197+
const nonce = useNonce()
198+
const theme = useOptionalTheme()
199+
return (
200+
<Document nonce={nonce} theme={theme} env={data?.ENV}>
201+
{children}
202+
</Document>
203+
)
204+
}
205+
191206
function App() {
192207
const data = useLoaderData<typeof loader>()
193-
const nonce = useNonce()
194208
const user = useOptionalUser()
195209
const theme = useTheme()
196210
const matches = useMatches()
197211
const isOnSearchPage = matches.find((m) => m.id === 'routes/users+/index')
198212
const searchBar = isOnSearchPage ? null : <SearchBar status="idle" />
199-
const allowIndexing = data.ENV.ALLOW_INDEXING !== 'false'
200213
useToast(data.toast)
201214

202215
return (
203-
<Document
204-
nonce={nonce}
205-
theme={theme}
206-
allowIndexing={allowIndexing}
207-
env={data.ENV}
208-
>
216+
<>
209217
<div className="flex h-screen flex-col justify-between">
210218
<header className="container py-6">
211219
<nav className="flex flex-wrap items-center justify-between gap-4 sm:flex-nowrap md:gap-8">
@@ -237,7 +245,7 @@ function App() {
237245
</div>
238246
<EpicToaster closeButton position="top-center" theme={theme} />
239247
<EpicProgress />
240-
</Document>
248+
</>
241249
)
242250
}
243251

@@ -326,21 +334,6 @@ function UserDropdown() {
326334
)
327335
}
328336

329-
export function ErrorBoundary() {
330-
// the nonce doesn't rely on the loader so we can access that
331-
const nonce = useNonce()
332-
333-
// NOTE: you cannot use useLoaderData in an ErrorBoundary because the loader
334-
// likely failed to run so we have to do the best we can.
335-
// We could probably do better than this (it's possible the loader did run).
336-
// This would require a change in Remix.
337-
338-
// Just make sure your root route never errors out and you'll always be able
339-
// to give the user a better UX.
340-
341-
return (
342-
<Document nonce={nonce}>
343-
<GeneralErrorBoundary />
344-
</Document>
345-
)
346-
}
337+
// this is a last resort error boundary. There's not much useful information we
338+
// can offer at this level.
339+
export const ErrorBoundary = GeneralErrorBoundary

app/routes/resources+/theme-switch.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import { redirect, useFetcher, useFetchers } from '@remix-run/react'
66
import { ServerOnly } from 'remix-utils/server-only'
77
import { z } from 'zod'
88
import { Icon } from '#app/components/ui/icon.tsx'
9-
import { useHints } from '#app/utils/client-hints.tsx'
10-
import { useRequestInfo } from '#app/utils/request-info.ts'
9+
import { useHints, useOptionalHints } from '#app/utils/client-hints.tsx'
10+
import {
11+
useOptionalRequestInfo,
12+
useRequestInfo,
13+
} from '#app/utils/request-info.ts'
1114
import { type Theme, setTheme } from '#app/utils/theme.server.ts'
1215

1316
const ThemeFormSchema = z.object({
@@ -129,3 +132,13 @@ export function useTheme() {
129132
}
130133
return requestInfo.userPrefs.theme ?? hints.theme
131134
}
135+
136+
export function useOptionalTheme() {
137+
const optionalHints = useOptionalHints()
138+
const optionalRequestInfo = useOptionalRequestInfo()
139+
const optimisticMode = useOptimisticThemeMode()
140+
if (optimisticMode) {
141+
return optimisticMode === 'system' ? optionalHints?.theme : optimisticMode
142+
}
143+
return optionalRequestInfo?.userPrefs.theme ?? optionalHints?.theme
144+
}

app/utils/client-hints.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import { clientHint as timeZoneHint } from '@epic-web/client-hints/time-zone'
1111
import { useRevalidator } from '@remix-run/react'
1212
import * as React from 'react'
13-
import { useRequestInfo } from './request-info.ts'
13+
import { useOptionalRequestInfo, useRequestInfo } from './request-info.ts'
1414

1515
const hintsUtils = getHintUtils({
1616
theme: colorSchemeHint,
@@ -28,6 +28,11 @@ export function useHints() {
2828
return requestInfo.hints
2929
}
3030

31+
export function useOptionalHints() {
32+
const requestInfo = useOptionalRequestInfo()
33+
return requestInfo?.hints
34+
}
35+
3136
/**
3237
* @returns inline script element that checks for client hints and sets cookies
3338
* if they are not set then reloads the page if any cookie was set to an

app/utils/request-info.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ import { useRouteLoaderData } from '@remix-run/react'
33
import { type loader as rootLoader } from '#app/root.tsx'
44

55
/**
6-
* @returns the request info from the root loader
6+
* @returns the request info from the root loader (throws an error if it does not exist)
77
*/
88
export function useRequestInfo() {
9+
const maybeRequestInfo = useOptionalRequestInfo()
10+
invariant(maybeRequestInfo, 'No requestInfo found in root loader')
11+
12+
return maybeRequestInfo
13+
}
14+
15+
export function useOptionalRequestInfo() {
916
const data = useRouteLoaderData<typeof rootLoader>('root')
10-
invariant(data?.requestInfo, 'No requestInfo found in root loader')
1117

12-
return data.requestInfo
18+
return data?.requestInfo
1319
}

0 commit comments

Comments
 (0)