Skip to content

Commit dc8b42f

Browse files
Add server-rendered Remix hydration (#76)
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 6c3d6ba commit dc8b42f

40 files changed

Lines changed: 1709 additions & 334 deletions

.github/workflows/deploy.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ jobs:
5353
name: 🚀 Deploy to production
5454
needs: sha-guard
5555
if: needs.sha-guard.outputs.should_deploy == 'true'
56+
env:
57+
CLOUDFLARE_ACCOUNT_ID:
58+
${{ vars.CLOUDFLARE_ACCOUNT_ID || secrets.CLOUDFLARE_ACCOUNT_ID }}
5659
environment:
5760
name: production
5861
url: ${{ steps.deploy.outputs.url }}

.github/workflows/preview.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ jobs:
5252
deploy:
5353
runs-on: ubuntu-latest
5454
name: 🔎 Deploy Preview Resources
55+
env:
56+
CLOUDFLARE_ACCOUNT_ID:
57+
${{ vars.CLOUDFLARE_ACCOUNT_ID || secrets.CLOUDFLARE_ACCOUNT_ID }}
5558
environment:
5659
name:
5760
preview-${{ github.event.pull_request.number || inputs.pr_number ||
@@ -433,6 +436,9 @@ jobs:
433436
cleanup:
434437
runs-on: ubuntu-latest
435438
name: 🧹 Cleanup Preview Resources
439+
env:
440+
CLOUDFLARE_ACCOUNT_ID:
441+
${{ vars.CLOUDFLARE_ACCOUNT_ID || secrets.CLOUDFLARE_ACCOUNT_ID }}
436442
if: >-
437443
(github.event_name == 'pull_request' &&
438444
github.event.action == 'closed' &&

client/app-root.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { clientEntry, type EntryComponent, type Handle } from 'remix/ui'
2+
import { type AppLoaderData } from '#shared/loader-data.ts'
3+
import { App } from './app.tsx'
4+
import { AppLoaderDataProvider } from './loader-data-context.tsx'
5+
import { RouterLocationProvider } from './router-location.tsx'
6+
import { type SessionInfo } from './session.ts'
7+
8+
export const APP_ROOT_ENTRY_ID = '/client-entry.js#AppRoot'
9+
10+
export type AppRootProps = {
11+
url: string
12+
session: SessionInfo | null
13+
loaderData?: AppLoaderData
14+
notFound?: boolean
15+
}
16+
17+
export const AppRoot: EntryComponent<AppRootProps> = clientEntry(
18+
APP_ROOT_ENTRY_ID,
19+
function AppRoot(handle: Handle<AppRootProps>) {
20+
return () => (
21+
<RouterLocationProvider url={handle.props.url}>
22+
<AppLoaderDataProvider loaderData={handle.props.loaderData}>
23+
<App
24+
embeddedSession={handle.props.session}
25+
notFound={handle.props.notFound === true}
26+
/>
27+
</AppLoaderDataProvider>
28+
</RouterLocationProvider>
29+
)
30+
},
31+
)

client/app.tsx

Lines changed: 76 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { css, type Handle } from 'remix/ui'
2-
import { clientRoutes } from './routes/index.tsx'
2+
import { routes } from '#server/routes.ts'
3+
import { clientRouteLoaders, clientRoutes } from './routes/index.tsx'
34
import {
45
getPathname,
56
listenToRouterNavigation,
@@ -10,14 +11,21 @@ import {
1011
type SessionInfo,
1112
type SessionStatus,
1213
} from './session.ts'
14+
import { SessionProvider } from './session-context.tsx'
1315
import { buildAuthLink } from './auth-links.ts'
1416
import { colors, mq, spacing, typography } from './styles/tokens.ts'
15-
export function App(handle: Handle) {
16-
let session: SessionInfo | null = null
17-
let sessionStatus: SessionStatus = 'idle'
17+
18+
type AppProps = {
19+
embeddedSession?: SessionInfo | null
20+
notFound?: boolean
21+
}
22+
23+
export function App(handle: Handle<AppProps>) {
24+
let session: SessionInfo | null = handle.props?.embeddedSession ?? null
25+
let sessionStatus: SessionStatus = 'ready'
1826
let sessionRefreshInFlight = false
1927
let sessionRefreshQueued = false
20-
let currentPathname = getPathname()
28+
let currentPathname = getPathname(handle)
2129
function queueSessionRefresh() {
2230
sessionRefreshQueued = true
2331
if (sessionRefreshInFlight) return
@@ -29,9 +37,22 @@ export function App(handle: Handle) {
2937
sessionRefreshQueued = false
3038
sessionRefreshInFlight = true
3139
handle.queueTask(async (signal) => {
32-
const nextSession = await fetchSessionInfo(signal)
40+
let nextSession: SessionInfo | null = null
41+
try {
42+
nextSession = await fetchSessionInfo(signal)
43+
} catch (error) {
44+
sessionRefreshInFlight = false
45+
if (signal.aborted) return
46+
console.error('Session refresh failed', error)
47+
sessionStatus = 'ready'
48+
handle.update()
49+
return
50+
}
3351
sessionRefreshInFlight = false
34-
if (signal.aborted) return
52+
if (signal.aborted) {
53+
if (sessionRefreshQueued) queueSessionRefresh()
54+
return
55+
}
3556
session = nextSession
3657
sessionStatus = 'ready'
3758
handle.update()
@@ -43,11 +64,13 @@ export function App(handle: Handle) {
4364
handle.update()
4465
}
4566
}
46-
handle.queueTask(() => {
47-
queueSessionRefresh()
48-
})
67+
if (typeof window !== 'undefined') {
68+
handle.queueTask(() => {
69+
queueSessionRefresh()
70+
})
71+
}
4972
listenToRouterNavigation(handle, () => {
50-
currentPathname = getPathname()
73+
currentPathname = getPathname(handle)
5174
queueSessionRefresh()
5275
handle.update()
5376
})
@@ -85,11 +108,12 @@ export function App(handle: Handle) {
85108
const isLoggedIn = isSessionReady && Boolean(sessionEmail)
86109
const showAuthLinks = isSessionReady && !isLoggedIn
87110
const oauthRedirectTo =
88-
typeof window !== 'undefined' && currentPathname === '/oauth/authorize'
111+
typeof window !== 'undefined' &&
112+
currentPathname === routes.oauthAuthorize.href()
89113
? `${currentPathname}${window.location.search}`
90114
: null
91-
const loginHref = buildAuthLink('/login', oauthRedirectTo)
92-
const signupHref = buildAuthLink('/signup', oauthRedirectTo)
115+
const loginHref = buildAuthLink(routes.login.href(), oauthRedirectTo)
116+
const signupHref = buildAuthLink(routes.signup.href(), oauthRedirectTo)
93117
return (
94118
<main
95119
mix={[
@@ -126,7 +150,11 @@ export function App(handle: Handle) {
126150
}),
127151
]}
128152
>
129-
<a href="/" aria-label="Home" mix={[css(navHomeLinkCss)]}>
153+
<a
154+
href={routes.home.href()}
155+
aria-label="Home"
156+
mix={[css(navHomeLinkCss)]}
157+
>
130158
<img
131159
src="/logo.png"
132160
alt=""
@@ -153,42 +181,50 @@ export function App(handle: Handle) {
153181
) : null}
154182
{isLoggedIn ? (
155183
<>
156-
<a href="/chat" mix={[css(navLinkCss)]}>
184+
<a href={routes.chat.href()} mix={[css(navLinkCss)]}>
157185
Chat
158186
</a>
159-
<a href="/account" mix={[css(navLinkCss)]}>
187+
<a href={routes.account.href()} mix={[css(navLinkCss)]}>
160188
{sessionEmail}
161189
</a>
162-
<form method="post" action="/logout" mix={[css({ margin: 0 })]}>
190+
<form
191+
method="post"
192+
action={routes.logout.href()}
193+
mix={[css({ margin: 0 })]}
194+
>
163195
<button type="submit" mix={[css(logOutButtonCss)]}>
164196
Log out
165197
</button>
166198
</form>
167199
</>
168200
) : null}
169201
</nav>
170-
<Router
171-
routes={clientRoutes}
172-
fallback={
173-
<section>
174-
<h2
175-
mix={[
176-
css({
177-
fontSize: typography.fontSize.lg,
178-
fontWeight: typography.fontWeight.semibold,
179-
marginBottom: spacing.sm,
180-
color: colors.text,
181-
}),
182-
]}
183-
>
184-
Not Found
185-
</h2>
186-
<p mix={[css({ color: colors.textMuted })]}>
187-
That route does not exist.
188-
</p>
189-
</section>
190-
}
191-
/>
202+
<SessionProvider session={session}>
203+
<Router
204+
loaders={clientRouteLoaders}
205+
routes={clientRoutes}
206+
notFound={handle.props?.notFound === true}
207+
fallback={
208+
<section>
209+
<h2
210+
mix={[
211+
css({
212+
fontSize: typography.fontSize.lg,
213+
fontWeight: typography.fontWeight.semibold,
214+
marginBottom: spacing.sm,
215+
color: colors.text,
216+
}),
217+
]}
218+
>
219+
Not Found
220+
</h2>
221+
<p mix={[css({ color: colors.textMuted })]}>
222+
That route does not exist.
223+
</p>
224+
</section>
225+
}
226+
/>
227+
</SessionProvider>
192228
</main>
193229
)
194230
}

0 commit comments

Comments
 (0)