diff --git a/packages/realm-react/CHANGELOG.md b/packages/realm-react/CHANGELOG.md
index f0e663638e..a30c3bf55c 100644
--- a/packages/realm-react/CHANGELOG.md
+++ b/packages/realm-react/CHANGELOG.md
@@ -4,13 +4,29 @@
* None
### Enhancements
-* None
+* Added the ability to get `progress` information in `fallback` component of `RealmProvider` when opening a synced Realm. ([#6785](https://github.com/realm/realm-js/issues/6785))
+```tsx
+import { RealmProvider, RealmProviderFallback } from "@realm/react";
+
+const Fallback: RealmProviderFallback = ({ progress }) => {
+ return Loading:{(100 * progress).toFixed()}%;
+}
+
+const MyApp() = () => {
+ return (
+
+ ...
+
+ );
+}
+```
### Fixed
* ([#????](https://github.com/realm/realm-js/issues/????), since v?.?.?)
* None
### Compatibility
+* Realm JavaScript >= v12.12.0
* React Native >= v0.71.4
* Realm Studio v15.0.0.
* File format: generates Realms with format v24 (reads and upgrades file format v10).
diff --git a/packages/realm-react/package.json b/packages/realm-react/package.json
index 5f6f2eba49..305824956c 100644
--- a/packages/realm-react/package.json
+++ b/packages/realm-react/package.json
@@ -70,7 +70,7 @@
},
"peerDependencies": {
"react": ">=17.0.2",
- "realm": "^12.0.0-browser || ^12.0.0 || ^12.0.0-rc || ^11.0.0"
+ "realm": ">=12.12.0"
},
"optionalDependencies": {
"@babel/runtime": ">=7",
diff --git a/packages/realm-react/src/RealmProvider.tsx b/packages/realm-react/src/RealmProvider.tsx
index f70e6d6198..29162c1be7 100644
--- a/packages/realm-react/src/RealmProvider.tsx
+++ b/packages/realm-react/src/RealmProvider.tsx
@@ -27,6 +27,10 @@ type PartialRealmConfiguration = Omit, "sync"> & {
sync?: Partial;
};
+export type RealmProviderFallback = React.ComponentType<{
+ progress: number;
+}>;
+
/** Props used for a configuration-based Realm provider */
type RealmProviderConfigurationProps = {
/**
@@ -42,7 +46,7 @@ type RealmProviderConfigurationProps = {
/**
* The fallback component to render if the Realm is not open.
*/
- fallback?: React.ComponentType | React.ReactElement | null | undefined;
+ fallback?: RealmProviderFallback | React.ComponentType | React.ReactElement | null | undefined;
children: React.ReactNode;
} & PartialRealmConfiguration;
@@ -158,6 +162,8 @@ export function createRealmProviderFromConfig(
}
}, [realm]);
+ const [progress, setProgress] = useState(0);
+
useEffect(() => {
const realmRef = currentRealm.current;
// Check if we currently have an open Realm. If we do not (i.e. it is the first
@@ -165,7 +171,9 @@ export function createRealmProviderFromConfig(
// need to open a new Realm.
const shouldInitRealm = realmRef === null;
const initRealm = async () => {
- const openRealm = await Realm.open(configuration.current);
+ const openRealm = await Realm.open(configuration.current).progress((estimate: number) => {
+ setProgress(estimate);
+ });
setRealm(openRealm);
};
if (shouldInitRealm) {
@@ -184,7 +192,7 @@ export function createRealmProviderFromConfig(
if (!realm) {
if (typeof Fallback === "function") {
- return ;
+ return ;
}
return <>{Fallback}>;
}
diff --git a/packages/realm-react/src/__tests__/RealmProvider.test.tsx b/packages/realm-react/src/__tests__/RealmProvider.test.tsx
index f43112ab58..d6cc4d94f9 100644
--- a/packages/realm-react/src/__tests__/RealmProvider.test.tsx
+++ b/packages/realm-react/src/__tests__/RealmProvider.test.tsx
@@ -22,9 +22,15 @@ import { Button, Text, View } from "react-native";
import { act, fireEvent, render, renderHook, waitFor } from "@testing-library/react-native";
import { RealmProvider, createRealmContext } from "..";
-import { RealmProviderFromRealm, areConfigurationsIdentical, mergeRealmConfiguration } from "../RealmProvider";
+import {
+ RealmProviderFallback,
+ RealmProviderFromRealm,
+ areConfigurationsIdentical,
+ mergeRealmConfiguration,
+} from "../RealmProvider";
import { randomRealmPath } from "./helpers";
import { RealmContext } from "../RealmContext";
+import { MockedProgressRealmPromiseWithDelay, mockRealmOpen } from "./mocks";
const dogSchema: Realm.ObjectSchema = {
name: "dog",
@@ -231,11 +237,16 @@ describe("RealmProvider", () => {
// TODO: Now that local realm is immediately set, the fallback never renders.
// We need to test synced realm in order to produce the fallback
- describe.skip("initially renders a fallback, until realm exists", () => {
+ describe("initially renders a fallback, until realm exists", () => {
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
it("as a component", async () => {
+ const slowRealmOpen = mockRealmOpen();
const App = () => {
return (
- }>
+ }>
);
@@ -245,17 +256,19 @@ describe("RealmProvider", () => {
expect(queryByTestId("fallbackContainer")).not.toBeNull();
expect(queryByTestId("testContainer")).toBeNull();
- await waitFor(() => queryByTestId("testContainer"));
+ await act(async () => await slowRealmOpen);
expect(queryByTestId("fallbackContainer")).toBeNull();
expect(queryByTestId("testContainer")).not.toBeNull();
});
it("as an element", async () => {
+ const slowRealmOpen = mockRealmOpen();
+
const Fallback = ;
const App = () => {
return (
-
+
);
@@ -265,11 +278,43 @@ describe("RealmProvider", () => {
expect(queryByTestId("fallbackContainer")).not.toBeNull();
expect(queryByTestId("testContainer")).toBeNull();
- await waitFor(() => queryByTestId("testContainer"));
+ await act(async () => await slowRealmOpen);
expect(queryByTestId("fallbackContainer")).toBeNull();
expect(queryByTestId("testContainer")).not.toBeNull();
});
+
+ it("should receive progress information", async () => {
+ const expectedProgressValues = [0, 0.25, 0.5, 0.75, 1];
+ const slowRealmOpen = mockRealmOpen(
+ new MockedProgressRealmPromiseWithDelay({ progressValues: expectedProgressValues }),
+ );
+ const renderedProgressValues: number[] = [];
+
+ const Fallback: RealmProviderFallback = ({ progress }) => {
+ renderedProgressValues.push(progress);
+ return {progress};
+ };
+ const App = () => {
+ return (
+
+
+
+ );
+ };
+ const { queryByTestId } = render();
+
+ expect(queryByTestId("fallbackContainer")).not.toBeNull();
+ expect(queryByTestId("testContainer")).toBeNull();
+ expect(renderedProgressValues).toStrictEqual([expectedProgressValues[0]]);
+
+ await act(async () => await slowRealmOpen);
+
+ expect(queryByTestId("fallbackContainer")).toBeNull();
+ expect(queryByTestId("testContainer")).not.toBeNull();
+
+ expect(renderedProgressValues).toStrictEqual(expectedProgressValues);
+ });
});
});
diff --git a/packages/realm-react/src/__tests__/mocks.ts b/packages/realm-react/src/__tests__/mocks.ts
new file mode 100644
index 0000000000..ad15cb17e0
--- /dev/null
+++ b/packages/realm-react/src/__tests__/mocks.ts
@@ -0,0 +1,144 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2024 Realm Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////
+
+import { act } from "@testing-library/react-native";
+import { EstimateProgressNotificationCallback, ProgressRealmPromise, Realm } from "realm";
+import { sleep } from "../helpers";
+
+/**
+ * Mocks {@link Realm.ProgressRealmPromise} with a custom
+ * promise completion and progress handler.
+ */
+export class MockedProgressRealmPromise extends Promise implements ProgressRealmPromise {
+ private progressHandler?: (callback: EstimateProgressNotificationCallback) => void;
+ private cancelHandler?: () => void;
+ private realmPromise!: Promise;
+
+ constructor(
+ getRealm: () => Promise,
+ options?: {
+ progress?: (callback: EstimateProgressNotificationCallback) => void;
+ cancel?: () => void;
+ },
+ ) {
+ let realmPromise: Promise;
+ super((resolve) => {
+ realmPromise = getRealm();
+ realmPromise.then((realm) => resolve(realm));
+ });
+ // @ts-expect-error realmPromise value will be assigned right away
+ this.realmPromise = realmPromise;
+ this.progressHandler = options?.progress;
+ this.cancelHandler = options?.cancel;
+ }
+
+ get [Symbol.toStringTag]() {
+ return "MockedProgressRealmPromise";
+ }
+
+ cancel = () => {
+ if (!this.cancelHandler) {
+ throw new Error("cancel handler not set");
+ }
+ this.cancelHandler();
+ };
+
+ then(
+ onfulfilled?: ((value: Realm) => TResult1 | PromiseLike) | null | undefined,
+ onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null | undefined,
+ ): Promise {
+ return this.realmPromise.then(onfulfilled, onrejected);
+ }
+
+ progress = (callback: EstimateProgressNotificationCallback) => {
+ if (!this.progressHandler) {
+ throw new Error("progress handler not set");
+ }
+ this.progressHandler(callback);
+ return this;
+ };
+}
+
+/**
+ * Mocked {@link ProgressRealmPromise} which resolves after a set delay.
+ * If `options.progressValues` is specified, passes it through an
+ * equal interval to `Realm.open(...).progress(...)` callback.
+ */
+export class MockedProgressRealmPromiseWithDelay extends MockedProgressRealmPromise {
+ public currentProgressIndex = 0;
+ public progressValues: number[] | undefined;
+ private progressTimeout: NodeJS.Timeout | undefined;
+
+ constructor(
+ options: {
+ delay?: number;
+ /** Progress values which the `Realm.open(...).progress(...)` will receive in an equal interval. */
+ progressValues?: number[];
+ } = {},
+ ) {
+ const { progressValues, delay = 100 } = options;
+ super(
+ async () => {
+ await sleep(delay);
+ return new Realm();
+ },
+ {
+ progress: (callback) => {
+ this.progressTimeout = callMockedProgressNotifications(callback, delay, progressValues);
+ },
+ cancel: () => clearTimeout(this.progressTimeout),
+ },
+ );
+ this.progressValues = progressValues;
+ }
+}
+
+/** Calls given callbacks with progressValues in an equal interval */
+export function callMockedProgressNotifications(
+ callback: EstimateProgressNotificationCallback,
+ timeFrame: number,
+ progressValues: number[] = [0, 0.25, 0.5, 0.75, 1],
+): NodeJS.Timeout {
+ let progressIndex = 0;
+ let progressInterval: NodeJS.Timeout | undefined = undefined;
+ const sendProgress = () => {
+ // Uses act as this causes a component state update.
+ act(() => callback(progressValues[progressIndex]));
+ progressIndex++;
+
+ if (progressIndex >= progressValues.length) {
+ // Send the next progress update in equidistant time
+ clearInterval(progressInterval);
+ }
+ };
+ progressInterval = setInterval(sendProgress, timeFrame / (progressValues.length + 1));
+ sendProgress();
+ return progressInterval;
+}
+
+/**
+ * Mocks the Realm.open operation with a delayed, predictable Realm creation.
+ * @returns Promise which resolves when the Realm is opened.
+ */
+export function mockRealmOpen(
+ progressRealmPromise: MockedProgressRealmPromise = new MockedProgressRealmPromiseWithDelay(),
+): MockedProgressRealmPromise {
+ const delayedRealmOpen = jest.spyOn(Realm, "open");
+ delayedRealmOpen.mockImplementation(() => progressRealmPromise);
+ return progressRealmPromise;
+}
diff --git a/packages/realm-react/src/helpers.ts b/packages/realm-react/src/helpers.ts
index ed6c2d8d24..8158443349 100644
--- a/packages/realm-react/src/helpers.ts
+++ b/packages/realm-react/src/helpers.ts
@@ -58,3 +58,12 @@ export type RestrictivePick = Pick & { [RestrictedKe
export function isClassModelConstructor(value: unknown): value is RealmClassType {
return Object.getPrototypeOf(value) === Realm.Object;
}
+
+/**
+ * Adapted from integration-tests
+ * @param ms For how long should the promise be pending?
+ * @returns A promise that returns after `ms` milliseconds.
+ */
+export function sleep(ms = 1000): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}