From 9bd0c91f5e696dffa5c61a20c25c473aa9f5d2b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 6 Mar 2024 08:52:27 +0100 Subject: [PATCH] WIP --- packages/realm-react/src/AppProvider.tsx | 2 +- packages/realm-react/src/RealmContext.ts | 71 +++++++ packages/realm-react/src/RealmProvider.tsx | 33 +++- packages/realm-react/src/UserProvider.tsx | 3 +- packages/realm-react/src/helpers.ts | 5 +- packages/realm-react/src/index.tsx | 175 ++---------------- packages/realm-react/src/useAuth.tsx | 8 +- packages/realm-react/src/useAuthOperation.tsx | 9 +- .../realm-react/src/useEmailPasswordAuth.tsx | 4 +- packages/realm-react/src/useObject.tsx | 19 +- packages/realm-react/src/useQuery.tsx | 47 ++++- packages/realm-react/src/useRealm.tsx | 8 + packages/realm/src/deprecated-global.ts | 2 +- 13 files changed, 194 insertions(+), 192 deletions(-) create mode 100644 packages/realm-react/src/RealmContext.ts diff --git a/packages/realm-react/src/AppProvider.tsx b/packages/realm-react/src/AppProvider.tsx index b0541b0420..e3b85747b8 100644 --- a/packages/realm-react/src/AppProvider.tsx +++ b/packages/realm-react/src/AppProvider.tsx @@ -53,7 +53,7 @@ const AuthOperationProvider: React.FC = ({ children }) => { * can be used to create a Realm.App instance: * https://www.mongodb.com/docs/realm-sdks/js/latest/Realm.App.html#~AppConfiguration */ -type AppProviderProps = Realm.AppConfiguration & { +export type AppProviderProps = Realm.AppConfiguration & { /** * A ref to the App instance. This is useful if you need to access the App * instance outside of a component that uses the App hooks. diff --git a/packages/realm-react/src/RealmContext.ts b/packages/realm-react/src/RealmContext.ts new file mode 100644 index 0000000000..a74c326ae7 --- /dev/null +++ b/packages/realm-react/src/RealmContext.ts @@ -0,0 +1,71 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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 { createContext } from "react"; +import Realm from "realm"; + +import { RealmProvider, createRealmProvider } from "./RealmProvider"; +import { UseObjectHook, createUseObject } from "./useObject"; +import { UseQueryHook, createUseQuery } from "./useQuery"; +import { UseRealmHook, createUseRealm } from "./useRealm"; + +export type RealmContext = { + RealmProvider: RealmProvider; + useRealm: UseRealmHook; + useQuery: UseQueryHook; + useObject: UseObjectHook; +}; + +/** + * Creates Realm React hooks and Provider component for a given Realm configuration + * @example + * ``` + *class Task extends Realm.Object { + * ... + * + * static schema = { + * 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 const createRealmContext: (realmConfig?: Realm.Configuration) => RealmContext = ( + realmConfig: Realm.Configuration = {}, +) => { + const RealmContext = createContext(null); + const RealmProvider = createRealmProvider(realmConfig, 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 43cb788424..7a6cf1e846 100644 --- a/packages/realm-react/src/RealmProvider.tsx +++ b/packages/realm-react/src/RealmProvider.tsx @@ -22,11 +22,11 @@ import { isEqual } from "lodash"; import { UserContext } from "./UserProvider"; -type PartialRealmConfiguration = Omit, "sync"> & { +export type PartialRealmConfiguration = Omit, "sync"> & { sync?: Partial; }; -type ProviderProps = PartialRealmConfiguration & { +export type RealmProviderProps = PartialRealmConfiguration & { /** * The fallback component to render if the Realm is not opened. */ @@ -44,6 +44,33 @@ type ProviderProps = PartialRealmConfiguration & { children: React.ReactNode; }; +/** + * 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. + */ +export type RealmProvider = React.FC; + /** * Generates a `RealmProvider` given a {@link Realm.Configuration} and {@link React.Context}. * @param realmConfig - The configuration of the Realm to be instantiated @@ -53,7 +80,7 @@ type ProviderProps = PartialRealmConfiguration & { export function createRealmProvider( realmConfig: Realm.Configuration, RealmContext: React.Context, -): React.FC { +): RealmProvider { /** * Returns a Context Provider component that is required to wrap any component using * the Realm hooks. diff --git a/packages/realm-react/src/UserProvider.tsx b/packages/realm-react/src/UserProvider.tsx index 90da85ee20..48f6b0d57c 100644 --- a/packages/realm-react/src/UserProvider.tsx +++ b/packages/realm-react/src/UserProvider.tsx @@ -26,7 +26,7 @@ import { useApp } from "./AppProvider"; */ export const UserContext = createContext(null); -type UserProviderProps = { +export type UserProviderProps = { /** * The fallback component to render if there is no authorized user. This can be used * to render a login screen or another component which will log the user in. @@ -75,7 +75,6 @@ export const UserProvider: React.FC = ({ fallback: Fallback, * {@link UserProvider} context. The user is stored as React state, * so will trigger a re-render whenever it changes (e.g. logging in, * logging out, switching user). - * */ export const useUser = < FunctionsFactoryType extends Realm.DefaultFunctionsFactory, diff --git a/packages/realm-react/src/helpers.ts b/packages/realm-react/src/helpers.ts index 61f2a9b13a..fd31406b62 100644 --- a/packages/realm-react/src/helpers.ts +++ b/packages/realm-react/src/helpers.ts @@ -46,8 +46,9 @@ export type CollectionCallback = Parameters; -export type RealmClassType = { new (...args: any): T }; +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +export type RealmClassType = { new (...args: any): T }; -export function isClassModelConstructor(value: unknown): value is RealmClassType { +export function isClassModelConstructor(value: unknown): value is RealmClassType { return Object.getPrototypeOf(value) === Realm.Object; } diff --git a/packages/realm-react/src/index.tsx b/packages/realm-react/src/index.tsx index d6f36b118c..97837cc75f 100644 --- a/packages/realm-react/src/index.tsx +++ b/packages/realm-react/src/index.tsx @@ -17,141 +17,21 @@ //////////////////////////////////////////////////////////////////////////// import Realm from "realm"; -import React, { createContext } from "react"; - -import { createRealmProvider } from "./RealmProvider"; -import { createUseObject } from "./useObject"; -import { createUseQuery } from "./useQuery"; -import { createUseRealm } from "./useRealm"; +export type { RealmProviderProps, PartialRealmConfiguration } from "./RealmProvider"; +export type { UserProviderProps } from "./UserProvider"; export type { UseObjectHook } from "./useObject"; -export type { UseQueryHook, QueryHookOptions, QueryHookClassBasedOptions } from "./useQuery"; - -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: ReturnType; - /** - * 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; -}; - -/** - * Creates Realm React hooks and Provider component for a given Realm configuration - * @example - * ``` - *class Task extends Realm.Object { - * ... - * - * static schema = { - * 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 const createRealmContext: (realmConfig?: Realm.Configuration) => RealmContext = ( - realmConfig: Realm.Configuration = {}, -) => { - const RealmContext = createContext(null); - const RealmProvider = createRealmProvider(realmConfig, RealmContext); +export type { + UseQueryHook, + QueryHookOptions, + QueryHookClassBasedOptions, + QueryCallback, + DependencyList, +} from "./useQuery"; - const useRealm = createUseRealm(RealmContext); - const useQuery = createUseQuery(useRealm); - const useObject = createUseObject(useRealm); +import { UseQueryHook } from "./useQuery"; - return { - RealmProvider, - useRealm, - useQuery, - useObject, - }; -}; +import { createRealmContext } from "./RealmContext"; const defaultContext = createRealmContext(); @@ -188,39 +68,6 @@ export const RealmProvider = defaultContext.RealmProvider; */ export const useRealm = defaultContext.useRealm; -/** - * 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 - */ export const useQuery = defaultContext.useQuery; /** diff --git a/packages/realm-react/src/useAuth.tsx b/packages/realm-react/src/useAuth.tsx index e400fcd167..0ee2265472 100644 --- a/packages/realm-react/src/useAuth.tsx +++ b/packages/realm-react/src/useAuth.tsx @@ -23,7 +23,9 @@ import { AuthOperationName, AuthResult } from "./types"; import { useApp, useAuthResult } from "./AppProvider"; import { useAuthOperation } from "./useAuthOperation"; -interface UseAuth { +export type UseAuthHook = () => AuthMethods; + +export interface AuthMethods { /** * Log in with a {@link Realm.Credentials} instance. This allows login with any * authentication mechanism supported by Realm. @@ -108,7 +110,7 @@ interface UseAuth { * code. * @returns An object containing operations and state for authenticating with an Atlas App. */ -export function useAuth(): UseAuth { +export const useAuth: UseAuthHook = () => { const app = useApp(); const [result] = useAuthResult(); @@ -182,4 +184,4 @@ export function useAuth(): UseAuth { logInWithFunction, logOut, }; -} +}; diff --git a/packages/realm-react/src/useAuthOperation.tsx b/packages/realm-react/src/useAuthOperation.tsx index 0370f85b01..f55c6f3f8c 100644 --- a/packages/realm-react/src/useAuthOperation.tsx +++ b/packages/realm-react/src/useAuthOperation.tsx @@ -21,13 +21,12 @@ import { useCallback } from "react"; import { AuthError, AuthOperationName, OperationState } from "./types"; import { useAuthResult } from "./AppProvider"; -export function useAuthOperation({ - operation, - operationName, -}: { +type UseAuthOperationOptions = { operation: (...args: Args) => Promise; operationName: AuthOperationName; -}) { +}; + +export function useAuthOperation({ operation, operationName }: UseAuthOperationOptions) { const [result, setResult] = useAuthResult(); return useCallback<(...args: Args) => void>( diff --git a/packages/realm-react/src/useEmailPasswordAuth.tsx b/packages/realm-react/src/useEmailPasswordAuth.tsx index f952502c97..17ebe674f2 100644 --- a/packages/realm-react/src/useEmailPasswordAuth.tsx +++ b/packages/realm-react/src/useEmailPasswordAuth.tsx @@ -23,7 +23,7 @@ import { AuthOperationName, AuthResult } from "./types"; import { useApp, useAuthResult } from "./AppProvider"; import { useAuthOperation } from "./useAuthOperation"; -interface UseEmailPasswordAuth { +export interface EmailPasswordAuthMethods { /** * Convenience function to log in a user with an email and password - users * could also call `logIn(Realm.Credentials.emailPassword(email, password))`. @@ -107,7 +107,7 @@ interface UseEmailPasswordAuth { * code. * @returns An object containing operations and state related to Email/Password authentication. */ -export function useEmailPasswordAuth(): UseEmailPasswordAuth { +export function useEmailPasswordAuth(): EmailPasswordAuthMethods { const app = useApp(); const [result] = useAuthResult(); diff --git a/packages/realm-react/src/useObject.tsx b/packages/realm-react/src/useObject.tsx index 0cc323c487..14502fb7dd 100644 --- a/packages/realm-react/src/useObject.tsx +++ b/packages/realm-react/src/useObject.tsx @@ -23,27 +23,38 @@ import { CachedObject, createCachedObject } from "./cachedObject"; import { AnyRealmObject, CollectionCallback, + RealmClassType, getObjectForPrimaryKey, getObjects, isClassModelConstructor, } from "./helpers"; import { UseRealmHook } from "./useRealm"; -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -type RealmClassType = { new (...args: any): T }; - export type ObjectHookOptions = { type: string; primaryKey: T[keyof T]; keyPaths?: string | string[]; }; -export type ObjectHookClassBasedOptions = { +export type ObjectHookClassBasedOptions = { type: RealmClassType; primaryKey: T[keyof T]; keyPaths?: string | string[]; }; +/** + * 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. + */ export type UseObjectHook = { (options: ObjectHookOptions): (T & Realm.Object) | null; (options: ObjectHookClassBasedOptions): T | null; diff --git a/packages/realm-react/src/useQuery.tsx b/packages/realm-react/src/useQuery.tsx index cfeb629f9c..84ba44b4bb 100644 --- a/packages/realm-react/src/useQuery.tsx +++ b/packages/realm-react/src/useQuery.tsx @@ -22,8 +22,8 @@ import Realm from "realm"; import { createCachedCollection } from "./cachedCollection"; import { AnyRealmObject, RealmClassType, getObjects, isClassModelConstructor } from "./helpers"; -type QueryCallback = (collection: Realm.Results) => Realm.Results; -type DependencyList = ReadonlyArray; +export type QueryCallback = (collection: Realm.Results) => Realm.Results; +export type DependencyList = ReadonlyArray; export type QueryHookOptions = { type: string; @@ -31,16 +31,53 @@ export type QueryHookOptions = { keyPaths?: string | string[]; }; -export type QueryHookClassBasedOptions = { +export type QueryHookClassBasedOptions = { type: RealmClassType; query?: QueryCallback; keyPaths?: string | string[]; }; -export type UseQueryHook = { +/** + * 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 memoized, + * 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 useMemo} + * @returns a collection of realm objects or an empty array + */ +export interface UseQueryHook { + /** ... */ (options: QueryHookOptions, deps?: DependencyList): Realm.Results>; + /** ... */ (options: QueryHookClassBasedOptions, deps?: DependencyList): Realm.Results; + /** ... */ (type: string): Realm.Results>; + /** ... */ (type: RealmClassType): Realm.Results; /** @deprecated To help the `react-hooks/exhaustive-deps` eslint rule detect missing dependencies, we've suggest passing a option object as the first argument */ @@ -51,7 +88,7 @@ export type UseQueryHook = { query?: QueryCallback, deps?: DependencyList, ): Realm.Results; -}; +} /** * Maps a value to itself diff --git a/packages/realm-react/src/useRealm.tsx b/packages/realm-react/src/useRealm.tsx index 596c9d60ef..e274ff2463 100644 --- a/packages/realm-react/src/useRealm.tsx +++ b/packages/realm-react/src/useRealm.tsx @@ -19,6 +19,14 @@ import Realm from "realm"; import { useContext } from "react"; +/** + * Returns the instance of the {@link Realm} opened by the `RealmProvider`. + * @example + * ``` + * const realm = useRealm(); + * ``` + * @returns a realm instance + */ export type UseRealmHook = { (): Realm; }; diff --git a/packages/realm/src/deprecated-global.ts b/packages/realm/src/deprecated-global.ts index db101fac6b..14c52f0e6c 100644 --- a/packages/realm/src/deprecated-global.ts +++ b/packages/realm/src/deprecated-global.ts @@ -56,7 +56,7 @@ declare global { * @deprecated Will be removed in v13.0.0. Please use an import statement. */ export namespace Realm { - export import Realm = RealmItself; + export import Realm = RealmConstructor; export import flags = internal.flags; export import Object = internal.RealmObject;