Flags SDK Workshop

Precompute

So far we relied on server side rendering for our feature flags. But we can actually do better.

edge network

Manual approach

We could just create app/home-a/page.tsx and app/home-b/page.tsx and use Routing Middleware to determine which one to render.

precompute-flow

Do not make these changes, this is just for educational purposes.

middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
import { homeFlag } from './flags.ts';
 
export const config = { matcher: ['/'] };
 
export async function middleware(request: NextRequest) {
  const home = await homeFlag();
 
  // Determine which version to show based on the feature flag
  const version = home ? '/home-b' : '/home-a';
 
  // Rewrite the request to the appropriate version
  const nextUrl = new URL(version, request.url);
  return NextResponse.rewrite(nextUrl);
}

The manual approach breaks down when you want to:

  • combine multiple feature flags on a single, static page
  • use a single feature flag across multiple pages

Precomputing

Make these changes, we're back to editing.

Export an array of flags we want to precompute.

flags.ts
import { flag, dedupe } from "flags/next"; 

type Entities = { 
  browser: "Safari" | "Chrome"; 
}; 

const identify = dedupe(({ headers, cookies }): Entities => { 
  const ua = headers.get("user-agent") || ""; 
  const isSafari = ua.includes("Safari") && !ua.includes("Chrome"); 
  return { browser: isSafari ? "Safari" : "Chrome" }; 
}); 

export const enableSummerSale = flag<boolean, Entities>({
  key: "summer-sale",
  description: "Shows a bright yellow banner for a 20% discount",
  defaultValue: false,
  identify, 
  decide({ entities }) {
    return entities?.browser !== "Safari";
  },
});

export const productFlags = [ 
  enableSummerSale 
] as const; 

Precompute flags in middleware

middleware.ts
import { type NextRequest, NextResponse } from "next/server";
import { precompute } from "flags/next";
import { productFlags } from "@/flags";

export const config = { matcher: "/" };

export async function middleware(request: NextRequest) {
  const code = await precompute(productFlags);

  // rewrites the request to the variant for this flag combination
  const nextUrl = new URL(
    `/${code}${request.nextUrl.pathname}${request.nextUrl.search}`,
    request.url
  );

  return NextResponse.rewrite(nextUrl, { request });
}

Rename the app/(main) folder to app/[code] and update all imports.

This dynamic route segment called [code] will hold the precomputed code.

Edit the page to access the precomputed code.

app/[code]/page.tsx
import { ImageGallery } from "@/components/image-gallery";
import { ProductDetails } from "@/components/product-detail-page/product-details";
import { ProductHeader } from "@/components/product-detail-page/product-header";
import { AddToCart } from "@/app/[code]/add-to-cart";
import { ColorPicker } from "@/components/product-detail-page/color-picker";
import { SizePicker } from "@/components/product-detail-page/size-picker";
import { ProductDetailPageProvider } from "@/components/utils/product-detail-page-context";
import { Main } from "@/components/main";
import { enableSummerSale, productFlags } from "@/flags"; 
import { SummerSaleBanner } from "@/components/banners/summer-sale-banner";

export default async function Page(props: { 
  params: Promise<{ code: string }>; 
}) { 
  const { code } = await props.params; 
  const summerBanner = await enableSummerSale(code, productFlags); 
  return (
    <ProductDetailPageProvider>
      {summerBanner ? <SummerSaleBanner /> : null}
      <Main>
        <div className="lg:grid lg:auto-rows-min lg:grid-cols-12 lg:gap-x-8">
          <ProductHeader />
          <ImageGallery />

          <div className="mt-8 lg:col-span-5">
            <ColorPicker />
            <SizePicker />
            <AddToCart />
            <ProductDetails />
          </div>
        </div>
      </Main>
    </ProductDetailPageProvider>
  );
}

We can now opt into Incremental Static Regeneration (ISR) for this page.

This means each variant will only ever be rendered on demand at request time once and can then stay cached in the CDN.

app/[code]/page.tsx
import { ImageGallery } from "@/components/image-gallery";
import { ProductDetails } from "@/components/product-detail-page/product-details";
import { ProductHeader } from "@/components/product-detail-page/product-header";
import { AddToCart } from "@/app/[code]/add-to-cart";
import { ColorPicker } from "@/components/product-detail-page/color-picker";
import { SizePicker } from "@/components/product-detail-page/size-picker";
import { ProductDetailPageProvider } from "@/components/utils/product-detail-page-context";
import { Main } from "@/components/main";
import { enableSummerSale, productFlags } from "@/flags";
import { SummerSaleBanner } from "@/components/banners/summer-sale-banner";

export async function generateStaticParams() { 
  // returning an empty array is enough to enable ISR
  return []; 
} 

export default async function Page(props: {
  params: Promise<{ code: string }>;
}) {
  const { code } = await props.params;
  const summerBanner = await enableSummerSale(code, productFlags);
  return (
    <ProductDetailPageProvider>
      {summerBanner ? <SummerSaleBanner /> : null}
      <Main>
        <div className="lg:grid lg:auto-rows-min lg:grid-cols-12 lg:gap-x-8">
          <ProductHeader />
          <ImageGallery />

          <div className="mt-8 lg:col-span-5">
            <ColorPicker />
            <SizePicker />
            <AddToCart />
            <ProductDetails />
          </div>
        </div>
      </Main>
    </ProductDetailPageProvider>
  );
}

We can futher optimize this by opting into build time generation of pages.

app/[code]/page.tsx
import { ImageGallery } from "@/components/image-gallery";
import { ProductDetails } from "@/components/product-detail-page/product-details";
import { ProductHeader } from "@/components/product-detail-page/product-header";
import { AddToCart } from "@/app/[code]/add-to-cart";
import { ColorPicker } from "@/components/product-detail-page/color-picker";
import { SizePicker } from "@/components/product-detail-page/size-picker";
import { ProductDetailPageProvider } from "@/components/utils/product-detail-page-context";
import { Main } from "@/components/main";
import { enableSummerSale, productFlags } from "@/flags";
import { SummerSaleBanner } from "@/components/banners/summer-sale-banner";
import { generatePermutations } from 'flags/next'; 

export async function generateStaticParams() { 
  const codes = await generatePermutations(productFlags); 
  return codes.map((code) => ({ code })); 
} 

export default async function Page(props: {
  params: Promise<{ code: string }>;
}) {
  const { code } = await props.params;
  const summerBanner = await enableSummerSale(code, productFlags);
  return (
    <ProductDetailPageProvider>
      {summerBanner ? <SummerSaleBanner /> : null}
      <Main>
        <div className="lg:grid lg:auto-rows-min lg:grid-cols-12 lg:gap-x-8">
          <ProductHeader />
          <ImageGallery />

          <div className="mt-8 lg:col-span-5">
            <ColorPicker />
            <SizePicker />
            <AddToCart />
            <ProductDetails />
          </div>
        </div>
      </Main>
    </ProductDetailPageProvider>
  );
}