Skip to content

✨ feat: Add client-side feature flag system#3

Open
ForisKuang wants to merge 1 commit intocBioPortal:v0.8.2-custom-v3from
ForisKuang:feat/client-feature-flags
Open

✨ feat: Add client-side feature flag system#3
ForisKuang wants to merge 1 commit intocBioPortal:v0.8.2-custom-v3from
ForisKuang:feat/client-feature-flags

Conversation

@ForisKuang
Copy link

@ForisKuang ForisKuang commented Feb 25, 2026

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

  • Adds a lightweight, client-side feature flag system inspired by cBioPortal/cbioportal-frontend
  • Flags can be activated via URL query param (?featureFlags=FLAG_NAME) or localStorage, enabling developers to test features in production without exposing them to all users
  • Flags set via URL are automatically persisted to localStorage so they survive page reloads
  • Built with Jotai atoms to integrate cleanly with LibreChat's existing state management

Changes

New Files

  • client/src/store/featureFlags.ts — Jotai-based feature flag store with FeatureFlag enum, read/write atoms, and URL + localStorage initialization
  • client/src/hooks/useFeatureFlag.tsuseFeatureFlag(flag) React hook that returns a boolean

Modified Files

  • client/src/store/index.ts — Export feature flag atoms and enum
  • client/src/hooks/index.ts — Export useFeatureFlag hook

Usage

Define a flag

// client/src/store/featureFlags.ts
export enum FeatureFlag {
  PRODUCT_FEEDBACK = 'PRODUCT_FEEDBACK',
  MY_NEW_FEATURE = 'MY_NEW_FEATURE',
}

Test Plan

  • URL activation: Visit ?featureFlags=PRODUCT_FEEDBACK and verify useFeatureFlag(FeatureFlag.PRODUCT_FEEDBACK) returns true
  • localStorage persistence: Activate via URL, reload the page (without the query param), verify the flag is still active
  • Multiple flags: Visit ?featureFlags=FLAG_A,FLAG_B and verify both are active
  • No flags: Visit without featureFlags param and with empty localStorage, verify all flags return false
  • Clear: Call clearFeatureFlagsAtom and verify all flags are removed from both state and localStorage
  • Build: npm run build:packages && npm run build:client passes cleanly
    EOF
    )"

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.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 useFeatureFlag for 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);
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +60
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);
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +36
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;
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +61
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);
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +33
if (fromUrl.length > 0) {
localStorage.setItem(FEATURE_FLAG_KEY, [...merged].join(DELIMITER));
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +68

export default {
featureFlagsAtom,
addFeatureFlagAtom,
removeFeatureFlagAtom,
clearFeatureFlagsAtom,
};
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
export default {
featureFlagsAtom,
addFeatureFlagAtom,
removeFeatureFlagAtom,
clearFeatureFlagsAtom,
};

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants