diff --git a/templates/ords-remix-jwt-sample/app/entry.client.tsx b/templates/ords-remix-jwt-sample/app/entry.client.tsx
index f9084b1..7436180 100644
--- a/templates/ords-remix-jwt-sample/app/entry.client.tsx
+++ b/templates/ords-remix-jwt-sample/app/entry.client.tsx
@@ -21,13 +21,30 @@ import {
import { hydrateRoot } from 'react-dom/client';
import { MuiProvider } from './mui/MuiProvider';
+const SERVER_HEAD_PATTERN = /[\s\S]*?/;
+
+/**
+ * Removes the static server-rendered head so the client portal can own it.
+ */
+function removeServerHead() : void {
+ document.head.innerHTML = document.head.innerHTML.replace(SERVER_HEAD_PATTERN, '');
+}
+
startTransition(() => {
+ const rootElement = document.getElementById('root');
+
+ if (rootElement === null) {
+ throw new Error('Could not find the root element to hydrate.');
+ }
+
hydrateRoot(
- document,
+ rootElement,
,
);
+
+ removeServerHead();
});
diff --git a/templates/ords-remix-jwt-sample/app/entry.server.tsx b/templates/ords-remix-jwt-sample/app/entry.server.tsx
index 058a20e..89037f2 100644
--- a/templates/ords-remix-jwt-sample/app/entry.server.tsx
+++ b/templates/ords-remix-jwt-sample/app/entry.server.tsx
@@ -11,126 +11,24 @@
/* eslint-disable jsdoc/require-param-description */
/* eslint-disable jsdoc/require-returns */
/* eslint-disable jsdoc/require-jsdoc */
-/* eslint-disable no-magic-numbers */
/* eslint-disable no-console */
-/* eslint-disable no-param-reassign */
-import { PassThrough } from 'node:stream';
-
import type {
AppLoadContext, EntryContext,
} from '@remix-run/node';
-import { createReadableStreamFromReadable } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
-import { isbot } from 'isbot';
-import { renderToPipeableStream } from 'react-dom/server';
+import { renderToString } from 'react-dom/server';
import { MuiProvider } from './mui/MuiProvider';
+import createEmotionCache from './mui/createEmotionCache';
+import { Head } from './root';
-const ABORT_DELAY = 5_000;
-
-function handleBotRequest(
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- remixContext: EntryContext,
-) {
- return new Promise((resolve, reject) => {
- let shellRendered = false;
- const {
- pipe, abort,
- } = renderToPipeableStream(
- ,
- {
- onAllReady() {
- shellRendered = true;
- const body = new PassThrough();
- const stream = createReadableStreamFromReadable(body);
-
- responseHeaders.set('Content-Type', 'text/html');
+const HEAD_START = '';
+const HEAD_END = '';
- resolve(
- new Response(stream, {
- headers: responseHeaders,
- status: responseStatusCode,
- }),
- );
-
- pipe(body);
- },
- onShellError(error: unknown) {
- reject(error);
- },
- onError(error: unknown) {
- responseStatusCode = 500;
- // Log streaming rendering errors from inside the shell. Don't log
- // errors encountered during initial shell rendering since they'll
- // reject and get logged in handleDocumentRequest.
- if (shellRendered) {
- console.error(error);
- }
- },
- },
- );
-
- setTimeout(abort, ABORT_DELAY);
- });
-}
-
-function handleBrowserRequest(
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- remixContext: EntryContext,
-) {
- return new Promise((resolve, reject) => {
- let shellRendered = false;
- const {
- pipe, abort,
- } = renderToPipeableStream(
-
-
- ,
- {
- onShellReady() {
- shellRendered = true;
- const body = new PassThrough();
- const stream = createReadableStreamFromReadable(body);
-
- responseHeaders.set('Content-Type', 'text/html');
-
- resolve(
- new Response(stream, {
- headers: responseHeaders,
- status: responseStatusCode,
- }),
- );
-
- pipe(body);
- },
- onShellError(error: unknown) {
- reject(error);
- },
- onError(error: unknown) {
- responseStatusCode = 500;
- // Log streaming rendering errors from inside the shell. Don't log
- // errors encountered during initial shell rendering since they'll
- // reject and get logged in handleDocumentRequest.
- if (shellRendered) {
- console.error(error);
- }
- },
- },
- );
-
- setTimeout(abort, ABORT_DELAY);
- });
+function renderDocument(
+ head: string,
+ body: string,
+): string {
+ return `
${HEAD_START}${head}${HEAD_END}${body}
`;
}
/**
@@ -154,17 +52,33 @@ export default function handleRequest(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadContext: AppLoadContext,
) {
- return isbot(request.headers.get('user-agent') || '')
- ? handleBotRequest(
- request,
- responseStatusCode,
- responseHeaders,
- remixContext,
- )
- : handleBrowserRequest(
- request,
- responseStatusCode,
- responseHeaders,
- remixContext,
- );
+ const rootRouteModule = remixContext.routeModules.root;
+ const headContext: EntryContext = {
+ ...remixContext,
+ routeModules: {
+ ...remixContext.routeModules,
+ root: {
+ ...rootRouteModule,
+ default: Head,
+ ErrorBoundary: Head,
+ },
+ },
+ };
+
+ const head = renderToString(
+ ,
+ );
+
+ const body = renderToString(
+
+
+ ,
+ );
+
+ responseHeaders.set('Content-Type', 'text/html');
+
+ return new Response(renderDocument(head, body), {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ });
}
diff --git a/templates/ords-remix-jwt-sample/app/mui/MuiProvider.tsx b/templates/ords-remix-jwt-sample/app/mui/MuiProvider.tsx
index 7fa0e31..7eb8301 100644
--- a/templates/ords-remix-jwt-sample/app/mui/MuiProvider.tsx
+++ b/templates/ords-remix-jwt-sample/app/mui/MuiProvider.tsx
@@ -7,28 +7,29 @@
import { CacheProvider } from '@emotion/react';
import React from 'react';
import { ThemeProvider } from '@mui/material';
-import createCache from '@emotion/cache';
import type { EmotionCache } from '@emotion/react';
+import createEmotionCache from './createEmotionCache';
import theme from './theme';
-/**
- * Create an instance of the emotion cache on every request to make the style
- * configuration available to all components in the component tree.
- * @returns an instance of the emotion cache.
- */
-function createEmotionCache(): EmotionCache {
- return createCache({ key: 'css' });
+interface MuiProviderProps {
+ children: React.ReactNode;
+ emotionCache?: EmotionCache;
}
+
/**
* Provider used to wrap the App root component to make all of the
* MUI style configurations available.
* @param root0 the Page React Nodes.
* @param root0.children the page React Node children's.
+ * @param root0.emotionCache the Emotion cache used for SSR and hydration.
* @see {@link https://mui.com/material-ui/guides/server-rendering/}
* @returns the react root component.
*/
-export function MuiProvider({ children }: { children: React.ReactNode }): React.ReactNode {
- const cache = createEmotionCache();
+export function MuiProvider({
+ children,
+ emotionCache,
+}: MuiProviderProps): React.ReactNode {
+ const [cache] = React.useState(() => emotionCache ?? createEmotionCache());
return (
diff --git a/templates/ords-remix-jwt-sample/app/mui/createEmotionCache.tsx b/templates/ords-remix-jwt-sample/app/mui/createEmotionCache.tsx
new file mode 100644
index 0000000..d00776e
--- /dev/null
+++ b/templates/ords-remix-jwt-sample/app/mui/createEmotionCache.tsx
@@ -0,0 +1,18 @@
+/*
+**
+** Copyright (c) 2024, Oracle and/or its affiliates.
+** All rights reserved
+** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
+*/
+import createCache from '@emotion/cache';
+import type { EmotionCache } from '@emotion/react';
+
+/**
+ * Creates the Emotion cache used by both the server and the client.
+ * @returns the Emotion cache instance.
+ */
+export default function createEmotionCache(): EmotionCache {
+ return createCache({
+ key: 'css',
+ });
+}
diff --git a/templates/ords-remix-jwt-sample/app/root.tsx b/templates/ords-remix-jwt-sample/app/root.tsx
index 4adc44d..cc69a29 100644
--- a/templates/ords-remix-jwt-sample/app/root.tsx
+++ b/templates/ords-remix-jwt-sample/app/root.tsx
@@ -5,11 +5,13 @@
** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
*/
import type {
+ LinksFunction,
LoaderFunctionArgs,
} from '@remix-run/node';
import { json, ErrorResponse } from '@remix-run/node';
import {
Links,
+ LiveReload,
Meta,
Outlet,
Scripts,
@@ -18,8 +20,11 @@ import {
useLoaderData,
useRouteError,
} from '@remix-run/react';
-import { ReactElement } from 'react';
-import { StyledEngineProvider } from '@mui/material';
+import {
+ ReactElement, ReactNode, useEffect, useState,
+} from 'react';
+import { createPortal } from 'react-dom';
+import datepicker from 'react-datepicker/dist/react-datepicker.css';
import stylesheet from './tailwind.css?url';
import type { LoaderError } from './models/LoaderError';
import {
@@ -27,6 +32,7 @@ import {
} from './utils/auth.server';
import NavBar from './components/navbar/NavBar';
import {
+ BASIC_SCHEMA_AUTH,
CITIES_ENDPOINT,
} from './routes/constants/index.server';
import TooltipButton from './components/tooltips/TooltipButton';
@@ -38,12 +44,12 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
const userProfile = await auth.isAuthenticated(request);
const profile = userProfile?.profile || null;
const USER_CREDENTIALS = userProfile === null
- ? null
+ ? BASIC_SCHEMA_AUTH
: `${userProfile.tokenType} ${userProfile.accessToken}`;
const session = await getSession(request.headers.get('Cookie'));
const error = session.get(auth.sessionErrorKey) as LoaderError;
- const cities = await ORDSFetcher(CITIES_ENDPOINT, USER_CREDENTIALS!);
+ const cities = await ORDSFetcher(CITIES_ENDPOINT, USER_CREDENTIALS);
if (cities.items.length === 0) {
const errorMessage = 'The cities endpoint has no elements. Review your database configuration and try again.';
throw new Response(errorMessage, {
@@ -59,6 +65,64 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
});
};
+export const links: LinksFunction = () => [
+ {
+ rel: 'stylesheet',
+ href: stylesheet,
+ },
+ {
+ rel: 'stylesheet',
+ href: datepicker,
+ },
+];
+
+/**
+ * Renders the route metadata and stylesheet links for the document head.
+ * @returns the head contents.
+ */
+export function Head(): ReactElement {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+/**
+ * Mounts the head contents into `document.head` after hydration finishes.
+ * @returns the client-only head portal.
+ */
+function HydratedHead() {
+ const [isHydrated, setIsHydrated] = useState(false);
+
+ useEffect(() => {
+ setIsHydrated(true);
+ }, []);
+
+ return isHydrated ? createPortal(, document.head) : null;
+}
+
+/**
+ * Shared app shell used by both the main app and the root error boundary.
+ * @param root0 the shell children.
+ * @param root0.children the rendered route content.
+ * @returns the app shell.
+ */
+function AppShell({ children }: { children: ReactNode }): ReactElement {
+ return (
+ <>
+
+ {children}
+
+
+
+ >
+ );
+}
+
/**
*
* @returns Display the error page.
@@ -67,22 +131,10 @@ export function ErrorBoundary() : ReactElement {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
);
} if (error instanceof Error) {
const unknownError : ErrorResponse = {
@@ -91,30 +143,19 @@ export function ErrorBoundary() : ReactElement {
data: error.stack,
};
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
);
}
- return Unknown Error
;
-}
-export const links = () => [
- { rel: 'stylesheet', href: stylesheet },
-];
+ return (
+
+ Unknown Error
+
+ );
+}
/**
*
@@ -126,23 +167,11 @@ export default function App() : ReactElement {
cities,
} = useLoaderData();
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
);
}