From 58ae94ed0aa31dfbf5940113730a53031322c629 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Mon, 10 Jun 2024 16:58:06 +0200 Subject: [PATCH 01/26] update 5 files --- CHANGELOG.md | 11 + packages/realm-react/src/RealmContext.ts | 109 +++ packages/realm-react/src/RealmProvider.tsx | 83 ++- .../src/__tests__/RealmProvider.test.tsx | 690 ++++++++++++------ packages/realm-react/src/index.tsx | 129 +--- 5 files changed, 658 insertions(+), 364 deletions(-) create mode 100644 packages/realm-react/src/RealmContext.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f4f1424373..974e655793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ ### Deprecations * None +### Enhancements +* None + +### Fixed +* None + +## vNext (TBD) + +### Deprecations +* None + ### Enhancements * Report the originating error that caused a client reset to occur. ([realm/realm-core#6154](https://github.com/realm/realm-core/issues/6154)) diff --git a/packages/realm-react/src/RealmContext.ts b/packages/realm-react/src/RealmContext.ts new file mode 100644 index 0000000000..cc4a90af11 --- /dev/null +++ b/packages/realm-react/src/RealmContext.ts @@ -0,0 +1,109 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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 { RealmProviderFC } from "./RealmProvider"; + +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} 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"` + * 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 + * and return an empty array if the collection is empty. + * + * 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; +}; diff --git a/packages/realm-react/src/RealmProvider.tsx b/packages/realm-react/src/RealmProvider.tsx index 43cb788424..0ac891e717 100644 --- a/packages/realm-react/src/RealmProvider.tsx +++ b/packages/realm-react/src/RealmProvider.tsx @@ -26,11 +26,7 @@ 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; +export type RealmProviderWithConfigurationProps = { /** * If false, Realm will not be closed when the component unmounts. * @default true @@ -41,8 +37,57 @@ 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; -}; + + realm?: never; +} & PartialRealmConfiguration; + +/** + * Explicitly sets all the properties of a type to never. + * Useful for ensuring different prop types are mutually exclusive. + */ +type Never = { [K in keyof T]?: never }; + +export type RealmProviderWithRealmInstanceProps = + | { + /** + * The Realm instance to be used by the provider. + */ + realm: Realm; + children: React.ReactNode; + + fallback?: never; + closeOnUnmount?: never; + realmRef?: never; + } & Never; + +/** + * Represents the provider returned from using an existing realm at context creation i.e. `createRealmContext(new Realm))`. + */ +export type RealmProviderWithRealmInstanceFC = React.FC>; +/** + * Represents the provider returned from using a configuration at context creation i.e. `createRealmContext({schema: []}))`. + */ +export type RealmProviderWithConfigurationFC = React.FC; + +/** + * Represents the provider returned from creating context with no arguments (including the default context). + * Supports either passing a `realm` as a property or the schema configuration. + */ +export type RealmProviderFC = React.FC; + +export function createRealmProviderFromRealm( + realm: Realm | null, + RealmContext: React.Context, +): RealmProviderWithRealmInstanceFC { + return ({ children }) => { + return ; + }; +} /** * Generates a `RealmProvider` given a {@link Realm.Configuration} and {@link React.Context}. @@ -53,31 +98,7 @@ type ProviderProps = PartialRealmConfiguration & { export function createRealmProvider( 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"` - */ +): RealmProviderWithConfigurationFC { return ({ children, fallback: Fallback, closeOnUnmount = true, realmRef, ...restProps }) => { const [realm, setRealm] = useState(() => realmConfig.sync === undefined && restProps.sync === undefined diff --git a/packages/realm-react/src/__tests__/RealmProvider.test.tsx b/packages/realm-react/src/__tests__/RealmProvider.test.tsx index 63709d2d7c..9c74434696 100644 --- a/packages/realm-react/src/__tests__/RealmProvider.test.tsx +++ b/packages/realm-react/src/__tests__/RealmProvider.test.tsx @@ -22,8 +22,13 @@ 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 { + RealmProviderWithRealmInstanceFC, + areConfigurationsIdentical, + mergeRealmConfiguration, +} from "../RealmProvider"; import { randomRealmPath } from "./helpers"; +import { RealmContext } from "../RealmContext"; const dogSchema: Realm.ObjectSchema = { name: "dog", @@ -43,7 +48,7 @@ const catSchema: Realm.ObjectSchema = { }, }; -const { RealmProvider, useRealm } = createRealmContext({ +const realmContextWithConfig = createRealmContext({ schema: [dogSchema], inMemory: true, path: randomRealmPath(), @@ -56,304 +61,511 @@ describe("RealmProvider", () => { 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 } = realmContextWithConfig; - 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).not.toBe(null); + 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 ( -