diff --git a/packages/realm-react/CHANGELOG.md b/packages/realm-react/CHANGELOG.md
index 2725b2d5be..1dc59369ab 100644
--- a/packages/realm-react/CHANGELOG.md
+++ b/packages/realm-react/CHANGELOG.md
@@ -4,7 +4,39 @@
* None
### Enhancements
-* None
+* Added the ability to use an existing Realm instance in `RealmProvider` and `createRealmContext`. ([#6714](https://github.com/realm/realm-js/pull/6714))
+```jsx
+// Using RealmProvider
+import { RealmProvider } from "@realm/react";
+
+const realm = new Realm(...);
+
+function MyApp() {
+ return (
+
+ ...
+
+ );
+}
+
+// Using createRealmContext
+import { createRealmContext } from "@realm/react";
+
+const realm = new Realm(...);
+const { RealmProvider, useRealm } = createRealmContext(realm);
+
+function MyApp() {
+ return (
+ <>
+
+ ...
+
+
+ {/* Note: The hooks returned from `createRealmContext` using an existing Realm can be used outside of the scope of the provider! */}
+
+ >
+ );
+```
### Fixed
* ([#????](https://github.com/realm/realm-js/issues/????), since v?.?.?)
diff --git a/packages/realm-react/src/RealmContext.ts b/packages/realm-react/src/RealmContext.ts
new file mode 100644
index 0000000000..84c36b1140
--- /dev/null
+++ b/packages/realm-react/src/RealmContext.ts
@@ -0,0 +1,196 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// 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 { createUseObject } from "./useObject";
+import { createUseQuery } from "./useQuery";
+import { createUseRealm } from "./useRealm";
+import {
+ DynamicRealmProvider,
+ RealmProviderFromConfiguration,
+ RealmProviderFromRealm,
+ createRealmProvider,
+} from "./RealmProvider";
+import { createContext } from "react";
+import Realm from "realm";
+
+export type RealmContext = {
+ /**
+ * The Provider component that is required to wrap any component using
+ * the Realm hooks.
+ * @example
+ * ```
+ * const AppRoot = () => {
+ * const syncConfig = {
+ * flexible: true,
+ * user: currentUser
+ * };
+ *
+ * return (
+ *
+ *
+ *
+ * )
+ * }
+ * ```
+ * @param props - The {@link Realm.Configuration} or {@link Realm} of the provider
+ * are set based on the options passed to `createRealmProvider`. When using a
+ * {@link Realm.Configuration}, individual config keys can be overridden when
+ * creating a `` by passing them as props. For example, to override
+ * the `path` config value, use a prop named `path` e.g., `path="newPath.realm"` an
+ * attribute of the same key.
+ */
+ RealmProvider: RealmProvider;
+ /**
+ * Returns the instance of the {@link Realm} opened by the `RealmProvider`.
+ * @example
+ * ```
+ * const realm = useRealm();
+ * ```
+ * @returns a realm instance
+ */
+ useRealm: ReturnType;
+
+ /**
+ * Returns a {@link Realm.Collection} of {@link Realm.Object}s from a given type.
+ * The hook will update on any changes to any object in the collection.
+ *
+ * The result of this can be consumed directly by the `data` argument of any React Native
+ * VirtualizedList or FlatList. If the component used for the list's `renderItem` prop is {@link React.Memo}ized,
+ * then only the modified object will re-render.
+ * @example
+ * ```tsx
+ * // Return all collection items
+ * const collection = useQuery({ type: Object });
+ *
+ * // Return all collection items sorted by name and filtered by category
+ * const filteredAndSorted = useQuery({
+ * type: Object,
+ * query: (collection) => collection.filtered('category == $0',category).sorted('name'),
+ * }, [category]);
+ *
+ * // Return all collection items sorted by name and filtered by category, triggering re-renders only if "name" changes
+ * const filteredAndSorted = useQuery({
+ * type: Object,
+ * query: (collection) => collection.filtered('category == $0',category).sorted('name'),
+ * keyPaths: ["name"]
+ * }, [category]);
+ * ```
+ * @param options
+ * @param options.type - The object type, depicted by a string or a class extending Realm.Object
+ * @param options.query - A function that takes a {@link Realm.Collection} and returns a {@link Realm.Collection} of the same type. This allows for filtering and sorting of the collection, before it is returned.
+ * @param options.keyPaths - Indicates a lower bound on the changes relevant for the hook. This is a lower bound, since if multiple hooks add listeners (each with their own `keyPaths`) the union of these key-paths will determine the changes that are considered relevant for all listeners registered on the collection. In other words: A listener might fire and cause a re-render more than the key-paths specify, if other listeners with different key-paths are present.
+ * @param deps - An array of dependencies that will be passed to {@link React.useMemo}
+ * @returns a collection of realm objects or an empty array
+ */
+ useQuery: ReturnType;
+ /**
+ * Returns a {@link Realm.Object} from a given type and value of primary key.
+ * The hook will update on any changes to the properties on the returned object
+ * and return null if it either doesn't exists or has been deleted.
+ * @example
+ * ```
+ * const object = useObject(ObjectClass, objectId);
+ * ```
+ * @param type - The object type, depicted by a string or a class extending {@link Realm.Object}
+ * @param primaryKey - The primary key of the desired object which will be retrieved using {@link Realm.objectForPrimaryKey}
+ * @param keyPaths - Indicates a lower bound on the changes relevant for the hook. This is a lower bound, since if multiple hooks add listeners (each with their own `keyPaths`) the union of these key-paths will determine the changes that are considered relevant for all listeners registered on the object. In other words: A listener might fire and cause a re-render more than the key-paths specify, if other listeners with different key-paths are present.
+ * @returns either the desired {@link Realm.Object} or `null` in the case of it being deleted or not existing.
+ */
+ useObject: ReturnType;
+};
+
+/**
+ * Creates Realm React hooks and Provider component for a given Realm configuration
+ * @example
+ * ```
+ *class Task extends Realm.Object {
+ * ...
+ *
+ * static schema: ObjectSchema = {
+ * name: 'Task',
+ * primaryKey: '_id',
+ * properties: {
+ * ...
+ * },
+ * };
+ *}
+ *
+ *const {useRealm, useQuery, useObject, RealmProvider} = createRealmContext({schema: [Task]});
+ * ```
+ * @param realmConfig - {@link Realm.Configuration} used to open the Realm
+ * @returns An object containing a `RealmProvider` component, and `useRealm`, `useQuery` and `useObject` hooks
+ */
+export function createRealmContext(realmConfig: Realm.Configuration): RealmContext;
+/**
+ * Creates Realm React hooks and Provider component for a given Realm instance.
+ *
+ * **Note:** the hooks returned from `createRealmContext` using an existing Realm can be used outside of the scope of the provider.
+ * @example
+ * ```
+ * const realm = new Realm({ schema: [...] });
+ * const {useRealm, useQuery, useObject, RealmProvider} = createRealmContext(realm);
+ * ```
+ * @param realm - {@link Realm} instance
+ * @returns An object containing a `RealmProvider` component, and `useRealm`, `useQuery` and `useObject` hooks
+ */
+export function createRealmContext(realm: Realm): RealmContext;
+/**
+ * Creates Realm React hooks and Provider component.
+ * @example
+ * ```
+ * class Task extends Realm.Object {
+ * ...
+ * static schema: ObjectSchema = {
+ * name: 'Task',
+ * primaryKey: '_id',
+ * properties: {
+ * ...
+ * },
+ * };
+ * }
+ * const {useRealm, useQuery, useObject, RealmProvider} = createRealmContext();
+ * ...
+ *
+ * ```
+ * @example
+ * ```
+ * const realm = await Realm.open({ path: "example.realm", schema: [Task] });
+ * const {RealmProvider} = createRealmContext();
+ * ...
+ *
+ * ```
+ * @returns An object containing a `RealmProvider` component, and `useRealm`, `useQuery` and `useObject` hooks
+ */
+export function createRealmContext(): RealmContext;
+export function createRealmContext(
+ realmOrConfig?: Realm | Realm.Configuration,
+): RealmContext {
+ const RealmContext = createContext(realmOrConfig instanceof Realm ? realmOrConfig : null);
+ const RealmProvider = createRealmProvider(realmOrConfig, RealmContext);
+
+ const useRealm = createUseRealm(RealmContext);
+ const useQuery = createUseQuery(useRealm);
+ const useObject = createUseObject(useRealm);
+
+ return {
+ RealmProvider,
+ useRealm,
+ useQuery,
+ useObject,
+ };
+}
diff --git a/packages/realm-react/src/RealmProvider.tsx b/packages/realm-react/src/RealmProvider.tsx
index e789027afe..3f1d88f185 100644
--- a/packages/realm-react/src/RealmProvider.tsx
+++ b/packages/realm-react/src/RealmProvider.tsx
@@ -21,16 +21,14 @@ import Realm from "realm";
import isEqual from "lodash.isequal";
import { UserContext } from "./UserProvider";
+import { RestrictivePick } from "./helpers";
type PartialRealmConfiguration = Omit, "sync"> & {
sync?: Partial;
};
-type ProviderProps = PartialRealmConfiguration & {
- /**
- * The fallback component to render if the Realm is not opened.
- */
- fallback?: React.ComponentType | React.ReactElement | null | undefined;
+/** Props used for a configuration-based Realm provider */
+type RealmProviderConfigurationProps = {
/**
* If false, Realm will not be closed when the component unmounts.
* @default true
@@ -41,43 +39,77 @@ type ProviderProps = PartialRealmConfiguration & {
* instance outside of a component that uses the Realm hooks.
*/
realmRef?: React.MutableRefObject;
+ /**
+ * The fallback component to render if the Realm is not open.
+ */
+ fallback?: React.ComponentType | React.ReactElement | null | undefined;
+ children: React.ReactNode;
+} & PartialRealmConfiguration;
+
+/** Props used for a Realm instance-based Realm provider */
+type RealmProviderRealmProps = {
+ /**
+ * The Realm instance to be used by the provider.
+ */
+ realm: Realm;
children: React.ReactNode;
};
+type RealmProviderProps = RealmProviderConfigurationProps & RealmProviderRealmProps;
+
+/**
+ * Represents the provider returned from `createRealmContext` with a Realm instance i.e. `createRealmContext(new Realm(...))`.
+ * Omits "realm" as it gets set at creation and cannot be changed.
+
+ * **Note:** the hooks returned from `createRealmContext` using an existing Realm can be used outside of the scope of the provider.
+ */
+export type RealmProviderFromRealm = React.FC>;
+
+/**
+ * Represents the provider returned from `createRealmContext` with a configuration, i.e. `createRealmContext({schema: [...]})`.
+ */
+export type RealmProviderFromConfiguration = React.FC;
+
+/**
+ * Represents properties of a {@link DynamicRealmProvider} where Realm instance props are set and Configuration props are disallowed.
+ */
+export type DynamicRealmProviderWithRealmProps = RestrictivePick;
+
+/**
+ * Represents properties of a {@link DynamicRealmProvider} where Realm configuration props are set and Realm instance props are disallowed.
+ */
+export type DynamicsRealmProviderWithConfigurationProps = RestrictivePick<
+ RealmProviderProps,
+ keyof RealmProviderConfigurationProps
+>;
+
+/**
+ * Represents the provider returned from creating context with no arguments (including the default context).
+ * Supports either {@link RealmProviderRealmProps} or {@link RealmProviderConfigurationProps}.
+ */
+export type DynamicRealmProvider = React.FC<
+ DynamicRealmProviderWithRealmProps | DynamicsRealmProviderWithConfigurationProps
+>;
+
+export function createRealmProviderFromRealm(
+ realm: Realm,
+ RealmContext: React.Context,
+): RealmProviderFromRealm {
+ return ({ children }) => {
+ return ;
+ };
+}
+
/**
* Generates a `RealmProvider` given a {@link Realm.Configuration} and {@link React.Context}.
* @param realmConfig - The configuration of the Realm to be instantiated
* @param RealmContext - The context that will contain the Realm instance
* @returns a RealmProvider component that provides context to all context hooks
*/
-export function createRealmProvider(
+export function createRealmProviderFromConfig(
realmConfig: Realm.Configuration,
RealmContext: React.Context,
-): React.FC {
- /**
- * Returns a Context Provider component that is required to wrap any component using
- * the Realm hooks.
- * @example
- * ```
- * const AppRoot = () => {
- * const syncConfig = {
- * flexible: true,
- * user: currentUser
- * };
- *
- * return (
- *
- *
- *
- * )
- * }
- * ```
- * @param props - The {@link Realm.Configuration} for this Realm defaults to
- * the config passed to `createRealmProvider`, but individual config keys can
- * be overridden when creating a `` by passing them as props.
- * For example, to override the `path` config value, use a prop named `path`,
- * e.g. `path="newPath.realm"`
- */
+): RealmProviderFromConfiguration {
return ({ children, fallback: Fallback, closeOnUnmount = true, realmRef, ...restProps }) => {
const [realm, setRealm] = useState(() =>
realmConfig.sync === undefined && restProps.sync === undefined
@@ -161,6 +193,47 @@ export function createRealmProvider(
};
}
+/**
+ * Generates a `RealmProvider` which is either based on a configuration
+ * or based on a realm, depending on its props.
+ * @param RealmContext - The context that will contain the Realm instance
+ * @returns a RealmProvider component that provides context to all context hooks
+ */
+export function createDynamicRealmProvider(RealmContext: React.Context): DynamicRealmProvider {
+ return ({ realm, children, ...configurationProps }) => {
+ if (realm) {
+ if (Object.keys(configurationProps).length > 0) {
+ throw new Error("Cannot use configuration props when using an existing Realm instance.");
+ }
+
+ const RealmProvider = createRealmProviderFromRealm(realm, RealmContext);
+ return {children};
+ } else {
+ const RealmProvider = createRealmProviderFromConfig({}, RealmContext);
+ return {children};
+ }
+ };
+}
+
+/**
+ * Generates the appropriate `RealmProvider` based on whether there is a config, realm, or neither given.
+ * @param realmOrConfig - A Realm instance, a configuration, or undefined (including default provider).
+ * @param RealmContext - The context that will contain the Realm instance
+ * @returns a RealmProvider component that provides context to all context hooks
+ */
+export function createRealmProvider(
+ realmOrConfig: Realm.Configuration | Realm | undefined,
+ RealmContext: React.Context,
+): RealmProviderFromConfiguration | RealmProviderFromRealm | DynamicRealmProvider {
+ if (!realmOrConfig) {
+ return createDynamicRealmProvider(RealmContext);
+ } else if (realmOrConfig instanceof Realm) {
+ return createRealmProviderFromRealm(realmOrConfig, RealmContext);
+ } else {
+ return createRealmProviderFromConfig(realmOrConfig, RealmContext);
+ }
+}
+
/**
* Merge two configurations, creating a configuration using `configA` as the default,
* merged with `configB`, with properties in `configB` overriding `configA`.
diff --git a/packages/realm-react/src/__tests__/RealmProvider.test.tsx b/packages/realm-react/src/__tests__/RealmProvider.test.tsx
index 63709d2d7c..f43112ab58 100644
--- a/packages/realm-react/src/__tests__/RealmProvider.test.tsx
+++ b/packages/realm-react/src/__tests__/RealmProvider.test.tsx
@@ -21,9 +21,10 @@ import Realm, { User } from "realm";
import { Button, Text, View } from "react-native";
import { act, fireEvent, render, renderHook, waitFor } from "@testing-library/react-native";
-import { createRealmContext } from "..";
-import { areConfigurationsIdentical, mergeRealmConfiguration } from "../RealmProvider";
+import { RealmProvider, createRealmContext } from "..";
+import { RealmProviderFromRealm, areConfigurationsIdentical, mergeRealmConfiguration } from "../RealmProvider";
import { randomRealmPath } from "./helpers";
+import { RealmContext } from "../RealmContext";
const dogSchema: Realm.ObjectSchema = {
name: "dog",
@@ -43,317 +44,578 @@ const catSchema: Realm.ObjectSchema = {
},
};
-const { RealmProvider, useRealm } = createRealmContext({
+const withConfigRealmContext = createRealmContext({
schema: [dogSchema],
inMemory: true,
path: randomRealmPath(),
});
-const EmptyRealmContext = createRealmContext();
-
describe("RealmProvider", () => {
afterEach(() => {
Realm.clearTestState();
});
- it("returns the configured realm with useRealm and closes on unmount", async () => {
- const wrapper = ({ children }: { children: React.ReactNode }) => {children};
- const { result, unmount } = renderHook(() => useRealm(), { wrapper });
- await waitFor(() => expect(result.current).not.toBe(null));
- const realm = result.current;
- expect(realm).not.toBe(null);
- expect(realm.schema[0].name).toBe("dog");
- unmount();
- expect(realm.isClosed).toBe(true);
- });
+ describe("with a Realm Configuration", () => {
+ const { RealmProvider, useRealm } = withConfigRealmContext;
- it("returns the configured realm with useRealm and stays open if flagged", async () => {
- const wrapper = ({ children }: { children: React.ReactNode }) => (
- {children}
- );
- const { result, unmount } = renderHook(() => useRealm(), { wrapper });
- await waitFor(() => expect(result.current).not.toBe(null));
- const realm = result.current;
- expect(realm.schema[0].name).toBe("dog");
- unmount();
- expect(realm.isClosed).toBe(false);
- });
+ it("returns the configured realm with useRealm", async () => {
+ const wrapper = ({ children }: { children: React.ReactNode }) => {children};
+ const { result } = renderHook(() => useRealm(), { wrapper });
+ await waitFor(() => expect(result.current).not.toBe(null));
+ const realm = result.current;
+ expect(realm).toBeInstanceOf(Realm);
+ expect(realm.schema[0].name).toBe("dog");
+ });
- it("will override the the configuration provided in createRealmContext", async () => {
- const wrapper = ({ children }: { children: React.ReactNode }) => (
- {children}
- );
- const { result } = renderHook(() => useRealm(), { wrapper });
- await waitFor(() => expect(result.current).not.toBe(null));
- const realm = result.current;
- expect(realm).not.toBe(null);
- expect(realm.schema[0].name).toBe("cat");
- });
- it("can be used with an initially empty realm context", async () => {
- const wrapper = ({ children }: { children: React.ReactNode }) => (
- {children}
- );
- const { result } = renderHook(() => EmptyRealmContext.useRealm(), { wrapper });
- await waitFor(() => expect(result.current).not.toBe(null));
- const realm = result.current;
- expect(realm).not.toBe(null);
- expect(realm.schema[0].name).toBe("cat");
- });
+ it("closes realm on unmount by default", async () => {
+ const wrapper = ({ children }: { children: React.ReactNode }) => {children};
+ const { result, unmount } = renderHook(() => useRealm(), { wrapper });
+ await waitFor(() => expect(result.current).not.toBe(null));
+ const realm = result.current;
+ unmount();
+ expect(realm.isClosed).toBe(true);
+ });
- it("can be provided in multiple parts of an application", async () => {
- const RealmComponent = () => {
- const realm = useRealm();
- return (
-