//////////////////////////////////////////////////////////////////////////// // // Copyright 2021 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 React, { useContext, useEffect, useRef, useState } from "react"; import Realm from "realm"; import isEqual from "lodash.isequal"; import { RestrictivePick } from "./helpers"; type PartialRealmConfiguration = Partial; export type RealmProviderFallback = React.ComponentType; /** Props used for a configuration-based Realm provider */ type RealmProviderConfigurationProps = { /** * If false, Realm will not be closed when the component unmounts. * @default true */ closeOnUnmount?: boolean; /** * A ref to the Realm instance. This is useful if you need to access the Realm * 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?: RealmProviderFallback | 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 DynamicRealmProviderWithConfigurationProps = 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 | DynamicRealmProviderWithConfigurationProps >; 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 createRealmProviderFromConfig( realmConfig: Realm.Configuration, RealmContext: React.Context, ): RealmProviderFromConfiguration { return ({ children, fallback: Fallback, closeOnUnmount = true, realmRef, ...restProps }) => { const [realm, setRealm] = useState(() => realmConfig.sync === undefined && restProps.sync === undefined ? new Realm(mergeRealmConfiguration(realmConfig, restProps)) : null, ); // We increment `configVersion` when a config override passed as a prop // changes, which triggers a `useEffect` to re-open the Realm with the // new config const [configVersion, setConfigVersion] = useState(0); // We put realm in a ref to avoid have an endless loop of updates when the realm is updated const currentRealm = useRef(realm); // This will merge the configuration provided by createRealmContext and any configuration properties // set directly on the RealmProvider component. Any settings on the component will override the original configuration. const configuration = useRef(mergeRealmConfiguration(realmConfig, restProps)); // Merge and set the configuration again and increment the version if any // of the RealmProvider properties change. useEffect(() => { const combinedConfig = mergeRealmConfiguration(realmConfig, restProps); if (!areConfigurationsIdentical(configuration.current, combinedConfig)) { configuration.current = combinedConfig; // Only rerender if realm has already been configured if (currentRealm.current != null) { setConfigVersion((x) => x + 1); } } }, [restProps]); useEffect(() => { currentRealm.current = realm; if (realmRef) { realmRef.current = realm; } }, [realm]); useEffect(() => { const realmRef = currentRealm.current; // Check if we currently have an open Realm. If we do not (i.e. it is the first // render, or the Realm has been closed due to a config change), then we // need to open a new Realm. const shouldInitRealm = realmRef === null; const initRealm = async () => { const openRealm = await Realm.open(configuration.current); setRealm(openRealm); }; if (shouldInitRealm) { initRealm().catch(console.error); } return () => { if (realm) { if (closeOnUnmount) { realm.close(); } setRealm(null); } }; }, [configVersion, realm, setRealm, closeOnUnmount]); if (!realm) { if (typeof Fallback === "function") { return ; } return <>{Fallback}; } return ; }; } /** * 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 { const RealmProviderFromConfig = createRealmProviderFromConfig({}, RealmContext); return ({ realm, children, ...config }) => { if (realm) { if (Object.keys(config).length > 0) { throw new Error("Cannot use configuration props when using an existing Realm instance."); } return ; } else { return ; } }; } /** * 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`. * @param configA - The default config object * @param configB - Config overrides object * @returns Merged config object */ export function mergeRealmConfiguration( configA: PartialRealmConfiguration, configB: PartialRealmConfiguration, ): Realm.Configuration { return { ...configA, ...configB, }; } /** * Utility function that does a deep comparison (key: value) of object a with object b * @param a - Object to compare * @param b - Object to compare * @returns True if the objects are identical */ export function areConfigurationsIdentical(a: Realm.Configuration, b: Realm.Configuration): boolean { return isEqual(a, b); }