SiamCafe.net Blog
Technology

Radix UI Primitives Progressive Delivery

radix ui primitives progressive delivery
Radix UI Primitives Progressive Delivery | SiamCafe Blog
2026-04-09· อ. บอม — SiamCafe.net· 8,580 คำ

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

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 ต่างๆ

📖 บทความที่เกี่ยวข้อง

Radix UI Primitives Blue Green Canary Deployอ่านบทความ → Radix UI Primitives Freelance IT Careerอ่านบทความ → Radix UI Primitives Observability Stackอ่านบทความ → Radix UI Primitives API Integration เชื่อมต่อระบบอ่านบทความ → Radix UI Primitives Incident Managementอ่านบทความ →

📚 ดูบทความทั้งหมด →