Skip to content

Commit

Permalink
Implement keyPaths on useObject
Browse files Browse the repository at this point in the history
  • Loading branch information
kraenhansen committed Jan 9, 2024
1 parent c656688 commit fb613d7
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 14 deletions.
41 changes: 34 additions & 7 deletions packages/realm-react/src/__tests__/useObjectHook.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,23 @@ const dogSchema: Realm.ObjectSchema = {
properties: {
_id: "int",
name: "string",
age: "int",
},
};

interface IDog {
_id: number;
name: string;
age: number;
}

const context = createRealmTestContext({ schema: [dogSchema] });
const useObject = createUseObject(context.useRealm);

const testDataSet = [
{ _id: 4, name: "Vincent" },
{ _id: 5, name: "River" },
{ _id: 6, name: "Schatzi" },
{ _id: 4, name: "Vincent", age: 5 },
{ _id: 5, name: "River", age: 25 },
{ _id: 6, name: "Schatzi", age: 13 },
];

describe("useObject", () => {
Expand All @@ -63,10 +65,10 @@ describe("useObject", () => {
});

it("can retrieve a single object using useObject", () => {
const [, dog2] = testDataSet;
const { result } = renderHook(() => useObject<IDog>("dog", dog2._id));
const [, river] = testDataSet;
const { result } = renderHook(() => useObject<IDog>("dog", river._id));
const object = result.current;
expect(object).toMatchObject(dog2);
expect(object).toMatchObject(river);
});

describe("missing objects", () => {
Expand All @@ -81,10 +83,35 @@ describe("useObject", () => {
expect(renders).toHaveLength(1);
expect(result.current).toEqual(null);
write(() => {
realm.create<IDog>("dog", { _id: 12, name: "Lassie" });
realm.create<IDog>("dog", { _id: 12, name: "Lassie", age: 32 });
});
expect(renders).toHaveLength(2);
expect(result.current?.name).toEqual("Lassie");
});
});

describe("key-path filtering", () => {
it("re-renders only if a property in key-paths updates", () => {
const [vincent] = testDataSet;
const { write } = context;
const { result, renders } = profileHook(() => useObject<IDog>("dog", vincent._id, ["name"]));
expect(renders).toHaveLength(1);
expect(result.current).toMatchObject(vincent);
// Update the name and except a re-render
write(() => {
if (result.current) {
result.current.name = "Vince!";
}
});
expect(renders).toHaveLength(2);
expect(result.current?.name).toEqual("Vince!");
// Update the age and don't expect a re-render
write(() => {
if (result.current) {
result.current.age = 5;
}
});
expect(renders).toHaveLength(2);
});
});
});
17 changes: 14 additions & 3 deletions packages/realm-react/src/cachedObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ type CachedObjectArgs = {
* The implementing component should reset this to false when updating its object reference
*/
updatedRef: React.MutableRefObject<boolean>;

/**
* Optional list of key-paths to limit notifications.
*/
keyPaths?: string[];
};

export type CachedObject = {
Expand All @@ -62,7 +67,13 @@ export type CachedObject = {
* @param args - {@link CachedObjectArgs} object arguments
* @returns Proxy object wrapping the {@link Realm.Object}
*/
export function createCachedObject({ object, realm, updateCallback, updatedRef }: CachedObjectArgs): CachedObject {
export function createCachedObject({
object,
realm,
updateCallback,
updatedRef,
keyPaths,
}: CachedObjectArgs): CachedObject {
const listCaches = new Map();
const listTearDowns: Array<() => void> = [];
// If the object doesn't exist, just return it with an noop tearDown
Expand Down Expand Up @@ -135,10 +146,10 @@ export function createCachedObject({ object, realm, updateCallback, updatedRef }
// see https://github.com/realm/realm-js/issues/4375
if (realm.isInTransaction) {
setImmediate(() => {
object.addListener(listenerCallback);
object.addListener(listenerCallback, keyPaths);
});
} else {
object.addListener(listenerCallback);
object.addListener(listenerCallback, keyPaths);
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/realm-react/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
////////////////////////////////////////////////////////////////////////////

import Realm from "realm";
import { createContext } from "react";
import React, { createContext } from "react";

import { createRealmProvider } from "./RealmProvider";
import { createUseObject } from "./useObject";
Expand Down Expand Up @@ -93,6 +93,7 @@ type RealmContext = {
* ```
* @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>;
Expand Down
12 changes: 9 additions & 3 deletions packages/realm-react/src/useObject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import { CollectionCallback, getObjectForPrimaryKey, getObjects } from "./helper
import { UseRealmHook } from "./useRealm";

export type UseObjectHook = {
<T>(type: string, primaryKey: T[keyof T]): (T & Realm.Object<T>) | null;
<T extends Realm.Object<any>>(type: { new (...args: any): T }, primaryKey: T[keyof T]): T | null;
<T>(type: string, primaryKey: T[keyof T], keyPaths?: string[]): (T & Realm.Object<T>) | null;
<T extends Realm.Object<any>>(type: { new (...args: any): T }, primaryKey: T[keyof T], keyPaths?: string[]): T | null;
};

/**
Expand All @@ -37,6 +37,7 @@ export function createUseObject(useRealm: UseRealmHook): UseObjectHook {
return function useObject<T extends Realm.Object>(
type: string | { new (...args: any): T },
primaryKey: T[keyof T],
keyPaths?: string[],
): T | null {
const realm = useRealm();

Expand All @@ -62,12 +63,16 @@ export function createUseObject(useRealm: UseRealmHook): UseObjectHook {
// Ref: https://github.com/facebook/react/issues/14490
const cachedObjectRef = useRef<null | CachedObject>(null);

/* eslint-disable-next-line react-hooks/exhaustive-deps -- Memoizing the keyPaths to avoid renders */
const memoizedKeyPaths = useMemo(() => keyPaths, [JSON.stringify(keyPaths)]);

if (!cachedObjectRef.current) {
cachedObjectRef.current = createCachedObject({
object: originalObject ?? null,
realm,
updateCallback: forceRerender,
updatedRef,
keyPaths: memoizedKeyPaths,
});
}

Expand All @@ -94,6 +99,7 @@ export function createUseObject(useRealm: UseRealmHook): UseObjectHook {
realm,
updateCallback: forceRerender,
updatedRef,
keyPaths: memoizedKeyPaths,
});
originalObjectRef.current = originalObject;

Expand All @@ -104,7 +110,7 @@ export function createUseObject(useRealm: UseRealmHook): UseObjectHook {
}
return cachedObjectRef.current;
},
[realm, originalObject, primaryKey],
[realm, originalObject, primaryKey, memoizedKeyPaths],
);

// Invoke the tearDown of the cachedObject when useObject is unmounted
Expand Down

0 comments on commit fb613d7

Please sign in to comment.