Radix UI Primitives
Radix UI Primitives เป็น UI Component Library สำหรับ React ที่ออกแบบมาให้เป็น Unstyled Components ให้เฉพาะ Behavior และ Accessibility (WAI-ARIA) ที่ถูกต้อง Developer สามารถ Style เองได้อิสระ 100% ใช้ร่วมกับ Tailwind CSS, CSS Modules หรือ Styled Components
เมื่อรวมกับ Progressive Delivery Strategy ใช้ Feature Flags ควบคุมการปล่อย Components ใหม่ให้ผู้ใช้ทีละกลุ่ม Canary Deployment ทดสอบกับผู้ใช้ส่วันนี้อยก่อน A/B Testing เปรียบเทียบ Design ต่างๆ
Radix UI Components พร้อม Tailwind CSS
// === Radix UI + Tailwind CSS Components ===
// npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu
// npm install @radix-ui/react-tabs @radix-ui/react-tooltip
// npm install @radix-ui/react-switch @radix-ui/react-select
// npm install tailwindcss class-variance-authority clsx
// components/Dialog.tsx — Accessible Modal Dialog
import * as Dialog from "@radix-ui/react-dialog";
import { X } from "lucide-react";
interface ModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description?: string;
children: React.ReactNode;
}
export function Modal({ open, onOpenChange, title, description, children }: ModalProps) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm
data-[state=open]:animate-in data-[state=closed]:animate-out
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2
-translate-y-1/2 bg-white rounded-xl shadow-xl p-6 w-full max-w-md
data-[state=open]:animate-in data-[state=closed]:animate-out
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95">
<Dialog.Title className="text-lg font-semibold text-gray-900">
{title}
</Dialog.Title>
{description && (
<Dialog.Description className="mt-2 text-sm text-gray-500">
{description}
</Dialog.Description>
)}
<div className="mt-4">{children}</div>
<Dialog.Close className="absolute right-4 top-4 rounded-sm
opacity-70 hover:opacity-100 transition-opacity">
<X className="h-4 w-4" />
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
// components/Tabs.tsx — Accessible Tabs
import * as Tabs from "@radix-ui/react-tabs";
interface TabItem {
value: string;
label: string;
content: React.ReactNode;
}
export function TabGroup({ tabs, defaultValue }: { tabs: TabItem[]; defaultValue?: string }) {
return (
<Tabs.Root defaultValue={defaultValue || tabs[0]?.value}>
<Tabs.List className="flex border-b border-gray-200">
{tabs.map((tab) => (
<Tabs.Trigger key={tab.value} value={tab.value}
className="px-4 py-2 text-sm font-medium text-gray-500
hover:text-gray-700 border-b-2 border-transparent
data-[state=active]:border-blue-500
data-[state=active]:text-blue-600 transition-colors">
{tab.label}
</Tabs.Trigger>
))}
</Tabs.List>
{tabs.map((tab) => (
<Tabs.Content key={tab.value} value={tab.value} className="py-4">
{tab.content}
</Tabs.Content>
))}
</Tabs.Root>
);
}
// components/DropdownMenu.tsx — Accessible Dropdown
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
interface MenuItem {
label: string;
onClick: () => void;
icon?: React.ReactNode;
destructive?: boolean;
}
export function Dropdown({ trigger, items }: { trigger: React.ReactNode; items: MenuItem[] }) {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>{trigger}</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="min-w-[180px] bg-white rounded-lg
shadow-lg border border-gray-200 p-1 animate-in fade-in-0 zoom-in-95">
{items.map((item, i) => (
<DropdownMenu.Item key={i} onClick={item.onClick}
className={`flex items-center gap-2 px-3 py-2 text-sm rounded-md
cursor-pointer outline-none
`}>
{item.icon} {item.label}
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}
Feature Flags สำหรับ Progressive Delivery
# feature_flags.py — Feature Flag System สำหรับ Progressive Delivery
import hashlib
import json
import time
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
from enum import Enum
class RolloutStrategy(Enum):
ALL = "all"
PERCENTAGE = "percentage"
USER_LIST = "user_list"
GRADUAL = "gradual"
@dataclass
class FeatureFlag:
name: str
enabled: bool
strategy: RolloutStrategy
percentage: float = 0
user_list: List[str] = field(default_factory=list)
gradual_schedule: Dict[str, float] = field(default_factory=dict)
description: str = ""
created_at: float = field(default_factory=time.time)
class FeatureFlagService:
"""Feature Flag Service สำหรับ Progressive Delivery"""
def __init__(self):
self.flags: Dict[str, FeatureFlag] = {}
self.evaluations: Dict[str, int] = {}
def create_flag(self, name, strategy=RolloutStrategy.ALL,
percentage=0, user_list=None, description=""):
flag = FeatureFlag(
name=name, enabled=True, strategy=strategy,
percentage=percentage,
user_list=user_list or [],
description=description,
)
self.flags[name] = flag
return flag
def is_enabled(self, flag_name, user_id=None, attributes=None):
"""ตรวจสอบว่า Feature เปิดสำหรับ User นี้หรือไม่"""
flag = self.flags.get(flag_name)
if not flag or not flag.enabled:
return False
# Track evaluations
self.evaluations[flag_name] = self.evaluations.get(flag_name, 0) + 1
if flag.strategy == RolloutStrategy.ALL:
return True
elif flag.strategy == RolloutStrategy.PERCENTAGE:
if not user_id:
return False
hash_val = int(hashlib.md5(
f"{flag_name}:{user_id}".encode()
).hexdigest(), 16)
return (hash_val % 100) < flag.percentage
elif flag.strategy == RolloutStrategy.USER_LIST:
return user_id in flag.user_list
elif flag.strategy == RolloutStrategy.GRADUAL:
current_pct = self._get_current_percentage(flag)
if not user_id:
return False
hash_val = int(hashlib.md5(
f"{flag_name}:{user_id}".encode()
).hexdigest(), 16)
return (hash_val % 100) < current_pct
return False
def _get_current_percentage(self, flag):
"""คำนวณ Percentage ปัจจุบันตาม Schedule"""
now = time.time()
current_pct = 0
for ts_str, pct in sorted(flag.gradual_schedule.items()):
if float(ts_str) <= now:
current_pct = pct
return current_pct
def set_percentage(self, flag_name, percentage):
"""ปรับ Percentage"""
if flag_name in self.flags:
self.flags[flag_name].percentage = percentage
def kill_switch(self, flag_name):
"""ปิด Feature ทันที"""
if flag_name in self.flags:
self.flags[flag_name].enabled = False
def dashboard(self):
"""แสดง Feature Flags Dashboard"""
print(f"\n{'='*60}")
print(f"Feature Flags Dashboard")
print(f"{'='*60}")
for name, flag in self.flags.items():
status = "ON" if flag.enabled else "OFF"
evals = self.evaluations.get(name, 0)
print(f"\n [{status:>3}] {name}")
print(f" Strategy: {flag.strategy.value}")
if flag.strategy == RolloutStrategy.PERCENTAGE:
print(f" Percentage: {flag.percentage}%")
elif flag.strategy == RolloutStrategy.USER_LIST:
print(f" Users: {len(flag.user_list)}")
print(f" Evaluations: {evals}")
if flag.description:
print(f" Description: {flag.description}")
# === ตัวอย่าง ===
ff = FeatureFlagService()
# Canary: เปิดให้ 5% ก่อน
ff.create_flag("new_search_ui", RolloutStrategy.PERCENTAGE,
percentage=5, description="New Search UI with Radix Components")
# Beta Users
ff.create_flag("dark_mode", RolloutStrategy.USER_LIST,
user_list=["user_001", "user_002", "user_003"],
description="Dark Mode Feature")
# ทดสอบ
for i in range(100):
user = f"user_{i:03d}"
ff.is_enabled("new_search_ui", user)
ff.is_enabled("dark_mode", user)
ff.dashboard()
# ค่อยๆเพิ่ม: 5% -> 25% -> 50% -> 100%
ff.set_percentage("new_search_ui", 25)
print("\nIncreased new_search_ui to 25%")
CI/CD Pipeline สำหรับ Progressive Delivery
# === GitHub Actions — Progressive Delivery Pipeline ===
# .github/workflows/progressive-delivery.yml
name: Progressive Delivery
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: myapp-frontend
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run type-check
- run: npm run test -- --coverage
# Accessibility Tests
- run: npm run test:a11y
- run: npx playwright test
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t $REGISTRY/$IMAGE_NAME:} .
- run: docker push $REGISTRY/$IMAGE_NAME:}
canary:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy Canary (5%)
run: |
kubectl set image deployment/frontend-canary \
frontend=$REGISTRY/$IMAGE_NAME:} \
-n production
# Istio Traffic Split: 5% canary
kubectl apply -f - << 'EOF'
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: frontend
namespace: production
spec:
hosts: ["www.example.com"]
http:
- route:
- destination:
host: frontend-stable
weight: 95
- destination:
host: frontend-canary
weight: 5
EOF
- name: Monitor Canary (5 min)
run: |
sleep 300
ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query" \
--data-urlencode 'query=rate(http_requests_total{status=~"5.."}[5m])' \
| jq '.data.result[0].value[1]')
if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
echo "Canary failed! Error rate: $ERROR_RATE"
exit 1
fi
- name: Promote to 50%
run: |
# Update traffic split to 50/50
echo "Promoting canary to 50%"
- name: Promote to 100%
run: |
kubectl set image deployment/frontend-stable \
frontend=$REGISTRY/$IMAGE_NAME:} \
-n production
echo "Full rollout complete"
Best Practices
- Unstyled Components: ใช้ Radix UI Primitives เป็นฐาน Style เองด้วย Tailwind CSS
- Accessibility First: Radix UI มี WAI-ARIA ในตัว ทดสอบด้วย axe-core
- Feature Flags: ใช้ Feature Flags ควบคุม Component ใหม่ เปิดปิดโดยไม่ต้อง Deploy
- Canary Release: ปล่อย Feature ให้ผู้ใช้ 5% ก่อน Monitor Error Rate แล้วค่อยขยาย
- Kill Switch: ทุก Feature ต้องมี Kill Switch ปิดได้ทันทีถ้ามีปัญหา
- A/B Testing: ใช้ Feature Flags ทำ A/B Test เปรียบเทียบ UI ต่างๆ
Radix UI Primitives คืออะไร
Open-source UI Component Library สำหรับ React เน้น Accessibility Unstyled Components Composability ให้ Behavior WAI-ARIA ที่ถูกต้อง Developer Style เองได้อิสระ ใช้กับ Tailwind CSS ได้
Progressive Delivery คืออะไร
แนวทาง Deploy ค่อยๆปล่อย Feature ให้ผู้ใช้ทีละกลุ่ม Feature Flags ควบคุมว่าใครเห็น Canary Deployment ปล่อยให้ส่วันนี้อยก่อน ถ้าไม่มีปัญหาค่อยขยาย ลดความเสี่ยง
ทำไมต้องใช้ Radix UI แทน Material UI
Radix UI เป็น Unstyled อิสระออกแบบ 100% ไม่ต้อง Override Styles Bundle Size เล็กกว่า Accessibility ดีเยี่ยม Composable ใช้กับ Design System ใดก็ได้ Material UI มี Styles พร้อม เหมาะงานเร็ว
Feature Flags คืออะไร
กลไกควบคุม Feature แสดงให้ผู้ใช้กลุ่มไหน ไม่ต้อง Deploy ใหม่ เปิดปิด Runtime ทำ A/B Testing Canary Release Kill Switch ตัวอย่าง LaunchDarkly Unleash Flagsmith
สรุป
Radix UI Primitives ให้ Accessible Unstyled Components ที่ Style เองได้อิสระ เมื่อรวมกับ Progressive Delivery ใช้ Feature Flags ควบคุม Component ใหม่ Canary Deployment ปล่อยให้ 5% ก่อน Monitor Error Rate แล้วค่อยขยายไป 100% ทุก Feature มี Kill Switch ปิดได้ทันที ใช้ A/B Testing เปรียบเทียบ UI ต่างๆ
