Skip to content

Commit

Permalink
root layout export
Browse files Browse the repository at this point in the history
  • Loading branch information
nichtsam committed Dec 16, 2024
1 parent 50b0aa9 commit 8125847
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 64 deletions.
71 changes: 29 additions & 42 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ScrollRestoration,
useLoaderData,
useRouteError,
useRouteLoaderData,
} from '@remix-run/react'
import { captureRemixErrorBoundaryError, withSentry } from '@sentry/remix'
import clsx from 'clsx'
Expand All @@ -38,7 +39,7 @@ import { useNonce } from './utils/nonce-provider.tsx'
import { pipeHeaders } from './utils/remix.server.ts'
import { combineHeaders } from './utils/request.server.ts'
import { setTheme, getTheme, type Theme } from './utils/theme.server.ts'
import { SET_THEME_INTENT, useTheme } from './utils/theme.tsx'
import { SET_THEME_INTENT, useOptionalTheme } from './utils/theme.tsx'
import { ServerTiming } from './utils/timings.server.ts'
import { getToast } from './utils/toast.server.ts'
import { useToast } from './utils/toast.ts'
Expand Down Expand Up @@ -130,14 +131,14 @@ function Document({
nonce,
theme = 'light',
env,
disallowIndexing,
}: {
children: React.ReactNode
nonce: string
theme?: Theme
env?: PublicEnv
disallowIndexing?: boolean
}) {
const disallowIndexing = !!env?.DISALLOW_INDEXING

return (
<html lang="en" className={clsx(theme, 'relative')}>
<head>
Expand Down Expand Up @@ -171,33 +172,22 @@ function Document({

function App() {
const data = useLoaderData<typeof loader>()
const nonce = useNonce()
const env = data.env
const theme = useTheme()
const disallowIndexing = env.DISALLOW_INDEXING
useToast(data.toast)

return (
<Document
nonce={nonce}
env={env}
theme={theme}
disallowIndexing={disallowIndexing}
>
<div className="flex h-full min-h-screen flex-col">
<header>
<NavBar />
</header>

<main className="flex-1">
<Outlet />
</main>

<footer>
<Footer />
</footer>
</div>
</Document>
<div className="flex h-full min-h-screen flex-col">
<header>
<NavBar />
</header>

<main className="flex-1">
<Outlet />
</main>

<footer>
<Footer />
</footer>
</div>
)
}

Expand All @@ -215,24 +205,21 @@ function AppWithProviders() {

export default withSentry(AppWithProviders)

export function ErrorBoundary() {
// the nonce doesn't rely on the loader so we can access that
export function Layout({ children }: { children: React.ReactNode }) {
const data = useRouteLoaderData<typeof loader>('root')
const nonce = useNonce()

const error = useRouteError()
captureRemixErrorBoundaryError(error)

// NOTE: you cannot use useLoaderData in an ErrorBoundary because the loader
// likely failed to run so we have to do the best we can.
// We could probably do better than this (it's possible the loader did run).
// This would require a change in Remix.

// Just make sure your root route never errors out and you'll always be able
// to give the user a better UX.
const theme = useOptionalTheme()

return (
<Document nonce={nonce} disallowIndexing={true}>
<GeneralErrorBoundary />
<Document nonce={nonce} env={data?.env} theme={theme}>
{children}
</Document>
)
}

export function ErrorBoundary() {
const error = useRouteError()
captureRemixErrorBoundaryError(error)

return <GeneralErrorBoundary />
}
12 changes: 12 additions & 0 deletions app/utils/misc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useEffect, useState } from 'react'

const sleep = (ms: number) =>
new Promise<void>((res) => setTimeout(() => res(), ms))

Expand Down Expand Up @@ -33,6 +35,16 @@ async function downloadFile(
}
}

export function useClientJavascriptEnable() {
const [clientJavascriptEnable, setClientJavascriptEnable] = useState(false)

useEffect(() => {
setClientJavascriptEnable(true)
}, [])

return clientJavascriptEnable
}

export { sleep, generateCallAll, unvariant, downloadFile }

type Prettify<T> = {
Expand Down
16 changes: 13 additions & 3 deletions app/utils/request-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@ import { useRouteLoaderData } from '@remix-run/react'
import { type loader as rootLoader } from '#app/root.tsx'

export function useRequestInfo() {
const data = useRouteLoaderData<typeof rootLoader>('root')
if (!data?.requestInfo) {
const requestInfo = useOptionalRequestInfo()
if (!requestInfo) {
throw new Error('No requestInfo found in root loader')
}

return data.requestInfo
return requestInfo
}

export const useHints = () => {
const requestInfo = useRequestInfo()
return requestInfo.hints
}

export function useOptionalRequestInfo() {
const data = useRouteLoaderData<typeof rootLoader>('root')
return data?.requestInfo
}

export const useOptionalHints = () => {
const requestInfo = useOptionalRequestInfo()
return requestInfo?.hints
}
46 changes: 27 additions & 19 deletions app/utils/theme.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { getFormProps, useForm } from '@conform-to/react'
import { parseWithZod } from '@conform-to/zod'
import { useFetcher, useFetchers } from '@remix-run/react'
import { useEffect, useState } from 'react'
import { z } from 'zod'
import { Button } from '#app/components/ui/button.tsx'
import { Icon } from '#app/components/ui/icon.tsx'
import { unvariant } from './misc.ts'
import { useRequestInfo, useHints } from './request-info.ts'
import { unvariant, useClientJavascriptEnable } from './misc.ts'
import {
useRequestInfo,
useHints,
useOptionalRequestInfo,
useOptionalHints,
} from './request-info.ts'
import { type setTheme as setThemeAction } from './theme.server.ts'

export const SET_THEME_INTENT = 'set-theme'
Expand All @@ -18,7 +22,6 @@ export const ThemeFormSchema = z.object({
export const useTheme = () => {
const requestInfo = useRequestInfo()
const hints = useHints()

const optimisticMode = useOptimisticThemeMode()
if (optimisticMode) {
return optimisticMode === 'system' ? hints.theme : optimisticMode
Expand All @@ -27,6 +30,17 @@ export const useTheme = () => {
return requestInfo.userPreferences.theme ?? hints.theme
}

export const useOptionalTheme = () => {
const requestInfo = useOptionalRequestInfo()
const hints = useOptionalHints()
const optimisticMode = useOptimisticThemeMode()
if (optimisticMode) {
return optimisticMode === 'system' ? hints?.theme : optimisticMode
}

return requestInfo?.userPreferences.theme ?? hints?.theme
}

export function useOptimisticThemeMode() {
const fetchers = useFetchers()
const themeFetcher = fetchers.find((f) => f.formAction === '/')
Expand All @@ -42,32 +56,26 @@ export function useOptimisticThemeMode() {
}
}

export const ThemeSwitcher = () => {
const [clientJavascriptEnable, setClientJavascriptEnable] = useState(false)
const modeLabel = {
light: <Icon name="sun" className="animate-in fade-in" />,
dark: <Icon name="moon" className="animate-in fade-in" />,
system: <Icon name="laptop" className="animate-in fade-in" />,
}

const {
userPreferences: { theme: themPreference },
} = useRequestInfo()
export const ThemeSwitcher = () => {
const fetcher = useFetcher<typeof setThemeAction>()

const [form] = useForm({
id: 'theme-switch',
lastResult: fetcher.data?.result,
})

const themePreference = useOptionalRequestInfo()?.userPreferences.theme
const optimisticMode = useOptimisticThemeMode()
const mode = optimisticMode ?? themPreference ?? 'system'
const mode = optimisticMode ?? themePreference ?? 'system'
const nextMode =
mode === 'system' ? 'light' : mode === 'light' ? 'dark' : 'system'
const modeLabel = {
light: <Icon name="sun" className="animate-in fade-in" />,
dark: <Icon name="moon" className="animate-in fade-in" />,
system: <Icon name="laptop" className="animate-in fade-in" />,
}

useEffect(() => {
setClientJavascriptEnable(true)
}, [])
const clientJavascriptEnable = useClientJavascriptEnable()

return (
<fetcher.Form method="POST" action="/" {...getFormProps(form)}>
Expand Down

0 comments on commit 8125847

Please sign in to comment.