From 9bb5f4ecc8dfd670745d5b7d84bb95598b87b06a Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Fri, 3 Oct 2025 18:30:58 +0200 Subject: [PATCH] v0.0.14: Improve SSR support and fix race condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses critical issues identified in code review: 1. Server-Side Environment Handling: - Add warning when serverURL is not provided in SSR/SSG environments - Falls back to relative URLs with console warning - Prevents silent failures in server-side rendering 2. Race Condition Fix: - Use useRef for initialFlags to prevent re-creating fetchFlags on every render - Removes initialFlags from useCallback dependencies - Prevents excessive re-renders and potential infinite loops These improvements ensure better stability and reliability in both client-side and server-side environments. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- package.json | 2 +- src/hooks/client.ts | 30 ++++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index d7f4714..778c1ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-feature-flags", - "version": "0.0.13", + "version": "0.0.14", "description": "Feature flags plugin for Payload CMS - manage feature toggles, A/B tests, and gradual rollouts", "license": "MIT", "type": "module", diff --git a/src/hooks/client.ts b/src/hooks/client.ts index 7e762a3..1ea345c 100644 --- a/src/hooks/client.ts +++ b/src/hooks/client.ts @@ -1,5 +1,5 @@ 'use client' -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useState, useRef } from 'react' export interface FeatureFlag { name: string @@ -21,9 +21,19 @@ export interface FeatureFlagOptions { // Helper to get config from options or defaults function getConfig(options?: FeatureFlagOptions) { + // In server-side environments, serverURL must be explicitly provided const serverURL = options?.serverURL || - (typeof window !== 'undefined' ? window.location.origin : '') || - '' + (typeof window !== 'undefined' ? window.location.origin : undefined) + + if (!serverURL) { + console.warn( + 'FeatureFlags: serverURL must be provided when using hooks in server-side environment. ' + + 'Falling back to relative URL which may not work correctly.' + ) + // Use relative URL as fallback - will work if API is on same domain + return { serverURL: '', apiPath: options?.apiPath || '/api', collectionSlug: options?.collectionSlug || 'feature-flags' } + } + const apiPath = options?.apiPath || '/api' const collectionSlug = options?.collectionSlug || 'feature-flags' @@ -47,13 +57,21 @@ export function useFeatureFlags( const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + // Use ref to store initialFlags to avoid re-creating fetchFlags on every render + const initialFlagsRef = useRef(initialFlags) + + // Update ref when initialFlags changes (but won't trigger re-fetch) + useEffect(() => { + initialFlagsRef.current = initialFlags + }, [initialFlags]) + const fetchFlags = useCallback(async () => { try { setLoading(true) setError(null) // Use Payload's native collection API - const names = initialFlags.map(f => f.name).filter(Boolean) + const names = initialFlagsRef.current.map(f => f.name).filter(Boolean) const query = names.length > 0 ? `?where[name][in]=${names.join(',')}&limit=1000` : '?limit=1000' @@ -81,7 +99,7 @@ export function useFeatureFlags( } // Sort flags based on the order of names in initialFlags - const sortedFlags = initialFlags.map(initialFlag => { + const sortedFlags = initialFlagsRef.current.map(initialFlag => { const fetchedFlag = fetchedFlagsMap.get(initialFlag.name!) // Use fetched flag if available, otherwise keep the initial flag return fetchedFlag || initialFlag @@ -94,7 +112,7 @@ export function useFeatureFlags( } finally { setLoading(false) } - }, [serverURL, apiPath, collectionSlug, initialFlags]) + }, [serverURL, apiPath, collectionSlug]) // Remove initialFlags from dependencies useEffect(() => { void fetchFlags()