Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
kraenhansen committed Feb 28, 2024
1 parent d785a59 commit 2f0a7a5
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 146 deletions.
71 changes: 71 additions & 0 deletions packages/realm-react/src/RealmContext.ts
Original file line number Diff line number Diff line change
@@ -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<Realm | null>(null);
const RealmProvider = createRealmProvider(realmConfig, RealmContext);

const useRealm = createUseRealm(RealmContext);
const useQuery = createUseQuery(useRealm);
const useObject = createUseObject(useRealm);

return {
RealmProvider,
useRealm,
useQuery,
useObject,
};
};
31 changes: 29 additions & 2 deletions packages/realm-react/src/RealmProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type PartialRealmConfiguration = Omit<Partial<Realm.Configuration>, "sync"> & {
sync?: Partial<Realm.SyncConfiguration>;
};

type ProviderProps = PartialRealmConfiguration & {
export type ProviderProps = PartialRealmConfiguration & {
/**
* The fallback component to render if the Realm is not opened.
*/
Expand All @@ -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 (
* <RealmProvider schema={[Task, User]} path={"data.realm"} sync={syncConfig}>
* <App/>
* </RealmProvider>
* )
* }
* ```
* @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 `<RealmProvider>` 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<ProviderProps>;

/**
* Generates a `RealmProvider` given a {@link Realm.Configuration} and {@link React.Context}.
* @param realmConfig - The configuration of the Realm to be instantiated
Expand All @@ -53,7 +80,7 @@ type ProviderProps = PartialRealmConfiguration & {
export function createRealmProvider(
realmConfig: Realm.Configuration,
RealmContext: React.Context<Realm | null>,
): React.FC<ProviderProps> {
): RealmProvider {
/**
* Returns a Context Provider component that is required to wrap any component using
* the Realm hooks.
Expand Down
1 change: 0 additions & 1 deletion packages/realm-react/src/UserProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ export const UserProvider: React.FC<UserProviderProps> = ({ 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,
Expand Down
134 changes: 2 additions & 132 deletions packages/realm-react/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,142 +17,12 @@
////////////////////////////////////////////////////////////////////////////

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";
import React from "react";

import { createRealmContext } from "./RealmContext";
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 (
* <RealmProvider schema={[Task, User]} path={"data.realm"} sync={syncConfig}>
* <App/>
* </RealmProvider>
* )
* }
* ```
* @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 `<RealmProvider>` 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<typeof createRealmProvider>;
/**
* Returns the instance of the {@link Realm} opened by the `RealmProvider`.
* @example
* ```
* const realm = useRealm();
* ```
* @returns a realm instance
*/
useRealm: ReturnType<typeof createUseRealm>;

/**
* 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<typeof createUseQuery>;
/**
* 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<typeof createUseObject>;
};

/**
* 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<Realm | null>(null);
const RealmProvider = createRealmProvider(realmConfig, RealmContext);

const useRealm = createUseRealm(RealmContext);
const useQuery = createUseQuery(useRealm);
const useObject = createUseObject(useRealm);

return {
RealmProvider,
useRealm,
useQuery,
useObject,
};
};

const defaultContext = createRealmContext();

/**
Expand Down
8 changes: 5 additions & 3 deletions packages/realm-react/src/useAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import { AuthOperationName, AuthResult } from "./types";
import { useApp, useAuthResult } from "./AppProvider";
import { useAuthOperation } from "./useAuthOperation";

interface UseAuth {
export type UseAuthHook = () => AuthMethods;

interface AuthMethods {
/**
* Log in with a {@link Realm.Credentials} instance. This allows login with any
* authentication mechanism supported by Realm.
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -182,4 +184,4 @@ export function useAuth(): UseAuth {
logInWithFunction,
logOut,
};
}
};
9 changes: 4 additions & 5 deletions packages/realm-react/src/useAuthOperation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,12 @@ import { useCallback } from "react";
import { AuthError, AuthOperationName, OperationState } from "./types";
import { useAuthResult } from "./AppProvider";

export function useAuthOperation<Args extends unknown[]>({
operation,
operationName,
}: {
type UseAuthOperationOptions<Args extends unknown[]> = {
operation: (...args: Args) => Promise<unknown>;
operationName: AuthOperationName;
}) {
};

export function useAuthOperation<Args extends unknown[]>({ operation, operationName }: UseAuthOperationOptions<Args>) {
const [result, setResult] = useAuthResult();

return useCallback<(...args: Args) => void>(
Expand Down
4 changes: 2 additions & 2 deletions packages/realm-react/src/useEmailPasswordAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { AuthOperationName, AuthResult } from "./types";
import { useApp, useAuthResult } from "./AppProvider";
import { useAuthOperation } from "./useAuthOperation";

interface UseEmailPasswordAuth {
interface EmailPasswordAuthMethods {
/**
* Convenience function to log in a user with an email and password - users
* could also call `logIn(Realm.Credentials.emailPassword(email, password))`.
Expand Down Expand Up @@ -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();

Expand Down
13 changes: 13 additions & 0 deletions packages/realm-react/src/useObject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ export type ObjectHookClassBasedOptions<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 = {
<T>(options: ObjectHookOptions<T>): (T & Realm.Object<T>) | null;
<T extends AnyRealmObject>(options: ObjectHookClassBasedOptions<T>): T | null;
Expand Down
33 changes: 33 additions & 0 deletions packages/realm-react/src/useQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,39 @@ export type QueryHookClassBasedOptions<T> = {
keyPaths?: string | string[];
};

/**
* 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 type UseQueryHook = {
<T>(options: QueryHookOptions<T>, deps?: DependencyList): Realm.Results<T & Realm.Object<T>>;
<T extends AnyRealmObject>(options: QueryHookClassBasedOptions<T>, deps?: DependencyList): Realm.Results<T>;
Expand Down

0 comments on commit 2f0a7a5

Please sign in to comment.