Skip to content

Commit 7149c06

Browse files
authored
move theme switch from root.tsx to full stack component (#737)
1 parent 4d7a674 commit 7149c06

File tree

2 files changed

+117
-108
lines changed

2 files changed

+117
-108
lines changed

app/root.tsx

Lines changed: 3 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
import { useForm, getFormProps } from '@conform-to/react'
2-
import { parseWithZod } from '@conform-to/zod'
3-
import { invariantResponse } from '@epic-web/invariant'
41
import {
52
json,
63
type LoaderFunctionArgs,
7-
type ActionFunctionArgs,
84
type HeadersFunction,
95
type LinksFunction,
106
type MetaFunction,
@@ -17,16 +13,13 @@ import {
1713
Outlet,
1814
Scripts,
1915
ScrollRestoration,
20-
useFetcher,
21-
useFetchers,
2216
useLoaderData,
2317
useMatches,
2418
useSubmit,
2519
} from '@remix-run/react'
2620
import { withSentry } from '@sentry/remix'
2721
import { useRef } from 'react'
2822
import { HoneypotProvider } from 'remix-utils/honeypot/react'
29-
import { z } from 'zod'
3023
import { GeneralErrorBoundary } from './components/error-boundary.tsx'
3124
import { EpicProgress } from './components/progress-bar.tsx'
3225
import { SearchBar } from './components/search-bar.tsx'
@@ -41,16 +34,16 @@ import {
4134
} from './components/ui/dropdown-menu.tsx'
4235
import { Icon, href as iconsHref } from './components/ui/icon.tsx'
4336
import { EpicToaster } from './components/ui/sonner.tsx'
37+
import { ThemeSwitch, useTheme } from './routes/resources+/theme-switch.tsx'
4438
import tailwindStyleSheetUrl from './styles/tailwind.css?url'
4539
import { getUserId, logout } from './utils/auth.server.ts'
46-
import { ClientHintCheck, getHints, useHints } from './utils/client-hints.tsx'
40+
import { ClientHintCheck, getHints } from './utils/client-hints.tsx'
4741
import { prisma } from './utils/db.server.ts'
4842
import { getEnv } from './utils/env.server.ts'
4943
import { honeypot } from './utils/honeypot.server.ts'
5044
import { combineHeaders, getDomainUrl, getUserImgSrc } from './utils/misc.tsx'
5145
import { useNonce } from './utils/nonce-provider.ts'
52-
import { useRequestInfo } from './utils/request-info.ts'
53-
import { type Theme, setTheme, getTheme } from './utils/theme.server.ts'
46+
import { type Theme, getTheme } from './utils/theme.server.ts'
5447
import { makeTimings, time } from './utils/timing.server.ts'
5548
import { getToast } from './utils/toast.server.ts'
5649
import { useOptionalUser, useUser } from './utils/user.ts'
@@ -156,26 +149,6 @@ export const headers: HeadersFunction = ({ loaderHeaders }) => {
156149
return headers
157150
}
158151

159-
const ThemeFormSchema = z.object({
160-
theme: z.enum(['system', 'light', 'dark']),
161-
})
162-
163-
export async function action({ request }: ActionFunctionArgs) {
164-
const formData = await request.formData()
165-
const submission = parseWithZod(formData, {
166-
schema: ThemeFormSchema,
167-
})
168-
169-
invariantResponse(submission.status === 'success', 'Invalid theme received')
170-
171-
const { theme } = submission.value
172-
173-
const responseInit = {
174-
headers: { 'set-cookie': setTheme(theme) },
175-
}
176-
return json({ result: submission.reply() }, responseInit)
177-
}
178-
179152
function Document({
180153
children,
181154
nonce,
@@ -354,84 +327,6 @@ function UserDropdown() {
354327
)
355328
}
356329

357-
/**
358-
* @returns the user's theme preference, or the client hint theme if the user
359-
* has not set a preference.
360-
*/
361-
export function useTheme() {
362-
const hints = useHints()
363-
const requestInfo = useRequestInfo()
364-
const optimisticMode = useOptimisticThemeMode()
365-
if (optimisticMode) {
366-
return optimisticMode === 'system' ? hints.theme : optimisticMode
367-
}
368-
return requestInfo.userPrefs.theme ?? hints.theme
369-
}
370-
371-
/**
372-
* If the user's changing their theme mode preference, this will return the
373-
* value it's being changed to.
374-
*/
375-
export function useOptimisticThemeMode() {
376-
const fetchers = useFetchers()
377-
const themeFetcher = fetchers.find(f => f.formAction === '/')
378-
379-
if (themeFetcher && themeFetcher.formData) {
380-
const submission = parseWithZod(themeFetcher.formData, {
381-
schema: ThemeFormSchema,
382-
})
383-
384-
if (submission.status === 'success') {
385-
return submission.value.theme
386-
}
387-
}
388-
}
389-
390-
function ThemeSwitch({ userPreference }: { userPreference?: Theme | null }) {
391-
const fetcher = useFetcher<typeof action>()
392-
393-
const [form] = useForm({
394-
id: 'theme-switch',
395-
lastResult: fetcher.data?.result,
396-
})
397-
398-
const optimisticMode = useOptimisticThemeMode()
399-
const mode = optimisticMode ?? userPreference ?? 'system'
400-
const nextMode =
401-
mode === 'system' ? 'light' : mode === 'light' ? 'dark' : 'system'
402-
const modeLabel = {
403-
light: (
404-
<Icon name="sun">
405-
<span className="sr-only">Light</span>
406-
</Icon>
407-
),
408-
dark: (
409-
<Icon name="moon">
410-
<span className="sr-only">Dark</span>
411-
</Icon>
412-
),
413-
system: (
414-
<Icon name="laptop">
415-
<span className="sr-only">System</span>
416-
</Icon>
417-
),
418-
}
419-
420-
return (
421-
<fetcher.Form method="POST" {...getFormProps(form)}>
422-
<input type="hidden" name="theme" value={nextMode} />
423-
<div className="flex gap-2">
424-
<button
425-
type="submit"
426-
className="flex h-8 w-8 cursor-pointer items-center justify-center"
427-
>
428-
{modeLabel[mode]}
429-
</button>
430-
</div>
431-
</fetcher.Form>
432-
)
433-
}
434-
435330
export function ErrorBoundary() {
436331
// the nonce doesn't rely on the loader so we can access that
437332
const nonce = useNonce()
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { useForm, getFormProps } from '@conform-to/react'
2+
import { parseWithZod } from '@conform-to/zod'
3+
import { invariantResponse } from '@epic-web/invariant'
4+
import {
5+
json,
6+
type ActionFunctionArgs,
7+
} from '@remix-run/node'
8+
import {
9+
useFetcher,
10+
useFetchers,
11+
} from '@remix-run/react'
12+
import { z } from 'zod'
13+
import { Icon } from '#app/components/ui/icon.tsx'
14+
import { useHints } from '#app/utils/client-hints.tsx'
15+
import { useRequestInfo } from '#app/utils/request-info.ts'
16+
import { type Theme, setTheme } from '#app/utils/theme.server.ts'
17+
18+
const ThemeFormSchema = z.object({
19+
theme: z.enum(['system', 'light', 'dark']),
20+
})
21+
22+
export async function action({ request }: ActionFunctionArgs) {
23+
const formData = await request.formData()
24+
const submission = parseWithZod(formData, {
25+
schema: ThemeFormSchema,
26+
})
27+
28+
invariantResponse(submission.status === 'success', 'Invalid theme received')
29+
30+
const { theme } = submission.value
31+
32+
const responseInit = {
33+
headers: { 'set-cookie': setTheme(theme) },
34+
}
35+
return json({ result: submission.reply() }, responseInit)
36+
}
37+
38+
export function ThemeSwitch({ userPreference }: { userPreference?: Theme | null }) {
39+
const fetcher = useFetcher<typeof action>()
40+
41+
const [form] = useForm({
42+
id: 'theme-switch',
43+
lastResult: fetcher.data?.result,
44+
})
45+
46+
const optimisticMode = useOptimisticThemeMode()
47+
const mode = optimisticMode ?? userPreference ?? 'system'
48+
const nextMode =
49+
mode === 'system' ? 'light' : mode === 'light' ? 'dark' : 'system'
50+
const modeLabel = {
51+
light: (
52+
<Icon name="sun">
53+
<span className="sr-only">Light</span>
54+
</Icon>
55+
),
56+
dark: (
57+
<Icon name="moon">
58+
<span className="sr-only">Dark</span>
59+
</Icon>
60+
),
61+
system: (
62+
<Icon name="laptop">
63+
<span className="sr-only">System</span>
64+
</Icon>
65+
),
66+
}
67+
68+
return (
69+
<fetcher.Form method="POST" {...getFormProps(form)} action="/resources/theme-switch">
70+
<input type="hidden" name="theme" value={nextMode} />
71+
<div className="flex gap-2">
72+
<button
73+
type="submit"
74+
className="flex h-8 w-8 cursor-pointer items-center justify-center"
75+
>
76+
{modeLabel[mode]}
77+
</button>
78+
</div>
79+
</fetcher.Form>
80+
)
81+
}
82+
83+
/**
84+
* If the user's changing their theme mode preference, this will return the
85+
* value it's being changed to.
86+
*/
87+
export function useOptimisticThemeMode() {
88+
const fetchers = useFetchers()
89+
const themeFetcher = fetchers.find(f => f.formAction === '/resources/theme-switch')
90+
91+
if (themeFetcher && themeFetcher.formData) {
92+
const submission = parseWithZod(themeFetcher.formData, {
93+
schema: ThemeFormSchema,
94+
})
95+
96+
if (submission.status === 'success') {
97+
return submission.value.theme
98+
}
99+
}
100+
}
101+
102+
/**
103+
* @returns the user's theme preference, or the client hint theme if the user
104+
* has not set a preference.
105+
*/
106+
export function useTheme() {
107+
const hints = useHints()
108+
const requestInfo = useRequestInfo()
109+
const optimisticMode = useOptimisticThemeMode()
110+
if (optimisticMode) {
111+
return optimisticMode === 'system' ? hints.theme : optimisticMode
112+
}
113+
return requestInfo.userPrefs.theme ?? hints.theme
114+
}

0 commit comments

Comments
 (0)