Feature Flags - Frontend Integration Guide
Overview
Feature flags allow you to control feature availability across environments (Development, Staging, Production). This guide covers integrating feature flags into the FishingLog frontend applications.
API Endpoints
Get Current Environment Feature Flags
GET /api/admin/feature-flags/current-environment
Response:
{
"environment": "Development",
"featureFlags": {
"Circles": true,
"Tournaments": true,
"Charters": false
}
}
Get All Feature Flags (Admin Only)
GET /api/admin/feature-flags
Response:
[
{
"id": 1,
"key": "Circles",
"name": "Circles Feature",
"description": "Custom friend groups for targeted sharing",
"category": "Social",
"isEnabledInDevelopment": true,
"isEnabledInStaging": true,
"isEnabledInProduction": false,
"isEnabledInCurrentEnvironment": true,
"currentEnvironment": "Development",
"isActive": true
}
]
Implementation Strategy
1. Feature Flag Service/Hook
Create a service or React hook to fetch and cache feature flags:
// hooks/useFeatureFlags.ts
import { useQuery } from '@tanstack/react-query';
interface FeatureFlags {
[key: string]: boolean;
}
export function useFeatureFlags() {
return useQuery<FeatureFlags>({
queryKey: ['featureFlags'],
queryFn: async () => {
const response = await fetch('/api/admin/feature-flags/current-environment', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
return data.featureFlags;
},
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
refetchOnWindowFocus: false,
});
}
export function useFeatureFlag(featureKey: string) {
const { data: flags } = useFeatureFlags();
return flags?.[featureKey] ?? false;
}
2. Feature Gate Component
Create a component to conditionally render features:
// components/FeatureGate.tsx
import { useFeatureFlag } from '@/hooks/useFeatureFlags';
interface FeatureGateProps {
feature: string;
children: React.ReactNode;
fallback?: React.ReactNode;
}
export function FeatureGate({ feature, children, fallback = null }: FeatureGateProps) {
const isEnabled = useFeatureFlag(feature);
if (!isEnabled) {
return <>{fallback}</>;
}
return <>{children}</>;
}
3. Usage Examples
Conditional Rendering
// Hide Circles feature if disabled
<FeatureGate feature="Circles" fallback={null}>
<NavLink to="/circles">Circles</NavLink>
</FeatureGate>
Conditional Navigation
// Filter navigation items based on feature flags
const navigationItems = [
{ path: '/dashboard', label: 'Dashboard' },
{ path: '/circles', label: 'Circles', feature: 'Circles' },
{ path: '/tournaments', label: 'Tournaments', feature: 'Tournaments' },
].filter(item => !item.feature || useFeatureFlag(item.feature));
Conditional Routes
// Next.js App Router Example
// app/circles/page.tsx
import { redirect } from 'next/navigation';
import { useFeatureFlag } from '@/hooks/useFeatureFlags';
export default function CirclesPage() {
const isEnabled = useFeatureFlag('Circles');
if (!isEnabled) {
redirect('/404');
}
return <CirclesContent />;
}
React Router Example
// Protected route component
import { Navigate } from 'react-router-dom';
import { useFeatureFlag } from '@/hooks/useFeatureFlags';
export function ProtectedFeatureRoute({
feature,
children
}: {
feature: string;
children: React.ReactNode
}) {
const isEnabled = useFeatureFlag(feature);
if (!isEnabled) {
return <Navigate to="/404" replace />;
}
return <>{children}</>;
}
// Usage in router
<Route
path="/circles"
element={
<ProtectedFeatureRoute feature="Circles">
<CirclesPage />
</ProtectedFeatureRoute>
}
/>
React Native Implementation
// hooks/useFeatureFlags.ts (React Native)
import { useQuery } from '@tanstack/react-query';
import { useAuth } from './useAuth';
export function useFeatureFlags() {
const { token } = useAuth();
return useQuery({
queryKey: ['featureFlags'],
queryFn: async () => {
const response = await fetch(
'https://api.fishinglog.com/api/admin/feature-flags/current-environment',
{
headers: { 'Authorization': `Bearer ${token}` }
}
);
const data = await response.json();
return data.featureFlags;
},
staleTime: 5 * 60 * 1000,
});
}
// Usage in React Native
function CirclesScreen() {
const { data: flags } = useFeatureFlags();
if (!flags?.Circles) {
return (
<View>
<Text>This feature is not available in your environment.</Text>
</View>
);
}
return <CirclesContent />;
}
Admin Panel Integration
Feature Flag Management Page
// Admin Panel - Feature Flags Management
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export function FeatureFlagsPage() {
const queryClient = useQueryClient();
const { data: flags } = useQuery({
queryKey: ['admin', 'featureFlags'],
queryFn: async () => {
const response = await fetch('/api/admin/feature-flags');
return response.json();
}
});
const updateFlag = useMutation({
mutationFn: async ({ key, updates }: { key: string; updates: any }) => {
const response = await fetch(`/api/admin/feature-flags/${key}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(updates)
});
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'featureFlags'] });
queryClient.invalidateQueries({ queryKey: ['featureFlags'] });
}
});
return (
<div>
<h1>Feature Flags</h1>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Development</th>
<th>Staging</th>
<th>Production</th>
<th>Current Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{flags?.map(flag => (
<tr key={flag.key}>
<td>{flag.name}</td>
<td>
<input
type="checkbox"
checked={flag.isEnabledInDevelopment}
onChange={(e) => updateFlag.mutate({
key: flag.key,
updates: { isEnabledInDevelopment: e.target.checked }
})}
/>
</td>
<td>
<input
type="checkbox"
checked={flag.isEnabledInStaging}
onChange={(e) => updateFlag.mutate({
key: flag.key,
updates: { isEnabledInStaging: e.target.checked }
})}
/>
</td>
<td>
<input
type="checkbox"
checked={flag.isEnabledInProduction}
onChange={(e) => updateFlag.mutate({
key: flag.key,
updates: { isEnabledInProduction: e.target.checked }
})}
/>
</td>
<td>
<Badge color={flag.isEnabledInCurrentEnvironment ? 'green' : 'red'}>
{flag.isEnabledInCurrentEnvironment ? 'Enabled' : 'Disabled'}
</Badge>
</td>
<td>
<Button onClick={() => {/* Edit */}}>Edit</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Best Practices
1. Cache Feature Flags
- Cache flags for 5 minutes (matches backend cache)
- Refetch on app start
- Invalidate cache when admin updates flags
2. Graceful Degradation
- Show fallback UI when feature is disabled
- Don't break the app if flags fail to load
- Use default values (false) if flags unavailable
3. Type Safety
// types/features.ts
export type FeatureKey =
| 'Circles'
| 'Tournaments'
| 'Charters'
| 'Events';
export interface FeatureFlags {
Circles: boolean;
Tournaments: boolean;
Charters: boolean;
Events: boolean;
}
4. Error Handling
export function useFeatureFlag(featureKey: string) {
const { data: flags, error, isLoading } = useFeatureFlags();
if (error) {
console.error('Failed to load feature flags:', error);
// Default to false for safety
return false;
}
if (isLoading) {
// Return false while loading to prevent flash of content
return false;
}
return flags?.[featureKey] ?? false;
}
5. Testing
// Mock feature flags in tests
jest.mock('@/hooks/useFeatureFlags', () => ({
useFeatureFlag: jest.fn((key: string) => {
const mockFlags: Record<string, boolean> = {
Circles: true,
Tournaments: false,
};
return mockFlags[key] ?? false;
})
}));
UI Components
Feature Badge
// Show badge when feature is in beta/testing
<FeatureGate feature="Circles">
<Badge color="blue">Beta</Badge>
</FeatureGate>
Coming Soon Message
<FeatureGate
feature="Circles"
fallback={
<div className="coming-soon">
<h3>Coming Soon</h3>
<p>This feature will be available soon!</p>
</div>
}
>
<CirclesContent />
</FeatureGate>
Common Feature Flags
Based on the backend implementation:
- Circles - Social circles feature (enabled in dev/staging, disabled in prod)
- Tournaments - Tournament management
- Charters - Charter booking system
- Events - Fishing events and expos
- Advertising - Advertiser platform
Environment Detection
The backend automatically detects the environment. Frontend can also check:
const environment = process.env.NODE_ENV === 'production'
? 'Production'
: process.env.NEXT_PUBLIC_ENV === 'staging'
? 'Staging'
: 'Development';
Resources
- Backend API:
/api/admin/feature-flags - Backend Documentation:
docs/features/feature-flags.md - Feature Flag Service: Backend caches flags for 5 minutes