Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 45 additions & 30 deletions packages/react-native/src/components/formbricks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,54 @@ import { SurveyStore } from "@/lib/survey/store";
import React, { useCallback, useEffect, useSyncExternalStore } from "react";

interface FormbricksProps {
appUrl: string;
environmentId: string;
appUrl: string;
environmentId: string;
onSetup?: () => void;
}

const surveyStore = SurveyStore.getInstance();
const logger = Logger.getInstance();

export function Formbricks({ appUrl, environmentId }: FormbricksProps): React.JSX.Element | null {
// initializes sdk
useEffect(() => {
const setupFormbricks = async (): Promise<void> => {
try {
await setup({
environmentId,
appUrl,
});
} catch {
logger.debug("Initialization failed");
}
};

setupFormbricks().catch(() => {
logger.debug("Initialization error");
});
}, [environmentId, appUrl]);

const subscribe = useCallback((callback: () => void) => {
const unsubscribe = surveyStore.subscribe(callback);
return unsubscribe;
}, []);

const getSnapshot = useCallback(() => surveyStore.getSurvey(), []);
const survey = useSyncExternalStore(subscribe, getSnapshot);

return survey ? <SurveyWebView survey={survey} /> : null;
export function Formbricks({
appUrl,
environmentId,
onSetup,
}: FormbricksProps): React.JSX.Element | null {
// initializes sdk
useEffect(() => {
const setupFormbricks = async (): Promise<void> => {
Comment on lines +21 to +23
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Keep the latest onSetup without retriggering setup; also prevent duplicate calls per env/app pair.

React 18 StrictMode can double-invoke effects in dev; setup() may also return ok when already initialized. Use refs to (a) always call the latest onSetup and (b) ensure it fires once per (environmentId, appUrl).

 export function Formbricks({
   appUrl,
   environmentId,
   onSetup,
 }: FormbricksProps): React.JSX.Element | null {
-  // initializes sdk
+  // use latest onSetup and guard against duplicate calls per (envId|appUrl)
+  const onSetupRef = React.useRef(onSetup);
+  useEffect(() => {
+    onSetupRef.current = onSetup;
+  }, [onSetup]);
+  const onSetupKeyRef = React.useRef<string | null>(null);
+
+  // initializes sdk
   useEffect(() => {
+    const key = `${environmentId}|${appUrl}`;
     const setupFormbricks = async (): Promise<void> => {
       try {
         const result = await setup({
           environmentId,
           appUrl,
         });
 
-        if (result.ok) {
-          onSetup?.();
+        if (result.ok && onSetupKeyRef.current !== key) {
+          onSetupKeyRef.current = key;
+          onSetupRef.current?.();
         } else {
           logger.error(`Initialization failed: ${String(result.error)}`);
         }
       } catch (err) {
         logger.error(

Also applies to: 30-31

🤖 Prompt for AI Agents
In packages/react-native/src/components/formbricks.tsx around lines 21-23 (also
applies to 30-31), the effect that calls setup() should use refs to avoid stale
callbacks and to prevent duplicate initializations per environmentId/appUrl:
store the latest onSetup in a useRef and update it on each render so the effect
always invokes the newest callback; maintain a ref (or Map) keyed by
`${environmentId}:${appUrl}` to track which pairs have already been initialized
and skip calling setup if the key exists; ensure the effect uses minimal
dependencies (environmentId and appUrl) to run when those change and call the
setup routine only when the tracked key is not present, marking the key as
initialized immediately before/after successful setup to prevent double calls
under React StrictMode.

try {
const result = await setup({
environmentId,
appUrl,
});

if (result.ok) {
onSetup?.();
} else {
logger.error(`Initialization failed: ${String(result.error)}`);
}
Comment on lines +30 to +34
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Prefer robust error message extraction to avoid "object Object".

Log the message when present; fall back to stringifying the error.

-        } else {
-          logger.error(`Initialization failed: ${String(result.error)}`);
-        }
+        } else {
+          const msg =
+            (result as any)?.error?.message ?? JSON.stringify(result.error);
+          logger.error(`Initialization failed: ${msg}`);
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (result.ok) {
onSetup?.();
} else {
logger.error(`Initialization failed: ${String(result.error)}`);
}
if (result.ok) {
onSetup?.();
} else {
const msg =
(result as any)?.error?.message ?? JSON.stringify(result.error);
logger.error(`Initialization failed: ${msg}`);
}
🤖 Prompt for AI Agents
In packages/react-native/src/components/formbricks.tsx around lines 30 to 34,
the error logging may produce "[object Object]" because it blindly stringifies
the error; update the logger call to extract a clear message by using
result.error?.message when available, otherwise use the error if it's a string,
and as a last resort JSON.stringify(result.error) or result.error.toString();
replace the existing logger.error call with this prioritized extraction so logs
show useful error text.

} catch (err) {
logger.error(
`Initialization threw: ${
err instanceof Error ? err?.message : String(err)
}`
);
}
};

setupFormbricks().catch(() => {
logger.debug("Initialization error");
});
}, [environmentId, appUrl]);

const subscribe = useCallback((callback: () => void) => {
const unsubscribe = surveyStore.subscribe(callback);
return unsubscribe;
}, []);

const getSnapshot = useCallback(() => surveyStore.getSurvey(), []);
const survey = useSyncExternalStore(subscribe, getSnapshot);

return survey ? <SurveyWebView survey={survey} /> : null;
}