NextJS page router layout function - faster, lower flicker.

This commit is contained in:
Enrico Ros
2024-07-30 02:40:33 -07:00
parent ce93ab8234
commit 575efb07f4
16 changed files with 95 additions and 71 deletions
+10 -3
View File
@@ -24,8 +24,14 @@ import { hasGoogleAnalytics, OptionalGoogleAnalytics } from '~/common/components
import { isVercelFromFrontend } from '~/common/util/pwaUtils';
const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
<>
const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) => {
// We are using a nextjs per-page layout pattern to bring the (Optima) layout creation to a shared place
// This reduces the flicker and the time switching between apps, and seems to not have impact on
// the build. This is a good trade-off for now.
const getLayout = Component.getLayout ?? ((page: any) => page);
return <>
<Head>
<title>{Brand.Title.Common}</title>
@@ -39,7 +45,7 @@ const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
{/* ^ SSR boundary */}
<ProviderBootstrapLogic>
<ProviderSnacks>
<Component {...pageProps} />
{getLayout(<Component {...pageProps} />)}
</ProviderSnacks>
</ProviderBootstrapLogic>
</ProviderBackendCapabilities>
@@ -52,6 +58,7 @@ const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
{hasGoogleAnalytics && <OptionalGoogleAnalytics />}
</>;
};
// enables the React Query API invocation
export default apiQuery.withTRPC(MyApp);
+2 -4
View File
@@ -2,9 +2,7 @@ import * as React from 'react';
import { AppCall } from '../src/apps/call/AppCall';
import { withLayout } from '~/common/layout/withLayout';
import { withNextJSPerPageLayout } from '~/common/layout/withLayout';
export default function CallPage() {
return withLayout({ type: 'optima' }, <AppCall />);
}
export default withNextJSPerPageLayout({ type: 'optima' }, () => <AppCall />);
+2 -4
View File
@@ -2,9 +2,7 @@ import * as React from 'react';
import { AppBeam } from '../../src/apps/beam/AppBeam';
import { withLayout } from '~/common/layout/withLayout';
import { withNextJSPerPageLayout } from '~/common/layout/withLayout';
export default function BeamPage() {
return withLayout({ type: 'optima' }, <AppBeam />);
}
export default withNextJSPerPageLayout({ type: 'optima' }, () => <AppBeam />);
+2 -4
View File
@@ -2,9 +2,7 @@ import * as React from 'react';
import { AppDiff } from '../src/apps/diff/AppDiff';
import { withLayout } from '~/common/layout/withLayout';
import { withNextJSPerPageLayout } from '~/common/layout/withLayout';
export default function DiffPage() {
return withLayout({ type: 'optima' }, <AppDiff />);
}
export default withNextJSPerPageLayout({ type: 'optima' }, () => <AppDiff />);
+2 -4
View File
@@ -2,9 +2,7 @@ import * as React from 'react';
import { AppDraw } from '../src/apps/draw/AppDraw';
import { withLayout } from '~/common/layout/withLayout';
import { withNextJSPerPageLayout } from '~/common/layout/withLayout';
export default function DrawPage() {
return withLayout({ type: 'optima' }, <AppDraw />);
}
export default withNextJSPerPageLayout({ type: 'optima' }, () => <AppDraw />);
+7 -4
View File
@@ -2,13 +2,16 @@ import * as React from 'react';
import { AppChat } from '../src/apps/chat/AppChat';
import { withLayout } from '~/common/layout/withLayout';
import { withNextJSPerPageLayout } from '~/common/layout/withLayout';
import { useDebugHook } from '~/common/components/useDebugHook';
export default function IndexPage() {
export default withNextJSPerPageLayout({ type: 'optima' }, () => {
useDebugHook('IndexPage');
// TODO: This Index page will point to the Dashboard (or a landing page)
// For now it offers the chat experience, but this will change. #299
return withLayout({ type: 'optima' }, <AppChat />);
}
return <AppChat />;
});
+2 -4
View File
@@ -9,7 +9,7 @@ import { AppPlaceholder } from '../../src/apps/AppPlaceholder';
import { getBackendCapabilities } from '~/modules/backend/store-backend-capabilities';
import { getPlantUmlServerUrl } from '~/modules/blocks/code/RenderCode';
import { withLayout } from '~/common/layout/withLayout';
import { withNextJSPerPageLayout } from '~/common/layout/withLayout';
// app config
@@ -164,6 +164,4 @@ function AppDebug() {
}
export default function DebugPage() {
return withLayout({ type: 'plain' }, <AppDebug />);
};
export default withNextJSPerPageLayout({ type: 'plain' }, () => <AppDebug />);
+5 -4
View File
@@ -7,7 +7,7 @@ import { useModelsStore } from '~/modules/llms/store-llms';
import { InlineError } from '~/common/components/InlineError';
import { apiQuery } from '~/common/util/trpc.client';
import { navigateToIndex, useRouterQuery } from '~/common/app.routes';
import { withLayout } from '~/common/layout/withLayout';
import { withNextJSPerPageLayout } from '~/common/layout/withLayout';
function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
@@ -81,10 +81,11 @@ function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
* Docs: https://openrouter.ai/docs#oauth
* Example URL: https://localhost:3000/link/callback_openrouter?code=SomeCode
*/
export default function CallbackPage() {
export default withNextJSPerPageLayout({ type: 'plain' }, () => {
// external state - get the 'code=...' from the URL
const { code } = useRouterQuery<{ code: string | undefined }>();
return withLayout({ type: 'plain' }, <CallbackOpenRouterPage openRouterCode={code} />);
}
return <CallbackOpenRouterPage openRouterCode={code} />;
});
+5 -4
View File
@@ -3,13 +3,14 @@ import * as React from 'react';
import { AppLinkChat } from '../../../src/apps/link-chat/AppLinkChat';
import { useRouterQuery } from '~/common/app.routes';
import { withLayout } from '~/common/layout/withLayout';
import { withNextJSPerPageLayout } from '~/common/layout/withLayout';
export default function ChatLinkPage() {
export default withNextJSPerPageLayout({ type: 'optima', suspendAutoModelsSetup: true }, () => {
// external state
const { chatLinkId } = useRouterQuery<{ chatLinkId: string | undefined }>();
return withLayout({ type: 'optima', suspendAutoModelsSetup: true }, <AppLinkChat chatLinkId={chatLinkId || null} />);
}
return <AppLinkChat chatLinkId={chatLinkId || null} />;
});
+2 -4
View File
@@ -10,7 +10,7 @@ import { callBrowseFetchPage } from '~/modules/browse/browse.client';
import { LogoProgress } from '~/common/components/LogoProgress';
import { asValidURL } from '~/common/util/urlUtils';
import { navigateToIndex, useRouterQuery } from '~/common/app.routes';
import { withLayout } from '~/common/layout/withLayout';
import { withNextJSPerPageLayout } from '~/common/layout/withLayout';
/**
@@ -135,6 +135,4 @@ function AppShareTarget() {
* This page will be invoked on mobile when sharing Text/URLs/Files from other APPs
* Example URL: https://localhost:3000/link/share_target?title=This+Title&text=https%3A%2F%2Fexample.com%2Fapp%2Fpath
*/
export default function ShareTargetPage() {
return withLayout({ type: 'plain' }, <AppShareTarget />);
}
export default withNextJSPerPageLayout({ type: 'plain' }, () => <AppShareTarget />);
+5 -4
View File
@@ -3,12 +3,13 @@ import * as React from 'react';
import { AppNews } from '../src/apps/news/AppNews';
import { markNewsAsSeen } from '../src/apps/news/news.version';
import { withLayout } from '~/common/layout/withLayout';
import { withNextJSPerPageLayout } from '~/common/layout/withLayout';
export default function NewsPage() {
export default withNextJSPerPageLayout({ type: 'optima', suspendAutoModelsSetup: true }, () => {
// 'touch' the last seen news version
React.useEffect(() => markNewsAsSeen(), []);
return withLayout({ type: 'optima', suspendAutoModelsSetup: true }, <AppNews />);
}
return <AppNews />;
});
+2 -4
View File
@@ -2,9 +2,7 @@ import * as React from 'react';
import { AppPersonas } from '../src/apps/personas/AppPersonas';
import { withLayout } from '~/common/layout/withLayout';
import { withNextJSPerPageLayout } from '~/common/layout/withLayout';
export default function PersonasPage() {
return withLayout({ type: 'optima' }, <AppPersonas />);
}
export default withNextJSPerPageLayout({ type: 'optima' }, () => <AppPersonas />);
+2 -4
View File
@@ -2,9 +2,7 @@ import * as React from 'react';
import { AppTokens } from '../src/apps/tokens/AppTokens';
import { withLayout } from '~/common/layout/withLayout';
import { withNextJSPerPageLayout } from '~/common/layout/withLayout';
export default function TokenizerPage() {
return withLayout({ type: 'optima' }, <AppTokens />);
}
export default withNextJSPerPageLayout({ type: 'optima' }, () => <AppTokens />);
+3 -7
View File
@@ -1,12 +1,8 @@
import * as React from 'react';
import { Box } from '@mui/joy';
import { AppPlaceholder } from '../src/apps/AppPlaceholder';
// import { AppWorkspace } from '../src/apps/personas/AppWorkspace';
import { withLayout } from '~/common/layout/withLayout';
import { withNextJSPerPageLayout } from '~/common/layout/withLayout';
export default function PersonasPage() {
return withLayout({ type: 'optima' }, <Box />);
}
export default withNextJSPerPageLayout({ type: 'optima' }, () => <AppPlaceholder />);
+35 -7
View File
@@ -1,10 +1,12 @@
import * as React from 'react';
import type { NextPageWithLayout } from '~/common/types/next.page';
import { OptimaLayout } from './optima/OptimaLayout';
import { PlainLayout } from './plain/PlainLayout';
type WithLayout = {
type PerPageLayoutOptions = {
type: 'optima';
suspendAutoModelsSetup?: boolean;
} | {
@@ -13,23 +15,49 @@ type WithLayout = {
/**
* Dynamic page-level layouting: a wrapper that adds the layout around the children.
* Next.js page-level layouting: a wrapper that adds the layout around the page as a layout function.
*/
export function withLayout(layoutOptions: WithLayout, children: React.ReactNode): React.ReactElement {
export function withNextJSPerPageLayout(options: PerPageLayoutOptions, page: NextPageWithLayout): NextPageWithLayout {
const { type, ...rest } = layoutOptions;
const { type, ...rest } = options;
switch (type) {
case 'optima':
return <OptimaLayout {...rest}>{children}</OptimaLayout>;
page.getLayout = (page: React.ReactElement) => <OptimaLayout {...rest}>{page}</OptimaLayout>;
return page;
case 'plain':
return <PlainLayout {...rest}>{children}</PlainLayout>;
page.getLayout = (page: React.ReactElement) => <PlainLayout {...rest}>{page}</PlainLayout>;
return page;
default:
console.error('No layout specified for this top-level page');
return <>{children}</>;
return page;
}
}
// /**
// * Dynamic page-level layouting: a wrapper that adds the layout around the children.
// */
// export function withLayout(layoutOptions: LayoutOptions, children: React.ReactNode): React.ReactElement {
//
// const { type, ...rest } = layoutOptions;
//
// switch (type) {
//
// case 'optima':
// return <OptimaLayout {...rest}>{children}</OptimaLayout>;
//
// case 'plain':
// return <PlainLayout {...rest}>{children}</PlainLayout>;
//
// default:
// console.error('No layout specified for this top-level page');
// return <>{children}</>;
//
// }
// }
//
+9 -6
View File
@@ -1,17 +1,20 @@
import type { ReactElement, ReactNode } from 'react';
import type { NextPage } from 'next';
import type { EmotionCache } from '@emotion/react';
// export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
// // require .layoutOptions on the page component
// layoutOptions: LayoutOptions;
// };
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
// definition of the per-page layout function, as per:
// https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts#per-page-layouts
getLayout?: (page: ReactElement) => ReactNode;
}
// Extend the AppProps type with the custom page component type
declare module 'next/app' {
import { AppProps } from 'next/app';
type MyAppProps = AppProps & {
// Component: NextPageWithLayout;
Component: NextPageWithLayout
emotionCache?: EmotionCache;
};
}
}