Skip to content

Commit

Permalink
Update Next 14 examples to use GrowthBook SDK v1.0.0 (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdorn committed May 3, 2024
1 parent da97fc2 commit 1ef196a
Show file tree
Hide file tree
Showing 12 changed files with 239 additions and 213 deletions.
52 changes: 24 additions & 28 deletions next-js/src/app/client-optimized/ClientApp.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,31 @@
"use client";
import Cookies from "js-cookie";
import { AutoExperiment, FeatureDefinition } from "@growthbook/growthbook";
import { gb, setPayload } from "@/lib/growthbookClient";
import { GrowthBookProvider } from "@growthbook/growthbook-react";
import { onExperimentView } from "@/lib/GrowthBookTracking";
import ClientComponent from "./ClientComponent";
import { GrowthBook, GrowthBookPayload } from "@growthbook/growthbook";
import { GrowthBookProvider } from "@growthbook/growthbook-react";
import { useMemo } from "react";
import { GB_UUID_COOKIE } from "@/middleware";
import { useCallback, useEffect, useMemo, useRef } from "react";

export default function ClientApp({
payload,
}: {
payload: {
features: Record<string, FeatureDefinition<unknown>>;
experiments: AutoExperiment[];
};
}) {
// Helper to hydrate client-side GrowthBook instance with payload from the server
const hydrate = useCallback(() => {
setPayload(payload);
gb.setAttributes({
id: Cookies.get(GB_UUID_COOKIE),
});
}, [payload]);
import Cookies from "js-cookie";

// Hydrate immediately on first render and whenever the payload changes
const ref = useRef<boolean>();
if (!ref.current) {
ref.current = true;
hydrate();
}
useEffect(() => hydrate(), [hydrate]);
export default function ClientApp({ payload }: { payload: GrowthBookPayload }) {
// Create a singleton GrowthBook instance for this page
const gb = useMemo(
() =>
new GrowthBook({
apiHost: process.env.NEXT_PUBLIC_GROWTHBOOK_API_HOST,
clientKey: process.env.NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY,
decryptionKey: process.env.NEXT_PUBLIC_GROWTHBOOK_DECRYPTION_KEY,
trackingCallback: onExperimentView,
attributes: {
id: Cookies.get(GB_UUID_COOKIE),
},
}).initSync({
payload,
// Optional, enable streaming updates
streaming: true,
}),
[payload]
);

return (
<GrowthBookProvider growthbook={gb}>
Expand Down
24 changes: 20 additions & 4 deletions next-js/src/app/client-optimized/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import { getInstance, getPayload } from "@/lib/growthbookServer";
import { configureServerSideGrowthBook } from "@/lib/growthbookServer";
import ClientApp from "./ClientApp";
import { GrowthBook } from "@growthbook/growthbook";

export default async function PrerenderedClientPage() {
// Get server-side GrowthBook instance in order to fetch the feature flag payload
const gb = await getInstance();
// Helper to configure cache for next.js
configureServerSideGrowthBook();

return <ClientApp payload={getPayload(gb)} />;
// Create and initialize a GrowthBook instance
const gb = new GrowthBook({
apiHost: process.env.NEXT_PUBLIC_GROWTHBOOK_API_HOST,
clientKey: process.env.NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY,
decryptionKey: process.env.NEXT_PUBLIC_GROWTHBOOK_DECRYPTION_KEY,
});
await gb.init({ timeout: 1000 });

// Get the payload to hydrate the client-side GrowthBook instance
// We need the decrypted payload so the initial client-render can be synchronous
const payload = gb.getDecryptedPayload();

// Cleanup your GrowthBook instance
gb.destroy();

return <ClientApp payload={payload} />;
}
48 changes: 28 additions & 20 deletions next-js/src/app/client/page.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
"use client";
import Cookies from "js-cookie";
import { GrowthBookProvider } from "@growthbook/growthbook-react";
import { useEffect } from "react";
import { GrowthBook, GrowthBookProvider } from "@growthbook/growthbook-react";
import { useEffect, useMemo } from "react";
import ClientComponent from "./ClientComponent";
import { gb } from "@/lib/growthbookClient";
import { GB_UUID_COOKIE } from "@/middleware";
import { onExperimentView } from "@/lib/GrowthBookTracking";

export default function ClientPage() {
// Create a single memoized GrowthBook instance for the client
const gb = useMemo(() => {
return new GrowthBook({
apiHost: process.env.NEXT_PUBLIC_GROWTHBOOK_API_HOST,
clientKey: process.env.NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY,
decryptionKey: process.env.NEXT_PUBLIC_GROWTHBOOK_DECRYPTION_KEY,
trackingCallback: onExperimentView,
});
}, []);

useEffect(() => {
const load = async () => {
try {
await gb.loadFeatures();
// Fetch feature payload from GrowthBook
gb.init({
// Optional, enable streaming updates
streaming: true,
});

let uuid = Cookies.get(GB_UUID_COOKIE);
if (!uuid) {
uuid = Math.random().toString(36).substring(2);
Cookies.set(GB_UUID_COOKIE, uuid);
}
// Set targeting attributes for the user
let uuid = Cookies.get(GB_UUID_COOKIE);
if (!uuid) {
uuid = Math.random().toString(36).substring(2);
Cookies.set(GB_UUID_COOKIE, uuid);
}
gb.setAttributes({
id: uuid,
});
}, [gb]);

gb.setAttributes({
id: uuid,
});
} catch (e) {
console.error(e);
}
};
load();
}, []);
return (
<GrowthBookProvider growthbook={gb}>
<ClientComponent />
Expand Down
58 changes: 26 additions & 32 deletions next-js/src/app/hybrid/ClientApp.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,33 @@
"use client";
import Cookies from "js-cookie";
import { AutoExperiment, FeatureDefinition } from "@growthbook/growthbook";
import { gb, setPayload } from "@/lib/growthbookClient";
import { GrowthBookProvider } from "@growthbook/growthbook-react";
import ClientComponent from "./ClientComponent";
import { onExperimentView } from "@/lib/GrowthBookTracking";
import { GrowthBookPayload } from "@growthbook/growthbook";
import { GrowthBook, GrowthBookProvider } from "@growthbook/growthbook-react";
import { PropsWithChildren, useMemo } from "react";
import { GB_UUID_COOKIE } from "@/middleware";
import { useCallback, useEffect, useRef } from "react";
import Cookies from "js-cookie";

export default function ClientApp({
payload,
}: {
payload: {
features: Record<string, FeatureDefinition<unknown>>;
experiments: AutoExperiment[];
};
}) {
// Helper to hydrate client-side GrowthBook instance with payload from the server
const hydrate = useCallback(() => {
setPayload(payload);
gb.setAttributes({
id: Cookies.get(GB_UUID_COOKIE),
});
}, [payload]);

// Hydrate immediately on first render and whenever the payload changes
const ref = useRef<boolean>();
if (!ref.current) {
ref.current = true;
hydrate();
}
useEffect(() => hydrate(), [hydrate]);

return (
<GrowthBookProvider growthbook={gb}>
<ClientComponent />
</GrowthBookProvider>
children,
}: PropsWithChildren<{ payload: GrowthBookPayload }>) {
// Create a singleton GrowthBook instance for this page
const gb = useMemo(
() =>
new GrowthBook({
apiHost: process.env.NEXT_PUBLIC_GROWTHBOOK_API_HOST,
clientKey: process.env.NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY,
decryptionKey: process.env.NEXT_PUBLIC_GROWTHBOOK_DECRYPTION_KEY,
trackingCallback: onExperimentView,
attributes: {
id: Cookies.get(GB_UUID_COOKIE),
},
}).initSync({
payload,
// Optional, enable streaming updates
streaming: true,
}),
[payload]
);

return <GrowthBookProvider growthbook={gb}>{children}</GrowthBookProvider>;
}
38 changes: 31 additions & 7 deletions next-js/src/app/hybrid/page.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
import { getInstance, getPayload } from "@/lib/growthbookServer";
import { cookies } from "next/headers";
import { GrowthBookTracking } from "@/lib/growthbookClient";
import { GB_UUID_COOKIE } from "@/middleware";
import ClientApp from "./ClientApp";
import RevalidateMessage from "@/app/revalidate/RevalidateMessage";
import { GrowthBook } from "@growthbook/growthbook";
import { configureServerSideGrowthBook } from "@/lib/growthbookServer";
import { GrowthBookTracking } from "@/lib/GrowthBookTracking";
import ClientComponent from "./ClientComponent";

export default async function ServerCombo() {
// create instance per request, server-side
const gb = await getInstance();
// Helper to configure cache for next.js
configureServerSideGrowthBook();

// using cookies means next will render this page dynamically
// Create and initialize a GrowthBook instance
const gb = new GrowthBook({
apiHost: process.env.NEXT_PUBLIC_GROWTHBOOK_API_HOST,
clientKey: process.env.NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY,
decryptionKey: process.env.NEXT_PUBLIC_GROWTHBOOK_DECRYPTION_KEY,
});
await gb.init({ timeout: 1000 });

// Set targeting attributes for the user
gb.setAttributes({
id: cookies().get(GB_UUID_COOKIE)?.value || "",
});

// Evaluate any feature flags
const feature1Enabled = gb.isOn("feature1");
const feature2Value = gb.getFeatureValue("feature2", "fallback");

// Get the payload to hydrate the client-side GrowthBook instance
// We need the decrypted payload so the initial client-render can be synchronous
const payload = gb.getDecryptedPayload();

// If the above features ran any experiments, get the tracking call data
// This is passed into the <GrowthBookTracking> client component below
const trackingData = gb.getDeferredTrackingCalls();

// Cleanup your GrowthBook instance
gb.destroy();

return (
<div>
<h2>Server / Client Hybrid</h2>
Expand All @@ -35,11 +57,13 @@ export default async function ServerCombo() {
</li>
</ul>

<ClientApp payload={getPayload(gb)} />
<ClientApp payload={payload}>
<ClientComponent />
</ClientApp>

<RevalidateMessage />

<GrowthBookTracking data={gb.getDeferredTrackingCalls()} />
<GrowthBookTracking data={trackingData} />
</div>
);
}
13 changes: 4 additions & 9 deletions next-js/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,9 @@ export default function Home() {
language.
</li>
<li>
Next, create a <code>.env.local</code> file in the root of this
example with the following contents (fill in values with the ones
shown after creating your SDK Connection):
<pre>{`NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY=
NEXT_PUBLIC_GROWTHBOOK_API_HOST=
# Optional (only if you enabled encryption):
NEXT_PUBLIC_GROWTHBOOK_DECRYPTION_KEY=
`}</pre>
Next, in this example root, copy <code>.env.local.example</code> to{" "}
<code>.env.local</code> and fill in the values from your GrowthBook
SDK Connection.
</li>
<li>
<p>
Expand Down Expand Up @@ -98,7 +93,7 @@ NEXT_PUBLIC_GROWTHBOOK_DECRYPTION_KEY=
user saw is sent to the client where an analytics event is triggered
(or console.log in these examples). This happens via the{" "}
<code>GrowthBookTracking</code> client component defined in{" "}
<code>src/lib/growthbookClient.ts</code>.
<code>src/lib/GrowthBookTracking</code>.
</p>
</section>
</main>
Expand Down
29 changes: 23 additions & 6 deletions next-js/src/app/server/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
import { getInstance } from "@/lib/growthbookServer";
import { GrowthBookTracking } from "@/lib/growthbookClient";
import { cookies } from "next/headers";
import { GB_UUID_COOKIE } from "@/middleware";
import RevalidateMessage from "@/app/revalidate/RevalidateMessage";
import { GrowthBook } from "@growthbook/growthbook";
import { GrowthBookTracking } from "@/lib/GrowthBookTracking";
import { configureServerSideGrowthBook } from "@/lib/growthbookServer";

export default async function ServerDynamic() {
// create instance per request, server-side
const gb = await getInstance();
// Helper to configure cache for next.js
configureServerSideGrowthBook();

// using cookies means next will render this page dynamically
// Create and initialize a GrowthBook instance
const gb = new GrowthBook({
apiHost: process.env.NEXT_PUBLIC_GROWTHBOOK_API_HOST,
clientKey: process.env.NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY,
decryptionKey: process.env.NEXT_PUBLIC_GROWTHBOOK_DECRYPTION_KEY,
});
await gb.init({ timeout: 1000 });

// Set targeting attributes for the user
await gb.setAttributes({
id: cookies().get(GB_UUID_COOKIE)?.value || "",
});

// Evaluate any feature flags
const feature1Enabled = gb.isOn("feature1");
const feature2Value = gb.getFeatureValue("feature2", "fallback");

// If the above features ran any experiments, get the tracking call data
// This is passed into the <GrowthBookTracking> client component below
const trackingData = gb.getDeferredTrackingCalls();

// Cleanup
gb.destroy();

return (
<div>
<h2>Dynamic Server Rendering</h2>
Expand All @@ -34,7 +51,7 @@ export default async function ServerDynamic() {

<RevalidateMessage />

<GrowthBookTracking data={gb.getDeferredTrackingCalls()} />
<GrowthBookTracking data={trackingData} />
</div>
);
}
20 changes: 18 additions & 2 deletions next-js/src/app/static/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import RevalidateMessage from "@/app/revalidate/RevalidateMessage";
import { getInstance } from "@/lib/growthbookServer";
import { configureServerSideGrowthBook } from "@/lib/growthbookServer";
import { GrowthBook } from "@growthbook/growthbook";

export default async function ServerStatic() {
// Helper to configure cache for next.js
configureServerSideGrowthBook();

// Create and initialize a GrowthBook instance
const gb = new GrowthBook({
apiHost: process.env.NEXT_PUBLIC_GROWTHBOOK_API_HOST,
clientKey: process.env.NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY,
decryptionKey: process.env.NEXT_PUBLIC_GROWTHBOOK_DECRYPTION_KEY,
});
await gb.init({ timeout: 1000 });

// By not using cookies or headers, this page can be statically rendered
// Note: This means you can't target individual users or run experiments
const gb = await getInstance();

// Evaluate any feature flags
const feature1Enabled = gb.isOn("feature1");
const feature2Value = gb.getFeatureValue("feature2", "fallback");

// Cleanup your GrowthBook instance
gb.destroy();

return (
<div>
<h2>Static Pages</h2>
Expand Down
Loading

0 comments on commit 1ef196a

Please sign in to comment.