Remix** เป็น Full Stack React Framework ที่ผูก Frontend-Backend ใกล้ชิดผ่าน Data Loaders และ Form Actions บทความนี้สาธิตการสร้าง Feature Flag System ด้วย Upstash Redis
Source Code: GitHub Repository
1. Setup Project
npx create-remix@latest
npm add --save-dev dotenv
ไฟล์ .env (เพิ่มใน .gitignore):
UPSTASH_REDIS_REST_URL="https://your-db.upstash.io"
UPSTASH_REDIS_REST_TOKEN="your-token"
Update package.json:
{
"scripts": {
"dev": "node -r dotenv/config node_modules/.bin/remix dev"
}
}
2. Data Model: Redis Hash
Feature Flags เก็บใน Redis Hash `features`:
{
"chart": "1",
"graph": "0"
}
Commands ที่ใช้:
HGETALL- ดึงทั้งหมดHSET- Enable/Disable FlagHDEL- ลบ FlagHMGET- ดึงหลาย Flag พร้อมกัน
3. Management Page (app/routes/features.tsx)
Loader: โหลด Flags ทั้งหมด
import { Redis } from "@upstash/redis";
type LoaderData = { features: Array<[string, boolean]> };
const loadAllFeatures = async (): Promise => {
const redis = Redis.fromEnv();
const data = await redis.hgetall("features");
const features: Array<[string, boolean]> = [];
for (let i = 0; i < data.length; i += 2) {
features.push([data[i], data[i + 1] === "1"]);
}
return features.sort((a, b) => a[0].localeCompare(b[0]));
};
export const loader: LoaderFunction = async (): Promise => {
const features = await loadAllFeatures();
return { features };
};
Action: จัดการ Create/Enable/Disable/Delete
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const feature = formData.get("feature") as string;
const action = formData.get("_action") as string;
if (!feature) return json({ error: "Please provide a feature" });
const redis = Redis.fromEnv();
switch (action) {
case "create":
case "enable":
await redis.hset("features", { [feature]: 1 });
break;
case "disable":
await redis.hset("features", { [feature]: 0 });
break;
case "delete":
await redis.hdel("features", feature);
break;
}
return redirect("/features");
};
UI Components
// Add Feature Form
const AddFeature = () => (
<Form method="post" replace>
<input type="hidden" name="_action" value="create" />
<input type="text" name="feature" required placeholder="name" />
<button type="submit">Add</button>
</Form>
);
// Feature List with Enable/Disable/Delete
const FeatureList = () => {
const { features } = useLoaderData<LoaderData>();
return (
<ul>
{features.map(([feature, active]) => (
<li key={feature}>
<Form method="post" replace>
<input type="hidden" name="_action" value={active ? "disable" : "enable"} />
<input type="hidden" name="feature" value={feature} />
<button type="submit" className="btn-naked">{active ? "💪" : "🦾"}</button>
</Form>
<span>{feature}</span>
<Form method="post" replace>
<input type="hidden" name="_action" value="delete" />
<input type="hidden" name="feature" value={feature} />
<button type="submit">Delete</button>
</Form>
</li>
))}
</ul>
);
};
// Main Page
export default function Features() {
return (
<div>
<h1>Features</h1>
<AddFeature />
<FeatureList />
</div>
);
}
4. ใช้ Feature Flags ใน App
โหลด Flag เฉพาะที่ต้องการด้วย HMGET:
type LoaderData = { features: Record };
const loadFeatures = async (keys: string[]): Promise> => {
const redis = Redis.fromEnv();
const data = await redis.hmget("features", ...keys);
return keys.reduce((acc, key, index) => {
acc[key] = data[index] === "1";
return acc;
}, {});
};
export const loader: LoaderFunction = async (): Promise => {
const features = await loadFeatures(["chart", "graph"]);
return { features };
};
ใช้ใน Component:
export default function Index() {
const { features } = useLoaderData<LoaderData>();
return (
<div>
<h1>Dashboard</h1>
{features.chart ? <h2>Chart</h2> : <h2>No Chart</h2>}
{features.graph ? <h2>Graph</h2> : <h2>No Graph</h2>}
</div>
);
}
สรุป
- Remix Loaders + Actions = Backend/Frontend คบคู่กันในไฟล์เดียว
- Upstash Redis Hash เหมาะสำหรับ Feature Flags (Key-Value, Atomic Operations)
- REST API ทำงานได้ใน Edge/Serverless ทุก Platform
- ไม่ต้อง Setup GraphQL API แยก - ใช้ Web Standards (Forms) ตรงๆ