diff --git a/.changeset/tired-icons-cheat.md b/.changeset/tired-icons-cheat.md
new file mode 100644
index 00000000..3e88924e
--- /dev/null
+++ b/.changeset/tired-icons-cheat.md
@@ -0,0 +1,5 @@
+---
+"@reflag/react-sdk": patch
+---
+
+Avoid loading state during bootstrapped initialization
diff --git a/packages/react-sdk/src/index.tsx b/packages/react-sdk/src/index.tsx
index a7a28a7c..661ac91b 100644
--- a/packages/react-sdk/src/index.tsx
+++ b/packages/react-sdk/src/index.tsx
@@ -271,14 +271,26 @@ export function ReflagClientProvider({
initialLoading = true,
children,
}: ReflagClientProviderProps) {
+ const hasInitialized = useRef(client.getState() === "initialized");
const [isLoading, setIsLoading] = useState(
- client.getState() !== "initialized" ? initialLoading : false,
+ hasInitialized.current ? false : initialLoading,
);
useOnEvent(
"stateUpdated",
(state) => {
- setIsLoading(state === "initializing");
+ if (state === "initialized") {
+ hasInitialized.current = true;
+ setIsLoading(false);
+ return;
+ }
+
+ if (state === "initializing") {
+ setIsLoading(hasInitialized.current || initialLoading);
+ return;
+ }
+
+ setIsLoading(false);
},
client,
);
diff --git a/packages/react-sdk/test/usage.test.tsx b/packages/react-sdk/test/usage.test.tsx
index f3f69f4a..3a7c87d9 100644
--- a/packages/react-sdk/test/usage.test.tsx
+++ b/packages/react-sdk/test/usage.test.tsx
@@ -711,6 +711,57 @@ describe("", () => {
// Removed test "does not initialize when no flags are provided"
// because ReflagBootstrappedProvider requires flags to be provided
+ test("does not enter UI loading during initial bootstrap initialization", async () => {
+ const bootstrapFlags: BootstrappedFlags = {
+ context: { user, company, other },
+ flags: {
+ abc: {
+ key: "abc",
+ isEnabled: true,
+ targetingVersion: 1,
+ },
+ },
+ };
+ let resolveStorageRead: (value: string | null) => void;
+ const storageRead = new Promise((resolve) => {
+ resolveStorageRead = resolve;
+ });
+ const storage = {
+ getItem: vi.fn(() => storageRead),
+ setItem: vi.fn(async () => undefined),
+ };
+ let client: ReflagClient | undefined;
+
+ function Content() {
+ client = useClient();
+ return (
+ {String(useIsLoading())}
+ );
+ }
+
+ const { queryByTestId, unmount } = render(
+ getBootstrapProvider(bootstrapFlags, {
+ storage,
+ loadingComponent: Loading...,
+ children: ,
+ }),
+ );
+
+ await waitFor(() => {
+ expect(client?.getState()).toBe("initializing");
+ });
+
+ expect(queryByTestId("loading")).toBeNull();
+ expect(queryByTestId("bootstrap-content")?.textContent).toBe("false");
+
+ resolveStorageRead!(null);
+ await waitFor(() => {
+ expect(client?.getState()).toBe("initialized");
+ });
+
+ unmount();
+ });
+
test("shows content after initialization", async () => {
const bootstrapFlags: BootstrappedFlags = {
context: {
@@ -1217,7 +1268,10 @@ describe("useIsLoading", () => {
const initializeSpy = vi
.spyOn(ReflagClient.prototype, "initialize")
- .mockResolvedValue(undefined);
+ .mockImplementationOnce(async function () {
+ (this as any).hooks.trigger("stateUpdated", "initializing");
+ (this as any).hooks.trigger("stateUpdated", "initialized");
+ });
let resolveSetContext: (() => void) | undefined;
const setContextPromise = new Promise((resolve) => {
resolveSetContext = resolve;