Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CS: Implement SSR compatible dapper context #744

Merged
merged 2 commits into from
Jul 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}