✨ feat: Add client-side feature flag system#3
✨ feat: Add client-side feature flag system#3ForisKuang wants to merge 1 commit intocBioPortal:v0.8.2-custom-v3from
Conversation
Adds a lightweight feature flag store using Jotai atoms with URL query param and localStorage support. Flags can be activated by visiting ?featureFlags=FLAG_NAME and are persisted across page reloads. This enables developers to test features in production without exposing them to all users.
There was a problem hiding this comment.
Pull request overview
This PR introduces a client-side feature flag system that enables developers to test features in production without exposing them to all users. The implementation uses Jotai atoms for state management and supports activation via URL query parameters or localStorage, with automatic persistence across page reloads.
Changes:
- Added a Jotai-based feature flag store with URL query param and localStorage support
- Created a React hook
useFeatureFlagfor checking flag status in components - Exported feature flag utilities from store and hooks index files
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| client/src/store/featureFlags.ts | Core feature flag store implementation with atoms for reading/writing flags, URL parameter parsing, and localStorage persistence |
| client/src/hooks/useFeatureFlag.ts | React hook for checking feature flag status in components |
| client/src/store/index.ts | Added export of feature flag store exports |
| client/src/hooks/index.ts | Added export of useFeatureFlag hook |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| const initialFlags = typeof window !== 'undefined' ? readFlags() : new Set<string>(); | ||
|
|
||
| export const featureFlagsAtom = atom<Set<string>>(initialFlags); |
There was a problem hiding this comment.
The implementation doesn't use the existing createStorageAtom utility from ~/store/jotai-utils, which provides proper SSR support and error handling. The current manual localStorage access could fail during SSR when window is undefined. Consider refactoring to use createStorageAtom for consistency with other Jotai-based stores like favoritesAtom (client/src/store/favorites.ts:19) and proper SSR support with getOnInit: true option.
| localStorage.setItem(FEATURE_FLAG_KEY, [...next].join(DELIMITER)); | ||
| }); | ||
|
|
||
| export const removeFeatureFlagAtom = atom(null, (get, set, flag: string) => { | ||
| const current = get(featureFlagsAtom); | ||
| const next = new Set(current); | ||
| next.delete(flag); | ||
| set(featureFlagsAtom, next); | ||
| localStorage.setItem(FEATURE_FLAG_KEY, [...next].join(DELIMITER)); | ||
| }); | ||
|
|
||
| export const clearFeatureFlagsAtom = atom(null, (_get, set) => { | ||
| set(featureFlagsAtom, new Set<string>()); | ||
| localStorage.removeItem(FEATURE_FLAG_KEY); |
There was a problem hiding this comment.
Direct localStorage access without try-catch blocks could throw exceptions if localStorage is disabled, quota is exceeded, or in private browsing mode. This is inconsistent with the error handling pattern seen in jotai-utils.ts:59-87 where localStorage operations are wrapped in try-catch blocks with fallback to default values.
| function readFlags(): Set<string> { | ||
| const stored = localStorage.getItem(FEATURE_FLAG_KEY); | ||
| const fromStorage = stored ? stored.split(DELIMITER).filter(Boolean) : []; | ||
|
|
||
| const url = new URL(window.location.href); | ||
| const param = url.searchParams.get(FEATURE_FLAG_KEY); | ||
| const fromUrl = param ? param.split(DELIMITER).filter(Boolean) : []; | ||
|
|
||
| const merged = new Set([...fromStorage, ...fromUrl]); | ||
|
|
||
| if (fromUrl.length > 0) { | ||
| localStorage.setItem(FEATURE_FLAG_KEY, [...merged].join(DELIMITER)); | ||
| } | ||
|
|
||
| return merged; | ||
| } |
There was a problem hiding this comment.
The readFlags() function directly accesses window and localStorage without checking if they exist. While line 38 has a typeof window !== 'undefined' check, this doesn't protect the initial call to readFlags() itself. During server-side rendering or in Node.js environments, this will throw a ReferenceError when window is not defined.
| export const addFeatureFlagAtom = atom(null, (get, set, flag: string) => { | ||
| const current = get(featureFlagsAtom); | ||
| const next = new Set(current); | ||
| next.add(flag); | ||
| set(featureFlagsAtom, next); | ||
| localStorage.setItem(FEATURE_FLAG_KEY, [...next].join(DELIMITER)); | ||
| }); | ||
|
|
||
| export const removeFeatureFlagAtom = atom(null, (get, set, flag: string) => { | ||
| const current = get(featureFlagsAtom); | ||
| const next = new Set(current); | ||
| next.delete(flag); | ||
| set(featureFlagsAtom, next); | ||
| localStorage.setItem(FEATURE_FLAG_KEY, [...next].join(DELIMITER)); | ||
| }); | ||
|
|
||
| export const clearFeatureFlagsAtom = atom(null, (_get, set) => { | ||
| set(featureFlagsAtom, new Set<string>()); | ||
| localStorage.removeItem(FEATURE_FLAG_KEY); | ||
| }); |
There was a problem hiding this comment.
The write atoms (addFeatureFlagAtom, removeFeatureFlagAtom, clearFeatureFlagsAtom) access localStorage without environment checks. In SSR or when localStorage is unavailable, these operations will fail. Similar patterns in the codebase include environment checks, such as in createStorageAtomWithEffect (jotai-utils.ts:38) which checks typeof window !== 'undefined' before executing side effects.
| if (fromUrl.length > 0) { | ||
| localStorage.setItem(FEATURE_FLAG_KEY, [...merged].join(DELIMITER)); | ||
| } |
There was a problem hiding this comment.
Feature flags read from URL query parameters are user-controlled input that gets stored in localStorage without validation. While the current implementation with string Set storage is relatively safe, consider validating that URL parameter values match known FeatureFlag enum values before persisting them. This prevents arbitrary strings from being stored and improves type safety.
|
|
||
| export default { | ||
| featureFlagsAtom, | ||
| addFeatureFlagAtom, | ||
| removeFeatureFlagAtom, | ||
| clearFeatureFlagsAtom, | ||
| }; |
There was a problem hiding this comment.
The default export object at the end of the file is inconsistent with other Jotai-based store files in the codebase (favorites.ts, mcp.ts) which only use named exports. Since the store/index.ts uses export * from './featureFlags', this default export is not utilized and creates unnecessary inconsistency. Consider removing it.
| export default { | |
| featureFlagsAtom, | |
| addFeatureFlagAtom, | |
| removeFeatureFlagAtom, | |
| clearFeatureFlagsAtom, | |
| }; |
Adds a lightweight feature flag store using Jotai atoms with URL query param and localStorage support. Flags can be activated by visiting ?featureFlags=FLAG_NAME and are persisted across page reloads. This enables developers to test features in production without exposing them to all users.
Summary
?featureFlags=FLAG_NAME) or localStorage, enabling developers to test features in production without exposing them to all usersChanges
New Files
client/src/store/featureFlags.ts— Jotai-based feature flag store withFeatureFlagenum, read/write atoms, and URL + localStorage initializationclient/src/hooks/useFeatureFlag.ts—useFeatureFlag(flag)React hook that returns a booleanModified Files
client/src/store/index.ts— Export feature flag atoms and enumclient/src/hooks/index.ts— ExportuseFeatureFlaghookUsage
Define a flag
Test Plan
EOF
)"