Skip to content

Commit

Permalink
Change copy/design/layout, add experiment support, fix rendering edge…
Browse files Browse the repository at this point in the history
… cases, dedicated hydration examples
  • Loading branch information
jdorn committed Apr 24, 2024
1 parent dff1af9 commit b91d802
Show file tree
Hide file tree
Showing 27 changed files with 591 additions and 334 deletions.
39 changes: 39 additions & 0 deletions next-js/src/app/client-optimized/ClientApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"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 { 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]);

// 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>
);
}
28 changes: 28 additions & 0 deletions next-js/src/app/client-optimized/ClientComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";
import RevalidateMessage from "@/app/revalidate/RevalidateMessage";
import { useFeatureIsOn, useFeatureValue } from "@growthbook/growthbook-react";

export default function ClientComponent() {
const feature1Enabled = useFeatureIsOn("feature1");
const feature2Value = useFeatureValue("feature2", "fallback");
return (
<div>
<h2>Optimized Client Rendering</h2>
<p>
This page fetches feature flags and experiments at build time, but waits
to evaluate them until client-side rendering. This gives you flexibility
while avoiding flicker.
</p>
<ul>
<li>
feature1: <strong>{feature1Enabled ? "ON" : "OFF"}</strong>
</li>
<li>
feature2: <strong>{feature2Value}</strong>
</li>
</ul>

<RevalidateMessage />
</div>
);
}
9 changes: 9 additions & 0 deletions next-js/src/app/client-optimized/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { getInstance, getPayload } from "@/lib/growthbookServer";
import ClientApp from "./ClientApp";

export default async function PrerenderedClientPage() {
// Get server-side GrowthBook instance in order to fetch the feature flag payload
const gb = await getInstance();

return <ClientApp payload={getPayload(gb)} />;
}
18 changes: 13 additions & 5 deletions next-js/src/app/client/ClientComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
"use client";
import { useFeatureIsOn, useFeatureValue } from "@growthbook/growthbook-react";
import Link from "next/link";

export default function ClientComponent() {
const feature1Enabled = useFeatureIsOn("feature1");
const feature2Value = useFeatureValue("feature2", "fallback");
return (
<div>
<div className="text-4xl my-4">Client Component</div>
<p className="my-2">
This component renders client-side. The page initially delivered to the
client will not have FF values loaded, which can result in a
&apos;flicker&apos; when values are loaded asynchronously client-side.
<h2>Client Rendering (Unoptimized)</h2>
<p>
This component renders entirely client-side. The page initially
delivered to the client will not have feature definitions loaded, and a
network request will be required. This can result in a
&apos;flicker&apos; where fallback values are rendered first and then
swapped in with their real values later.
</p>
<p>
To avoid this flicker, check out the{" "}
<Link href="/client-optimized">Optimized Client</Link> example.
</p>
<ul>
<li>
Expand Down
35 changes: 0 additions & 35 deletions next-js/src/app/client/middleware/page.tsx

This file was deleted.

20 changes: 11 additions & 9 deletions next-js/src/app/client/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
"use client";
import Cookies from "js-cookie";
import { GrowthBookProvider } from "@growthbook/growthbook-react";
import { useEffect } from "react";
import Cookies from "js-cookie";
import { createGB } from "@/lib/growthbook";
import ClientComponent from "./ClientComponent";

const gb = createGB({
// client-side feature
subscribeToChanges: true,
});
import { gb } from "@/lib/growthbookClient";
import { GB_UUID_COOKIE } from "@/middleware";

export default function ClientPage() {
useEffect(() => {
const load = async () => {
try {
await gb.loadFeatures();
const userId = Cookies.get("gb-next-example-userId");

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: userId,
id: uuid,
});
} catch (e) {
console.error(e);
Expand Down
68 changes: 33 additions & 35 deletions next-js/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,38 @@
@tailwind components;
@tailwind utilities;

:root {
--foreground-rgb: 0, 0, 0;
--background-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-rgb: 0, 0, 0;
@layer base {
body {
@apply bg-stone-100 text-stone-900 dark:bg-stone-900 dark:text-stone-100 p-4 max-w-2xl;
}
}

body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-rgb))
)
rgb(var(--background-rgb));
}

@layer utilities {
.text-balance {
text-wrap: balance;
h1 {
@apply mb-2 text-3xl font-bold;
}
}

a {
@apply text-blue-600;
@apply hover:underline;
}

ul {
@apply list-disc;
@apply ml-4;
}
h2 {
@apply mb-2 text-xl font-bold;
}
a {
@apply text-blue-600 dark:text-blue-400 hover:underline;
}
code {
@apply text-sm border border-stone-300 dark:border-stone-700 text-fuchsia-700 dark:text-fuchsia-400 bg-stone-200 dark:bg-stone-800 rounded px-1;
}
pre {
@apply text-sm border border-stone-300 dark:border-stone-700 bg-stone-200 dark:bg-stone-800 rounded p-2
}
ul {
@apply list-disc ml-6;
}
ol {
@apply list-decimal ml-6;
}
li {
@apply mb-1;
}
p {
@apply mb-2;
}
section {
@apply mb-8;
}
}
39 changes: 39 additions & 0 deletions next-js/src/app/hybrid/ClientApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"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 { GB_UUID_COOKIE } from "@/middleware";
import { useCallback, useEffect, 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]);

// 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>
);
}
20 changes: 20 additions & 0 deletions next-js/src/app/hybrid/ClientComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client";
import { useFeatureIsOn, useFeatureValue } from "@growthbook/growthbook-react";

export default function ClientComponent() {
const feature1Enabled = useFeatureIsOn("feature1");
const feature2Value = useFeatureValue("feature2", "fallback");
return (
<div>
<p>And these features are rendered client-side:</p>
<ul>
<li>
feature1: <strong>{feature1Enabled ? "ON" : "OFF"}</strong>
</li>
<li>
feature2: <strong>{feature2Value}</strong>
</li>
</ul>
</div>
);
}
45 changes: 45 additions & 0 deletions next-js/src/app/hybrid/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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";

export default async function ServerCombo() {
// create instance per request, server-side
const gb = await getInstance();

// using cookies means next will render this page dynamically
gb.setAttributes({
id: cookies().get(GB_UUID_COOKIE)?.value || "",
});

const feature1Enabled = gb.isOn("feature1");
const feature2Value = gb.getFeatureValue("feature2", "fallback");

return (
<div>
<h2>Server / Client Hybrid</h2>
<p>
This page fetches and uses feature flags server-side, then hydrates the
client-side GrowthBook instance. This gives you maximum flexibility and
performance.
</p>
<p>These features were rendered server-side:</p>
<ul>
<li>
feature1: <strong>{feature1Enabled ? "ON" : "OFF"}</strong>
</li>
<li>
feature2: <strong>{feature2Value}</strong>
</li>
</ul>

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

<RevalidateMessage />

<GrowthBookTracking data={gb.getDeferredTrackingCalls()} />
</div>
);
}
12 changes: 5 additions & 7 deletions next-js/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={inter.className}>
<div className="p-4 max-w-2xl">
<div className="text-4xl my-4">
<Link href="/">GrowthBook Next.js Example</Link>
</div>
{children}
</div>
<body className={`${inter.className}`}>
<h1 className="mb-4">
<Link href="/">GrowthBook Next.js Example</Link>
</h1>
{children}
</body>
</html>
);
Expand Down

0 comments on commit b91d802

Please sign in to comment.