React 中的动态配置 — 无卡顿的 Feature Flags
看起来您只提供了来源链接,而没有贴出需要翻译的正文内容。请把要翻译的文本(除代码块和 URL 之外)粘贴在这里,我会按照要求将其翻译成简体中文并保留原有的格式。谢谢!
React 中特性标记的问题
大多数 React 应用以三种方式处理配置:
1. 构建时环境变量
const isNewCheckout = process.env.REACT_APP_NEW_CHECKOUT === "true";
function Checkout() {
return isNewCheckout ? : ;
}
要改动?重新构建并重新部署整个应用。
2. 从顶层传递 Props
function App() {
const [flags, setFlags] = useState(null);
useEffect(() => {
fetch("/api/flags")
.then((r) => r.json())
.then(setFlags);
}, []);
if (!flags) return ;
return ;
}
可行,但现在你得把 props 通过很多组件层层传递。使用 Context 可以解决层层传递的问题,但每次标记变化都会重新渲染所有内容。
3. 使用第三方 SDK 及其模式
import { useFlags } from "some-feature-flag-sdk";
function Checkout() {
const { newCheckout } = useFlags();
// …
}
更好,但现在你要为每月 300 美元的账单买单,并且受限于供应商的 API。
动态配置应有的体验
function Checkout() {
const isNewCheckout = useConfig("new-checkout");
return isNewCheckout ? : ;
}
当有人在仪表盘中切换该值时:
- 组件自动重新渲染
- 无需页面刷新
- 无需属性钻取
- 完整的 TypeScript 支持
使用 Replane 构建
Replane 是开源的(MIT 许可证),正好实现了这些功能。
1. 包裹你的应用
import { ReplaneProvider } from "@replanejs/react";
function App() {
return (
}
>
);
}
Provider 通过 Server‑Sent Events 进行连接。配置只加载一次,然后实时流式推送更新。
2. 在任意位置读取配置
import { useConfig } from "@replanejs/react";
function Checkout() {
const isNewCheckout = useConfig("new-checkout");
const discountBanner = useConfig("checkout-banner-text");
return (
{discountBanner && }
{isNewCheckout ? : }
);
}
该 Hook 只订阅所请求的配置键,因此只有使用该键的组件在配置变更时会重新渲染。
3. 使用上下文添加目标化
function Checkout() {
const { user } = useAuth();
const rateLimit = useConfig("api-rate-limit", {
context: {
userId: user.id,
plan: user.subscription,
country: user.country,
},
});
// Premium 用户可能得到 10 000,免费用户得到 100
}
覆盖规则在 Replane 仪表盘中定义:
- 如果
plan等于premium→ 返回10000 - 如果
country等于DE→ 返回500 - 默认 →
100
添加新规则时无需修改代码。
使其类型安全
通用 Hook 可以工作,但你可以收紧契约:
// config.ts
import { createConfigHook } from "@replanejs/react";
interface AppConfigs {
"new-checkout": boolean;
"checkout-banner-text": string | null;
"api-rate-limit": number;
"pricing-tiers": {
free: { requests: number };
pro: { requests: number };
};
}
export const useAppConfig = createConfigHook();
// Checkout.tsx
import { useAppConfig } from "./config";
function Checkout() {
// Autocomplete works, type is inferred
const isNewCheckout = useAppConfig("new-checkout");
// ^? boolean
const pricing = useAppConfig("pricing-tiers");
// ^? { free: { requests: number }; pro: { requests: number } }
}
配置名称拼写错误?TypeScript 会捕获。类型假设错误?TypeScript 也会捕获。
处理加载状态
选择适合您应用的策略:
选项 1 – Loader prop(默认)
}
>
显示加载指示,直到 所有 配置加载完成。简单,但会阻塞整个应用。
选项 2 – Suspense
}>
与 React 的 Suspense 集成。如果您已经在使用它进行数据获取,这将是理想选择。
选项 3 – Async mode with defaults
立即使用提供的默认值渲染。实际值在连接建立后会替换默认值。没有加载 UI,但值可能在首次渲染后“翻转”。
服务器端渲染 (SSR) 水合
// On server
import { Replane, getReplaneSnapshot } from "@replanejs/react";
const replane = new Replane();
await replane.connect({ baseUrl: "...", sdkKey: "..." });
const snapshot = replane.getSnapshot(); // ← server‑fetched snapshot
// Pass `snapshot` to the client via props or serialize it into HTML
// On client
客户端会立即从快照进行水合,然后再连接以获取实时更新。
何时使用
合适的场景
- 用于渐进式发布的功能标记
- 简单的 A/B 测试变体
- 按用户或租户的定制
- 市场营销想要调整的 UI 文本
- 运营限制(速率限制、最大条目数、超时)
- 用于事件响应的紧急开关
保持为构建时配置
- API 端点(运行时不要更改)
- 分析键(运行时不要更改)
- 任何影响构建输出的内容
常见错误
1. 将所有内容放入动态配置
并非每个值都需要实时更新。如果在应用运行期间不会改变,就保持为静态。
2. 没有默认值
// Bad – crashes if the config server is down
const limit = useConfig("rate-limit");
// Good – works even before the connection is established
{/* ... */}
3. 上下文放错位置
// Bad – creates a new object each render, breaks memoization
const value = useConfig("limit", { context: { userId: user.id } });
// Better – stable reference
const context = useMemo(() => ({ userId: user.id }), [user.id]);
const value = useConfig("limit", { context });
4. 忽视错误边界
import { ErrorBoundary } from "react-error-boundary";
Config failed to load}>
连接失败会抛出异常;使用错误边界捕获它们。
入门
npm install @replanejs/react
如果你自行托管 Replane,请将 baseUrl 指向你的实例。否则,免费套餐可在 cloud.replane.dev 使用。
import { ReplaneProvider, useConfig } from "@replanejs/react";
function App() {
return (
Loading…}
>
);
}
function Main() {
const isEnabled = useConfig("feature-enabled");
return Feature is {isEnabled ? "on" : "off"};
}
那个结账紧急开关?现在已经变成仪表盘里的一个切换按钮。产品团队可以自行切换。10 % 的灰度发布?只需改动一条规则,无需部署。当凌晨 2 点出现故障时,我可以直接在手机上禁用它。
有疑问吗? 留下评论或查看 GitHub 仓库。