From 8b7deca0349556cf3d6151298a959e57472c83ce Mon Sep 17 00:00:00 2001 From: Dominik Ferber Date: Fri, 14 Feb 2025 07:19:10 +0200 Subject: [PATCH] add flags context --- .../app/examples/flag-context/flags.ts | 26 ++++++++++ .../app/examples/flag-context/layout.tsx | 16 +++++++ .../app/examples/flag-context/page.tsx | 47 +++++++++++++++++++ packages/flags/src/react/index.tsx | 27 +++++++++++ 4 files changed, 116 insertions(+) create mode 100644 examples/snippets/app/examples/flag-context/flags.ts create mode 100644 examples/snippets/app/examples/flag-context/layout.tsx create mode 100644 examples/snippets/app/examples/flag-context/page.tsx diff --git a/examples/snippets/app/examples/flag-context/flags.ts b/examples/snippets/app/examples/flag-context/flags.ts new file mode 100644 index 0000000..c7e9bda --- /dev/null +++ b/examples/snippets/app/examples/flag-context/flags.ts @@ -0,0 +1,26 @@ +import type { ReadonlyRequestCookies } from '@vercel/flags'; +import { flag, dedupe } from '@vercel/flags/next'; + +interface Entities { + user?: { id: string }; +} + +const identify = dedupe( + ({ cookies }: { cookies: ReadonlyRequestCookies }): Entities => { + const userId = cookies.get('dashboard-user-id')?.value; + return { user: userId ? { id: userId } : undefined }; + }, +); + +export const dashboardFlag = flag({ + key: 'dashboard-flag', + identify, + description: 'Flag used on the Dashboard Pages example', + decide({ entities }) { + if (!entities?.user) return false; + // Allowed users could be loaded from Edge Config or elsewhere + const allowedUsers = ['user1']; + + return allowedUsers.includes(entities.user.id); + }, +}); diff --git a/examples/snippets/app/examples/flag-context/layout.tsx b/examples/snippets/app/examples/flag-context/layout.tsx new file mode 100644 index 0000000..67ba610 --- /dev/null +++ b/examples/snippets/app/examples/flag-context/layout.tsx @@ -0,0 +1,16 @@ +import { FlagContextProvider } from '@vercel/flags/react'; +import { dashboardFlag } from './flags'; + +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const dashboard = await dashboardFlag(); + + return ( + + {children} + + ); +} diff --git a/examples/snippets/app/examples/flag-context/page.tsx b/examples/snippets/app/examples/flag-context/page.tsx new file mode 100644 index 0000000..8449714 --- /dev/null +++ b/examples/snippets/app/examples/flag-context/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { useFlagContext } from '@vercel/flags/react'; +import { DemoFlag } from '@/components/demo-flag'; +import { useRouter } from 'next/navigation'; +import type { FlagValuesType } from '@vercel/flags'; + +export default function Page() { + const dashboard = useFlagContext('dashboard-flag'); + const router = useRouter(); + return ( + <> + +
+ + + +
+ + ); +} diff --git a/packages/flags/src/react/index.tsx b/packages/flags/src/react/index.tsx index bf81339..ba0092c 100644 --- a/packages/flags/src/react/index.tsx +++ b/packages/flags/src/react/index.tsx @@ -1,3 +1,5 @@ +'use client'; + import React from 'react'; import type { FlagDefinitionsType, FlagValuesType } from '../types'; import { safeJsonStringify } from '../lib/safe-json-stringify'; @@ -42,3 +44,28 @@ export function FlagValues({ /> ); } + +const emptyValues: FlagValuesType = {}; +const FlagContext = React.createContext(emptyValues); + +export function FlagContextProvider({ + children, + values, +}: { + children: React.ReactNode; + values: FlagValuesType; +}) { + return {children}; +} + +export function useFlagContext(): T; +export function useFlagContext(key: string): T; +export function useFlagContext(key?: string): T { + const values = React.useContext(FlagContext); + + if (values === emptyValues) { + throw new Error('FlagContextProvider not found'); + } + + return typeof key === 'string' ? (values[key] as T) : (values as T); +}