ai

Radix UI Primitives Feature Flag Management —

Radix UI Primitives Feature Flag Management —

Radix UI Primitives คืออะไร

Radix UI Primitives Feature Flag Management —

Radix UI เป็น open source component library สำหรับ React ที่ให้ unstyled, accessible primitives สำหรับสร้าง UI components คุณภาพสูง ต่างจาก component libraries อื่นที่มาพร้อม styles Radix UI ให้เฉพาะ behavior, accessibility และ keyboard interactions ให้ developers style เองตามต้องการ

ข้อดีของ Radix UI ได้แก่ WAI-ARIA compliant ทุก component accessible ตั้งแต่แรก, Unstyled ไม่มี opinionated styles ใช้กับ Tailwind CSS หรือ CSS-in-JS ได้, Composable ทุก component ประกอบจาก primitives เล็กๆ, Controlled/Uncontrolled รองรับทั้งสองรูปแบบ, TypeScript support types ครบถ้วน และ SSR compatible ใช้กับ Next.js ได้

Feature Flag Management สำหรับ UI components ช่วยให้ toggle UI features โดยไม่ต้อง redeploy, A/B test different component variants, gradual rollout ของ new UI designs, emergency disable ของ problematic features และ personalize UI ตาม user segments

การรวม Radix UI กับ Feature Flags ทำให้สามารถ switch ระหว่าง component versions (เช่น Dialog V1 กับ V2), enable/disable experimental features (เช่น new navigation), customize component behavior ตาม user role และ test new accessibility improvements กับ subset ของ users

ติดตั้งและใช้งาน Radix UI

เริ่มต้นใช้งาน Radix UI Primitives

=== ติดตั้ง Radix UI ===

สร้าง React project ด้วย Vite

npm create vite@latest my-app -- --template react-ts

cd my-app

ติดตั้ง Radix UI Primitives (แยกติดตั้งเฉพาะที่ใช้)

npm install @radix-ui/react-dialog

npm install @radix-ui/react-dropdown-menu

npm install @radix-ui/react-tabs

npm install @radix-ui/react-tooltip

npm install @radix-ui/react-switch

npm install @radix-ui/react-select

npm install @radix-ui/react-accordion

npm install @radix-ui/react-toast

npm install @radix-ui/react-popover

ติดตั้ง styling dependencies

npm install tailwindcss @tailwindcss/vite

npm install clsx tailwind-merge

npm install class-variance-authority

ติดตั้ง feature flag dependencies

npm install launchdarkly-react-client-sdk

หรือ

npm install @unleash/proxy-client-react

=== Project Structure ===

src/

├── components/

│ ├── ui/ # Radix UI wrapped components

│ │ ├── Dialog.tsx

│ │ ├── Button.tsx

│ │ ├── Select.tsx

│ │ └── Toast.tsx

เนื้อหาเกี่ยวข้อง — อ่านต่อ: Prometheus PromQL Service Level Objective SLO

│ ├── features/ # Feature-flagged components

│ │ ├── NewNavigation.tsx

│ │ └── ExperimentalSearch.tsx

│ └── shared/

├── hooks/

│ ├── useFeatureFlag.ts

│ └── useFeatureFlags.ts

├── providers/

│ └── FeatureFlagProvider.tsx

└── lib/

แนะนำเพิ่มเติม — ดูสัญญาณเทรดที่ XM Signal

├── flags.ts

└── utils.ts

=== tailwind.config.ts ===

import type { Config } from 'tailwindcss'

export default {

content: ['./src/**/*.{ts, tsx}'],

theme: {

extend: {

keyframes: {

'dialog-overlay-show': {

from: { opacity: '0' },

to: { opacity: '1' },

},

'dialog-content-show': {

from: { opacity: '0', transform: 'translate(-50%,-48%) scale(0.96)' },

to: { opacity: '1', transform: 'translate(-50%,-50%) scale(1)' },

},

},

animation: {

'dialog-overlay': 'dialog-overlay-show 150ms ease',

'dialog-content': 'dialog-content-show 150ms ease',

},

},

},

เนื้อหาเกี่ยวข้อง — แนะนำให้อ่าน MLflow Experiment Cloud Native Design: คู่มือฉบับสมบูรณ์ 2026 สำหรับการพัฒนา…

} satisfies Config

echo "Radix UI project setup complete"

สร้าง Feature Flag System สำหรับ React

Feature Flag Provider และ hooks

// === FeatureFlagProvider.tsx ===
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';

interface FeatureFlags {
 [key: string]: boolean | string | number;
}

interface FeatureFlagContextType {
 flags: FeatureFlags;
 isEnabled: (flagName: string) => boolean;
 getValue: (flagName: string) => any;
 loading: boolean;
}

const FeatureFlagContext = createContext<FeatureFlagContextType>({
 flags: {},
 isEnabled: () => false,
 getValue: () => undefined,
 loading: true,
});

// Default flags (fallback when service unavailable)
const DEFAULT_FLAGS: FeatureFlags = {
 'new-dialog-design': false,
 'experimental-search': false,
 'dark-mode-v2': false,
 'new-navigation': false,
 'toast-animations': true,
 'max-upload-size-mb': 10,
 'onboarding-variant': 'control',
};

interface Props {
 children: ReactNode;
 apiUrl?: string;
 userId?: string;
}

export function FeatureFlagProvider({ children, apiUrl, userId }: Props) {
 const [flags, setFlags] = useState<FeatureFlags>(DEFAULT_FLAGS);
 const [loading, setLoading] = useState(true);

 useEffect(() => {
 async function fetchFlags() {
 try {
 if (apiUrl) {
 const resp = await fetch(`/flags?user=`);
 const data = await resp.json();
 setFlags({ ...DEFAULT_FLAGS, ...data });
 }
 } catch (error) {
 console.warn('Feature flags fetch failed, using defaults');
 } finally {
 setLoading(false);
 }
 }

 fetchFlags();
 }, [apiUrl, userId]);

 const isEnabled = (flagName: string): boolean => {
 const value = flags[flagName];
 return value === true || value === 'true';
 };

 const getValue = (flagName: string) => flags[flagName];

 return (
 <FeatureFlagContext.Provider value={{ flags, isEnabled, getValue, loading }}>
 {children}
 </FeatureFlagContext.Provider>
 );
}

// === useFeatureFlag.ts ===
export function useFeatureFlag(flagName: string): boolean {
 const { isEnabled } = useContext(FeatureFlagContext);
 return isEnabled(flagName);
}

export function useFeatureFlagValue(flagName: string) {
 const { getValue } = useContext(FeatureFlagContext);
 return getValue(flagName);
}

export function useFeatureFlags() {
 return useContext(FeatureFlagContext);
}

// === FeatureGate Component ===
interface FeatureGateProps {
 flag: string;
 children: ReactNode;
 fallback?: ReactNode;
}

export function FeatureGate({ flag, children, fallback = null }: FeatureGateProps) {
 const enabled = useFeatureFlag(flag);
 return enabled ? <>{children}</> : <>{fallback}</>;
}

// === Usage in App.tsx ===
// <FeatureFlagProvider apiUrl="/api" userId={user.id}>
// <App />
// </FeatureFlagProvider>
//
// In components:
// const showNewNav = useFeatureFlag('new-navigation');
// <FeatureGate flag="experimental-search">
// <ExperimentalSearch />
// </FeatureGate>

รวม Radix UI กับ Feature Flags

ใช้ Feature Flags ควบคุม Radix UI components

// === Feature-Flagged Dialog Component ===
import * as Dialog from '@radix-ui/react-dialog';
import { useFeatureFlag } from '../hooks/useFeatureFlag';

// Dialog V1 (current)
function DialogV1({ trigger, title, children, open, onOpenChange }) {
 return (
 <Dialog.Root open={open} onOpenChange={onOpenChange}>
 <Dialog.Trigger asChild>{trigger}</Dialog.Trigger>
 <Dialog.Portal>
 <Dialog.Overlay className="fixed inset-0 bg-black/40" />
 <Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg p-6 w-[420px] shadow-xl">
 <Dialog.Title className="text-lg font-semibold">{title}</Dialog.Title>
 <div className="mt-4">{children}</div>
 <Dialog.Close asChild>
 <button className="absolute top-3 right-3 text-gray-400 hover:text-gray-600">
 ✕
 </button>
 </Dialog.Close>
 </Dialog.Content>
 </Dialog.Portal>
 </Dialog.Root>
 );
}

// Dialog V2 (new design - behind feature flag)
function DialogV2({ trigger, title, children, open, onOpenChange }) {
 return (
 <Dialog.Root open={open} onOpenChange={onOpenChange}>
 <Dialog.Trigger asChild>{trigger}</Dialog.Trigger>
 <Dialog.Portal>
 <Dialog.Overlay className="fixed inset-0 bg-black/60 backdrop-blur-sm animate-dialog-overlay" />
 <Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-gray-900 rounded-2xl p-8 w-[480px] shadow-2xl animate-dialog-content border border-gray-200 dark:border-gray-700">
 <Dialog.Title className="text-xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
 {title}
 </Dialog.Title>
 <Dialog.Description className="text-sm text-gray-500 mt-1">
 Make changes below
 </Dialog.Description>
 <div className="mt-6">{children}</div>
 <Dialog.Close asChild>
 <button className="absolute top-4 right-4 w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center hover:bg-gray-200 transition-colors">
 ✕
 </button>
 </Dialog.Close>
 </Dialog.Content>
 </Dialog.Portal>
 </Dialog.Root>
 );
}

// Smart Dialog that switches based on feature flag
export function SmartDialog(props) {
 const useNewDesign = useFeatureFlag('new-dialog-design');
 return useNewDesign ? <DialogV2 {...props} /> : <DialogV1 {...props} />;
}

// === Feature-Flagged Navigation ===
import * as NavigationMenu from '@radix-ui/react-navigation-menu';
import { FeatureGate } from '../providers/FeatureFlagProvider';

function MainNavigation() {
 return (
 <nav>
 <NavigationMenu.Root className="relative z-10">
 <NavigationMenu.List className="flex gap-2 p-2">
 <NavigationMenu.Item>
 <NavigationMenu.Link href="/" className="px-3 py-2 rounded hover:bg-gray-100">
 Home
 </NavigationMenu.Link>
 </NavigationMenu.Item>

 <NavigationMenu.Item>
 <NavigationMenu.Link href="/products" className="px-3 py-2 rounded hover:bg-gray-100">
 Products
 </NavigationMenu.Link>
 </NavigationMenu.Item>

 {/* Feature-flagged nav item */}
 <FeatureGate flag="new-navigation">
 <NavigationMenu.Item>
 <NavigationMenu.Trigger className="px-3 py-2 rounded hover:bg-gray-100">
 AI Tools ✨
 </NavigationMenu.Trigger>
 <NavigationMenu.Content className="absolute top-full left-0 w-[400px] bg-white rounded-lg shadow-lg p-4 mt-2">
 <div className="grid grid-cols-2 gap-3">
 <a href="/ai/chat" className="p-3 rounded hover:bg-gray-50">
 <div className="font-medium">AI Chat</div>
 <div className="text-sm text-gray-500">Chat assistant</div>
 </a>
 <a href="/ai/image" className="p-3 rounded hover:bg-gray-50">
 <div className="font-medium">Image Gen</div>
 <div className="text-sm text-gray-500">Generate images</div>
 </a>
 </div>
 </NavigationMenu.Content>
 </NavigationMenu.Item>
 </FeatureGate>
 </NavigationMenu.List>
 </NavigationMenu.Root>
 </nav>
 );
}

Testing Components กับ Feature Flags

ทดสอบ components ที่ใช้ feature flags

// === Testing Feature-Flagged Components ===
// __tests__/SmartDialog.test.tsx

import { render, screen, fireEvent } from '@testing-library/react';
import { FeatureFlagProvider } from '../providers/FeatureFlagProvider';
import { SmartDialog } from '../components/ui/SmartDialog';

// Helper to render with feature flags
function renderWithFlags(ui: React.ReactElement, flags: Record<string, any> = {}) {
 // Mock the flag context
 return render(
 <FeatureFlagProvider>
 {ui}
 </FeatureFlagProvider>
 );
}

describe('SmartDialog', () => {
 const defaultProps = {
 trigger: <button>Open</button>,
 title: 'Test Dialog',
 children: <p>Dialog content</p>,
 };

 it('renders V1 dialog when flag is disabled', () => {
 renderWithFlags(<SmartDialog {...defaultProps} />);

 fireEvent.click(screen.getByText('Open'));
 expect(screen.getByText('Test Dialog')).toBeInTheDocument();
 expect(screen.getByText('Dialog content')).toBeInTheDocument();
 });

 it('renders dialog with correct accessibility', () => {
 renderWithFlags(<SmartDialog {...defaultProps} />);

 fireEvent.click(screen.getByText('Open'));

 const dialog = screen.getByRole('dialog');
 expect(dialog).toBeInTheDocument();
 expect(dialog).toHaveAttribute('aria-modal', 'true');
 });

 it('closes on escape key', () => {
 renderWithFlags(<SmartDialog {...defaultProps} />);

 fireEvent.click(screen.getByText('Open'));
 expect(screen.getByText('Dialog content')).toBeInTheDocument();

 fireEvent.keyDown(document, { key: 'Escape' });
 expect(screen.queryByText('Dialog content')).not.toBeInTheDocument();
 });
});

// === Feature Flag Testing Utilities ===
// test-utils/flags.ts

export function createMockFlags(overrides: Record<string, any> = {}) {
 return {
 'new-dialog-design': false,
 'experimental-search': false,
 'new-navigation': false,
 'dark-mode-v2': false,
 'toast-animations': true,
 ...overrides,
 };
}

// Custom render that accepts flags
export function renderWithFeatureFlags(
 ui: React.ReactElement,
 flags: Record<string, any> = {}
) {
 const allFlags = createMockFlags(flags);

 return render(
 <TestFeatureFlagProvider flags={allFlags}>
 {ui}
 </TestFeatureFlagProvider>
 );
}

// === E2E Test with Playwright ===
// e2e/feature-flags.spec.ts

import { test, expect } from '@playwright/test';

test.describe('Feature Flagged Components', () => {
 test('shows new navigation when flag enabled', async ({ page }) => {
 // Set feature flag via API/cookie before navigating
 await page.addInitScript(() => {
 window.__FEATURE_FLAGS__ = { 'new-navigation': true };
 });

 await page.goto('/');

 // New nav item should be visible
 await expect(page.getByText('AI Tools')).toBeVisible();
 });

 test('hides new navigation when flag disabled', async ({ page }) => {
 await page.addInitScript(() => {
 window.__FEATURE_FLAGS__ = { 'new-navigation': false };
 });

 await page.goto('/');

 await expect(page.getByText('AI Tools')).not.toBeVisible();
 });

 test('dialog V2 has new design elements', async ({ page }) => {
 await page.addInitScript(() => {
 window.__FEATURE_FLAGS__ = { 'new-dialog-design': true };
 });

 await page.goto('/demo');
 await page.click('button:has-text("Open Dialog")');

 // V2 has gradient title
 const title = page.locator('[role="dialog"] h2');
 await expect(title).toBeVisible();
 });
});

Production Deployment และ A/B Testing

Deploy และ A/B test components

=== Production Feature Flag Setup ===

1. Feature Flag API Server (Express)

server.ts

import express from 'express';

import { createHash } from 'crypto';

const app = express();

interface FlagConfig {

enabled: boolean;

rolloutPct: number;

variants?: Record&lt;string, number&gt;; // variant -> weight

}

const FLAGS: Record&lt;string, FlagConfig&gt; = {

'new-dialog-design': { enabled: true, rolloutPct: 25 },

แนะนำเพิ่มเติม — แหล่งความรู้ Forex iCafeForex

'new-navigation': { enabled: true, rolloutPct: 50 },

'experimental-search': { enabled: false, rolloutPct: 0 },

'onboarding-variant': {

enabled: true,

rolloutPct: 100,

variants: { control: 50, variant_a: 25, variant_b: 25 },

},

};

function getUserBucket(userId: string, flagName: string): number {

const hash = createHash('md5')

.update(`:`)

.digest('hex');

return parseInt(hash.substring(0, 8), 16) % 100;

}

app.get('/api/flags', (req, res) => {

const userId = req.query.user as string || 'anonymous';

เนื้อหาเกี่ยวข้อง — ดูเพิ่มเติมเรื่อง อัตราการว่างงานโลก 2026: คู่มือพิชิตปัญหาเศรษฐกิจ

const result: Record&lt;string, any&gt; = {};

for (const [name, config] of Object.entries(FLAGS)) {

if (!config.enabled) {

result[name] = false;

continue;

}

const bucket = getUserBucket(userId, name);

if (config.variants) {

let cumulative = 0;

for (const [variant, weight] of Object.entries(config.variants)) {

cumulative += weight;

if (bucket < cumulative) {

result[name] = variant;

break;

}

}

} else {

result[name] = bucket < config.rolloutPct;

}

}

res.json(result);

});

2. Analytics Integration

Track which variant users see

function trackFeatureExposure(flagName: string, variant: string, userId: string) {

fetch('/api/analytics/track', {

method: 'POST',

headers: { 'Content-Type': 'application/json' },

body: JSON.stringify({

event: 'feature_exposure',

properties: {

flag_name: flagName,

variant: variant,

user_id: userId,

เนื้อหาเกี่ยวข้อง — ดูเพิ่มเติมเรื่อง Htmx Alpine.js Citizen Developer

timestamp: new Date().toISOString(),

},

}),

});

}

3. CI/CD Pipeline

.github/workflows/deploy.yml

name: Deploy with Feature Flags

on:

Radix UI Primitives Feature Flag Management —

push:

branches: [main]

jobs:

deploy:

runs-on: ubuntu-latest

steps:

  • uses: actions/checkout@v4
  • uses: actions/setup-node@v4

with:

node-version: 20

  • run: npm ci
  • run: npm run test
  • run: npm run build

# Verify feature flags are properly configured

  • name: Validate Feature Flags

run: |

node -e "

const flags = require('./src/lib/flags');

const required = ['new-dialog-design', 'new-navigation'];

for (const f of required) {

if (!(f in flags.DEFAULT_FLAGS)) {

throw new Error('Missing flag: ' + f);

}

}

console.log('Feature flags validated');

"

  • name: Deploy

run: npm run deploy

echo "Production setup complete"

FAQ คำถามที่พบบ่อย

Q: Radix UI กับ shadcn/ui ต่างกันอย่างไร?

A: Radix UI เป็น primitive library ที่ให้ behavior และ accessibility แต่ไม่มี styles shadcn/ui สร้างบน Radix UI โดยเพิ่ม Tailwind CSS styles ให้พร้อมใช้งาน ใช้ copy-paste approach (ไม่ใช่ npm package) ทำให้ customize ได้ง่าย สำหรับ projects ใหม่แนะนำ shadcn/ui เพราะประหยัดเวลา styling สำหรับ projects ที่ต้องการ design system เฉพาะ ใช้ Radix UI โดยตรงแล้ว style เอง

Q: Feature Flag ทำให้ bundle size ใหญ่ขึ้นไหม?

A: ถ้า import ทั้ง V1 และ V2 components จะเพิ่ม bundle size ทางแก้ใช้ React.lazy() กับ dynamic imports เพื่อ code-split feature-flagged components โหลดเฉพาะ version ที่ user ได้รับ สำหรับ flags ที่ควบคุม small UI changes (เช่น สี, text) ไม่กระทบ bundle size สำหรับ flags ที่ควบคุม entire pages/features ใช้ route-based code splitting

Q: จะทำ gradual rollout ของ new UI อย่างไร?

A: เริ่มจาก internal users (employees) 100%, ขยายเป็น beta users 100%, จากนั้น public users 5% -> 25% -> 50% -> 100% ที่แต่ละ stage monitor metrics สำคัญ (conversion rate, error rate, page load time, user feedback) ถ้า metrics ดีขึ้นหรือคงที่ เพิ่ม rollout percentage ถ้า metrics แย่ลง rollback ทันทีโดย set percentage เป็น 0

Q: Accessibility testing สำหรับ feature-flagged components ทำอย่างไร?

A: ต้อง test accessibility ทั้ง enabled และ disabled states ของทุก flag ใช้ axe-core หรือ Playwright accessibility testing ทดสอบ keyboard navigation, screen reader, focus management สำหรับทุก variant Radix UI primitives มี accessibility built-in แต่ custom styles อาจทำให้ contrast ratio ไม่ผ่าน ใช้ automated testing ใน CI pipeline ทดสอบทุก flag combination ที่เป็นไปได้

XM Legend · เทรดเดอร์ & ผู้สอน Forex 13 ปี

ผู้ก่อตั้ง SiamCafe ตั้งแต่ปี 1997 · เทรดเดอร์สาย Forex มากกว่า 13 ปี ได้รับการยกย่องเป็น XM Legend · แบ่งปันความรู้ Forex, ไอที, AI และการเทรด จากประสบการณ์จริงในตลาดจริง