Back to catalog

feature-flags

Skill from retex73/music-app

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • Updated: 2025-11-22

Download the latest version (v1.0.0) from this page.

SKILL.md

# Feature Flags Skill

## Purpose

Implement feature flags (feature toggles) to enable merging incomplete work to main branch while maintaining deployment-ready code, essential for trunk-based development.

## Scope

- Environment-based feature flags
- React component wrapping patterns
- Feature flag management
- Flag cleanup strategies
- Testing with flags

## Why Feature Flags for Trunk-Based Development

Feature flags allow you to:
- ✅ Merge incomplete features to main daily
- ✅ Keep main branch deployment-ready
- ✅ Avoid long-lived feature branches
- ✅ Deploy code that's not user-visible yet
- ✅ Test features in production before release
- ✅ Gradually roll out features

## Implementation Patterns

### Simple Environment-Based Flags

**Configuration File**:
```javascript
// src/config/featureFlags.js
export const FEATURE_FLAGS = {
  // New features
  PLAYLISTS: process.env.REACT_APP_FEATURE_PLAYLISTS === 'true',
  SOCIAL_SHARING: process.env.REACT_APP_FEATURE_SOCIAL === 'true',
  COLLABORATIVE_SESSIONS: process.env.REACT_APP_FEATURE_COLLAB === 'true',

  // Improvements
  NEW_AUDIO_PLAYER: process.env.REACT_APP_FEATURE_NEW_PLAYER === 'true',
  ADVANCED_SEARCH: process.env.REACT_APP_FEATURE_ADVANCED_SEARCH === 'true',

  // Experiments
  AI_RECOMMENDATIONS: process.env.REACT_APP_FEATURE_AI_RECS === 'true',
};

// Helper function
export function isFeatureEnabled(featureName) {
  return FEATURE_FLAGS[featureName] === true;
}
```

**Environment Files**:
```bash
# .env.development (flags ON for testing)
REACT_APP_FEATURE_PLAYLISTS=true
REACT_APP_FEATURE_SOCIAL=true
REACT_APP_FEATURE_COLLAB=false

# .env.production (flags OFF until ready)
REACT_APP_FEATURE_PLAYLISTS=false
REACT_APP_FEATURE_SOCIAL=false
REACT_APP_FEATURE_COLLAB=false

# .env.staging (gradual rollout)
REACT_APP_FEATURE_PLAYLISTS=true  # Testing in staging
REACT_APP_FEATURE_SOCIAL=false
```

### Component Wrapping

**Basic Conditional Rendering**:
```javascript
import { FEATURE_FLAGS } from '../config/featureFlags';

function TuneDetailsPage() {
  return (
    <div>
      <TuneInfo />
      <AudioPlayer />

      {/* New incomplete feature wrapped in flag */}
      {FEATURE_FLAGS.PLAYLISTS && (
        <AddToPlaylistButton tuneId={tuneId} />
      )}

      {/* Old component shown when flag disabled */}
      {!FEATURE_FLAGS.NEW_AUDIO_PLAYER && <OldAudioPlayer />}

      {/* New component shown when flag enabled */}
      {FEATURE_FLAGS.NEW_AUDIO_PLAYER && <NewAudioPlayer />}
    </div>
  );
}
```

**Route Wrapping**:
```javascript
import { Routes, Route } from 'react-router-dom';
import { FEATURE_FLAGS } from '../config/featureFlags';

function AppRoutes() {
  return (
    <Routes>
      <Route path="/" element={<HomePage />} />

      {/* Conditional routes */}
      {FEATURE_FLAGS.PLAYLISTS && (
        <>
          <Route path="/playlists" element={<PlaylistsPage />} />
          <Route path="/playlist/:id" element={<PlaylistDetailsPage />} />
        </>
      )}

      <Route path="*" element={<NotFoundPage />} />
    </Routes>
  );
}
```

**Navigation Wrapping**:
```javascript
function NavBar() {
  return (
    <AppBar>
      <Toolbar>
        <Link to="/">Home</Link>
        <Link to="/favorites">Favorites</Link>

        {/* Show link only when feature is ready */}
        {FEATURE_FLAGS.PLAYLISTS && (
          <Link to="/playlists">Playlists</Link>
        )}
      </Toolbar>
    </AppBar>
  );
}
```

### Context-Based Flags (Advanced)

**Feature Flag Provider**:
```javascript
// src/contexts/FeatureFlagsContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';

const FeatureFlagsContext = createContext();

export function useFeatureFlags() {
  const context = useContext(FeatureFlagsContext);
  if (!context) {
    throw new Error('useFeatureFlags must be used within FeatureFlagsProvider');
  }
  return context;
}

export function FeatureFlagsProvider({ children }) {
  const [flags, setFlags] = useState({
    playlists: process.env.REACT_APP_FEATURE_PLAYLISTS === 'true',
    socialSharing: process.env.REACT_APP_FEATURE_SOCIAL === 'true',
    collaborativeSessions: process.env.REACT_APP_FEATURE_COLLAB === 'true',
  });

  // Optional: Load from Firebase Remote Config
  useEffect(() => {
    // Load remote flags from Firestore or Remote Config
    // This allows changing flags without redeployment
  }, []);

  const isEnabled = (featureName) => flags[featureName] === true;

  return (
    <FeatureFlagsContext.Provider value={{ flags, isEnabled, setFlags }}>
      {children}
    </FeatureFlagsContext.Provider>
  );
}
```

**Usage with Context**:
```javascript
import { useFeatureFlags } from '../contexts/FeatureFlagsContext';

function Component() {
  const { isEnabled } = useFeatureFlags();

  return (
    <div>
      {isEnabled('playlists') && <PlaylistFeature />}
    </div>
  );
}
```

### Firebase Remote Config (Production-Grade)

```javascript
import { getRemoteConfig, getValue, fetchAndActivate } from 'firebase/remote-config';

const remoteConfig = getRemoteConfig();
remoteConfig.settings.minimumFetchIntervalMillis = 3600000; // 1 hour

// Fetch flags from Firebase
export async function loadRemoteFeatureFlags() {
  try {
    await fetchAndActivate(remoteConfig);

    return {
      playlists: getValue(remoteConfig, 'feature_playlists').asBoolean(),
      socialSharing: getValue(remoteConfig, 'feature_social').asBoolean(),
    };
  } catch (error) {
    console.error('Error loading remote config:', error);
    // Fallback to environment variables
    return {
      playlists: process.env.REACT_APP_FEATURE_PLAYLISTS === 'true',
      socialSharing: process.env.REACT_APP_FEATURE_SOCIAL === 'true',
    };
  }
}
```

## Testing with Feature Flags

### Test Both States
```javascript
describe('TuneDetailsPage', () => {
  beforeEach(() => {
    jest.resetModules();
  });

  describe('with playlists feature enabled', () => {
    beforeEach(() => {
      process.env.REACT_APP_FEATURE_PLAYLISTS = 'true';
    });

    test('shows add to playlist button', () => {
      render(<TuneDetailsPage />);
      expect(screen.getByText(/add to playlist/i)).toBeInTheDocument();
    });
  });

  describe('with playlists feature disabled', () => {
    beforeEach(() => {
      process.env.REACT_APP_FEATURE_PLAYLISTS = 'false';
    });

    test('hides add to playlist button', () => {
      render(<TuneDetailsPage />);
      expect(screen.queryByText(/add to playlist/i)).not.toBeInTheDocument();
    });
  });
});
```

## Flag Lifecycle Management

### Adding a New Flag
```javascript
// 1. Add to featureFlags.js
export const FEATURE_FLAGS = {
  NEW_FEATURE: process.env.REACT_APP_FEATURE_NEW === 'true',
};

// 2. Add to .env files
// .env.development
REACT_APP_FEATURE_NEW=true

// .env.production
REACT_APP_FEATURE_NEW=false

// 3. Wrap code in flag
{FEATURE_FLAGS.NEW_FEATURE && <NewComponent />}

// 4. Document flag
// Add comment with removal date
// NEW_FEATURE: Remove after 2024-12-01 when fully launched
```

### Removing a Flag (Feature Complete)
```javascript
// 1. Enable flag in all environments
REACT_APP_FEATURE_PLAYLISTS=true  // In all .env files

// 2. Deploy and verify
// 3. Remove flag checks from code
// Before:
{FEATURE_FLAGS.PLAYLISTS && <PlaylistButton />}

// After:
<PlaylistButton />

// 4. Remove from featureFlags.js
// 5. Remove from .env files

// 6. Commit cleanup
git commit -m "chore: remove PLAYLISTS feature flag (fully launched)"
```

## Best Practices

### ✅ Do

1. **Document flags** - Add comment with purpose and removal date
2. **Test both states** - Flag on and flag off
3. **Clean up flags** - Remove when feature complete
4. **Use for incomplete work** - Enable trunk-based merging
5. **Default to disabled** - Safety first in production
6. **Simple conditions** - Easy to understand and maintain

### ❌ Don't

1. **Accumulate dead flags** - Remove completed flags
2. **Complex flag logic** - Avoid nested flag conditions
3. **Use for A/B testing** - Different purpose (use analytics tools)
4. **Forget to document** - Always explain flag purpose
5. **Skip testing** - Test with flag on AND off
6. **Leave flags forever** - They're temporary by design

## Flag Naming Conventions

```
✅ Good names:
FEATURE_PLAYLISTS
FEATURE_NEW_AUDIO_PLAYER
FEATURE_SOCIAL_SHARING
ENABLE_ADVANCED_SEARCH
ENABLE_AI_RECOMMENDATIONS

❌ Bad names:
NEW_STUFF
TEMP_FIX
JOHNS_FEATURE
BETA
EXPERIMENTAL
```

## Common Patterns

### Gradual Rollout
```javascript
// Start: Flag off in production
REACT_APP_FEATURE_PLAYLISTS=false

// Phase 1: Enable for internal testing
// Enable in staging/dev

// Phase 2: Enable for subset of users (10%)
if (FEATURE_FLAGS.PLAYLISTS && Math.random() < 0.1) {
  return <NewFeature />;
}

// Phase 3: Enable for all users
REACT_APP_FEATURE_PLAYLISTS=true

// Phase 4: Remove flag completely
```

### Kill Switch
```javascript
// Emergency disable mechanism
const KILL_SWITCHES = {
  PLAYLISTS: process.env.REACT_APP_KILL_PLAYLISTS === 'true',
};

// Override feature flag if kill switch active
const isPlaylistsEnabled =
  FEATURE_FLAGS.PLAYLISTS && !KILL_SWITCHES.PLAYLISTS;
```

## Integration with Music-App

### Example: Adding Playlists Feature

**Day 1** (Start development):
```javascript
// Add flag
export const FEATURE_FLAGS = {
  PLAYLISTS: process.env.REACT_APP_FEATURE_PLAYLISTS === 'true',
};

// Create data model (incomplete, but mergeable)
// Wrap in flag
if (FEATURE_FLAGS.PLAYLISTS) {
  // Incomplete playlist logic
}

// Merge to main (flag disabled in production)
```

**Day 2** (Continue development):
```javascript
// Add UI components (still incomplete)
{FEATURE_FLAGS.PLAYLISTS && <PlaylistButton />}

// Merge to main again (flag still disabled)
```

**Day 3** (Complete feature):
```javascript
// Add final tests and polish
// Enable flag in staging
REACT_APP_FEATURE_PLAYLISTS=true  // .env.staging

// Test thoroughly
// Enable in production
REACT_APP_FEATURE_PLAYLISTS=true  // .env.production

// Deploy
```

**Day 7** (Cleanup):
```javascript
// Remove flag completely
// Feature proven stable
git commit -m "chore: remove PLAYLISTS feature flag"
```

## Resources

- **Martin Fowler on Feature Toggles**: https://martinfowler.com/articles/feature-toggles.html
- **Firebase Remote Config**: https://firebase.google.com/docs/remote-config
- **LaunchDarkly Guide**: https://launchdarkly.com/blog/what-are-feature-flags/