Skip to content

Commit

Permalink
Merge pull request #744 from thunderstore-io/ssr-context
Browse files Browse the repository at this point in the history
CS: Implement SSR compatible dapper context
  • Loading branch information
MythicManiac authored Jul 14, 2023
2 parents 71a6ad2 + cec3683 commit 921cecc
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 61 deletions.
19 changes: 11 additions & 8 deletions apps/cyberstorm-nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import "@thunderstore/cyberstorm-styles";
import Providers from "@/utils/provider";
import { CyberstormProviders } from "@thunderstore/cyberstorm";
import React from "react";
import { ServerDapper } from "@/dapper/server";
import { ClientDapper } from "@/dapper/client";

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
export default function RootLayout(props: React.PropsWithChildren) {
return (
<html lang="en">
<body>
<CyberstormProviders>
<Providers>{children}</Providers>
</CyberstormProviders>
<ServerDapper>
<ClientDapper>
<CyberstormProviders>
<Providers>{props.children}</Providers>
</CyberstormProviders>
</ClientDapper>
</ServerDapper>
</body>
</html>
);
Expand Down
14 changes: 14 additions & 0 deletions apps/cyberstorm-nextjs/dapper/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use client";
import React from "react";

import { Dapper, DapperProvider } from "@thunderstore/dapper/src";
import { API_DOMAIN } from "@/utils/constants";

export function ClientDapper(props: React.PropsWithChildren) {
const dapperConstructor = () => new Dapper(API_DOMAIN, undefined);
return (
<DapperProvider dapperConstructor={dapperConstructor}>
<>{props.children}</>
</DapperProvider>
);
}
13 changes: 13 additions & 0 deletions apps/cyberstorm-nextjs/dapper/server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";

import { Dapper, DapperProvider } from "@thunderstore/dapper/src";
import { API_DOMAIN } from "@/utils/constants";

export function ServerDapper(props: React.PropsWithChildren) {
const dapperConstructor = () => new Dapper(API_DOMAIN, undefined);
return (
<DapperProvider dapperConstructor={dapperConstructor}>
<>{props.children}</>
</DapperProvider>
);
}
17 changes: 2 additions & 15 deletions apps/cyberstorm-nextjs/utils/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { QueryClientProvider, QueryClient } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
import { LinkLibrary } from "@/utils/LinkLibrary";
import { LinkingProvider } from "@thunderstore/cyberstorm";
import { Dapper, DapperProvider } from "@thunderstore/dapper/src";
import { SessionProvider, useSession } from "./SessionContext";
import { API_DOMAIN } from "./constants";
import { SessionProvider } from "./SessionContext";

function Providers({ children }: React.PropsWithChildren) {
const [client] = React.useState(
Expand All @@ -17,22 +15,11 @@ function Providers({ children }: React.PropsWithChildren) {
return (
<QueryClientProvider client={client}>
<SessionProvider>
<Substack>{children}</Substack>
<LinkingProvider value={LinkLibrary}>{children}</LinkingProvider>
</SessionProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}

function Substack({ children }: React.PropsWithChildren): JSX.Element {
const { sessionId } = useSession();
const dapper = new Dapper(API_DOMAIN, sessionId);

return (
<DapperProvider dapper={dapper}>
<LinkingProvider value={LinkLibrary}>{children}</LinkingProvider>
</DapperProvider>
);
}

export default Providers;
5 changes: 2 additions & 3 deletions apps/cyberstorm-storybook/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,10 @@ export const decorators = [
];

function Substack({ children }) {
const { sessionId } = useSession();
const dapper = new Dapper(API_DOMAIN, sessionId);
const dapperConstructor = () => new Dapper(API_DOMAIN, undefined);

return (
<DapperProvider dapper={dapper}>
<DapperProvider dapperConstructor={dapperConstructor}>
<LinkingProvider value={LinkLibrary}>{children}</LinkingProvider>
</DapperProvider>
);
Expand Down
9 changes: 5 additions & 4 deletions apps/nextjs/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AppProps } from "next/app";
import { LinkingProvider, RootWrapper, theme } from "@thunderstore/components";
import { Dapper, DapperProvider } from "@thunderstore/dapper";

import { SessionProvider, useSession } from "components/SessionContext";
import { SessionProvider } from "components/SessionContext";
import { LinkLibrary } from "LinkLibrary";
import { API_DOMAIN } from "utils/constants";

Expand All @@ -23,11 +23,12 @@ export default function ThunderstoreApp(appProps: AppProps): JSX.Element {
}

function Substack({ Component, pageProps }: AppProps): JSX.Element {
const { sessionId } = useSession();
const dapper = new Dapper(API_DOMAIN, sessionId);
// const { sessionId } = useSession();
// const dapper = new Dapper(API_DOMAIN, sessionId);
const dapperConstructor = () => new Dapper(API_DOMAIN, undefined);

return (
<DapperProvider dapper={dapper}>
<DapperProvider dapperConstructor={dapperConstructor}>
<LinkingProvider value={LinkLibrary}>
<Component {...pageProps} />
</LinkingProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { TeamLink, UserLink } from "../../../../Links/Links";
import { TextInput } from "../../../../TextInput/TextInput";
import { useState } from "react";
import { CopyButton } from "../../../../CopyButton/CopyButton";
import { Alert } from "../../../../..";
import { Alert } from "../../../../Alert/Alert";

export interface TeamServiceAccountsProps {
serviceAccountData: string[];
Expand Down
34 changes: 19 additions & 15 deletions packages/dapper/src/context.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import React from "react";
import { PropsWithChildren } from "react";

import { DapperInterface } from "./dapper";
import { getDapperContext } from "./singleton";

type ContextProps = { dapper: DapperInterface; children?: React.ReactNode };
const DapperContext = React.createContext<DapperInterface | null>(null);
type DapperProviderProps = PropsWithChildren<{
dapperConstructor: () => DapperInterface;
}>;

export function DapperProvider(props: ContextProps) {
const { dapper, children } = props;
return (
<DapperContext.Provider value={dapper}>{children}</DapperContext.Provider>
);
export function DapperProvider(props: DapperProviderProps) {
/**
* Does NOT support changing the dapper instance after initialization. The
* dapper instance will be created only once regardless of prop changes and
* bound to the global scope. On NextJS the instance may be shared between all
* requests handled by the server depending on the server configuration.
*
* TREAT AS A GLOBAL SINGLETON THAT'S SHARED ACROSS THE ENTIRE PROCESS.
*/
const dapperContext = getDapperContext();
dapperContext.initialize(props.dapperConstructor);
return <>{props.children}</>;
}

export const useDapper = (): DapperInterface => {
const contextState = React.useContext(DapperContext);

if (contextState === null) {
throw new Error("useDapper must be used within a DapperProvider tag");
}

return contextState;
const dapperContext = getDapperContext();
return dapperContext.getDapper();
};
50 changes: 35 additions & 15 deletions packages/dapper/src/singleton.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
/*
Singleton variant of dapper initialization, as React context is not yet
supported in NextJS 13.
Usage:
- Call `SetDapperSingleton` in the initialization phase of the app
- Use the `useDapper` React hook as usual afterwards.
*/
import { DapperInterface } from "./dapper";

let instance: DapperInterface | null = null;
interface GlobalContext {
Dapper?: DapperContext;
}

function getGlobalContext(): GlobalContext {
if (typeof window === "undefined") {
return globalThis as unknown as GlobalContext;
} else {
if (globalThis as unknown) {
return globalThis as unknown as GlobalContext;
} else {
return window as unknown as GlobalContext;
}
}
}

const globalContext = getGlobalContext();

class DapperContext {
private dapper?: DapperInterface = undefined;

export function SetDapperSingleton(dapper: DapperInterface) {
instance = dapper;
public initialize(dapperConstructor: () => DapperInterface) {
if (this.dapper) return;
this.dapper = dapperConstructor();
}

public getDapper(): DapperInterface {
if (!this.dapper) {
throw new Error("Attempted to access dapper before initialization!");
}
return this.dapper;
}
}

export function GetDapperSingleton(): DapperInterface {
if (instance == null) {
throw new Error("Attempted to access dapper before initialization!");
export function getDapperContext(): DapperContext {
if (!globalContext.Dapper) {
globalContext.Dapper = new DapperContext();
}
return instance;
return globalContext.Dapper;
}

0 comments on commit 921cecc

Please sign in to comment.