Skip to content

Commit

Permalink
Refactor useObject
Browse files Browse the repository at this point in the history
* Prime any list properties with an cachedCollection so that updates fire correctly (Fixes #5185)
* Primary Keys as non-primative values would reset the cached objects, since their reference always changes
* Create a listener on the collection if the object doesn't exist, and rerender when it is created (Fixes #4514)
  • Loading branch information
takameyer committed Dec 30, 2022
1 parent 4c83959 commit b5d36a0
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 64 deletions.
9 changes: 3 additions & 6 deletions packages/realm-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
* Immediately bind local Realm in the RealmProvider ([#5074](https://github.com/realm/realm-js/issues/5074))

### Fixed
* <How to hit and notice issue? what was the impact?> ([#????](https://github.com/realm/realm-js/issues/????), since v?.?.?)
* Prime any list properties with an cachedCollection so that updates fire correctly ([#5185](https://github.com/realm/realm-js/issues/5185))
* Primary Keys as non-primative values would reset the cached objects, since their reference always changes
* Create a listener on the collection if the object doesn't exist, and rerender when it is created ([#4514](https://github.com/realm/realm-js/issues/4514))
* None

### Compatibility
Expand All @@ -13,11 +15,6 @@
* Realm Studio v12.0.0.
* File format: generates Realms with format v22 (reads and upgrades file format v5 or later for non-synced Realm, upgrades file format v10 or later for synced Realms).

### Internal
<!-- * Either mention core version or upgrade -->
<!-- * Using Realm Core vX.Y.Z -->
<!-- * Upgraded Realm Core from vX.Y.Z to vA.B.C -->

## 0.4.1 (2022-11-3)

### Fixed
Expand Down
139 changes: 103 additions & 36 deletions packages/realm-react/src/__tests__/useObjectRender.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ import Realm from "realm";
import { createUseObject } from "../useObject";

export class ListItem extends Realm.Object {
id!: number;
id!: Realm.BSON.ObjectId;
name!: string;
lists!: Realm.List<List>;

static schema: Realm.ObjectSchema = {
name: "ListItem",
properties: {
id: "int",
id: "objectId",
name: "string",
lists: {
type: "linkingObjects",
Expand All @@ -43,7 +43,7 @@ export class ListItem extends Realm.Object {
}

export class List extends Realm.Object {
id!: number;
id!: Realm.BSON.ObjectId;
title!: string;
items!: Realm.List<ListItem>;
favoriteItem?: ListItem;
Expand All @@ -52,7 +52,7 @@ export class List extends Realm.Object {
static schema: Realm.ObjectSchema = {
name: "List",
properties: {
id: "int",
id: "objectId",
title: "string",
items: "ListItem[]",
favoriteItem: "ListItem",
Expand Down Expand Up @@ -81,7 +81,10 @@ const listRenderCounter = jest.fn();

let testRealm: Realm = new Realm(configuration);

const testCollection = [...new Array(100)].map((_, index) => ({ id: index, name: `${index}` }));
const testCollection = [...new Array(100)].map(() => {
const id = new Realm.BSON.ObjectId();
return { id, name: id.toHexString() };
});

const useRealm = () => {
testRealm = new Realm(configuration);
Expand All @@ -92,14 +95,16 @@ const useRealm = () => {

const useObject = createUseObject(useRealm);

const App = () => {
const App = ({ renderItems = true }) => {
return (
<SetupComponent>
<TestComponent testID="testContainer" />
<TestComponent testID="testContainer" renderItems={renderItems} />
</SetupComponent>
);
};

const parentObjectId = new Realm.BSON.ObjectId();

const SetupComponent = ({ children }: { children: JSX.Element }): JSX.Element | null => {
const realm = useRealm();

Expand All @@ -113,7 +118,6 @@ const SetupComponent = ({ children }: { children: JSX.Element }): JSX.Element |
useEffect(() => {
realm.write(() => {
realm.deleteAll();
realm.create(List, { id: 1, title: "List", items: testCollection });
});
setSetupComplete(true);
}, [realm]);
Expand All @@ -128,14 +132,15 @@ const SetupComponent = ({ children }: { children: JSX.Element }): JSX.Element |
const Item: React.FC<{ item: ListItem }> = React.memo(({ item }) => {
itemRenderCounter();
const realm = useRealm();
const idString = item.id.toHexString();

return (
<View testID={`result${item.id}`}>
<View testID={`name${item.id}`}>
<View testID={`result${idString}`}>
<View testID={`name${idString}`}>
<Text>{item.name}</Text>
</View>
<TextInput
testID={`input${item.id}`}
testID={`input${idString}`}
value={item.name}
onChangeText={(text) => {
realm.write(() => {
Expand All @@ -144,7 +149,7 @@ const Item: React.FC<{ item: ListItem }> = React.memo(({ item }) => {
}}
></TextInput>
<TouchableHighlight
testID={`deleteButton${item.id}`}
testID={`deleteButton${idString}`}
onPress={() => {
realm.write(() => {
realm.delete(item);
Expand All @@ -157,12 +162,10 @@ const Item: React.FC<{ item: ListItem }> = React.memo(({ item }) => {
);
});

const TestComponent: React.FC<{ testID?: string }> = ({ testID }) => {
const list = useObject(List, 1);
const TestComponent: React.FC<{ testID?: string; renderItems?: boolean }> = ({ testID, renderItems }) => {
const list = useObject(List, parentObjectId);
const realm = useRealm();

listRenderCounter();

const renderItem = useCallback<ListRenderItem<ListItem>>(({ item }) => <Item item={item} />, []);

const keyExtractor = useCallback((item: ListItem) => `${item.id}`, []);
Expand All @@ -171,15 +174,21 @@ const TestComponent: React.FC<{ testID?: string }> = ({ testID }) => {
return <View testID={testID} />;
}

listRenderCounter();

const listIdString = list.id.toHexString();

return (
<View testID={testID}>
<FlatList testID="list" data={list?.items ?? []} keyExtractor={keyExtractor} renderItem={renderItem} />;
<View testID={`list${list.id}`}>
<View testID={`listTitle${list.id}`}>
<View testID="list">
{renderItems && <FlatList data={list.items} keyExtractor={keyExtractor} renderItem={renderItem} />}
</View>
<View testID={`list${listIdString}`}>
<View testID={`listTitle${listIdString}`}>
<Text>{list.title}</Text>
</View>
<TextInput
testID={`listTitleInput${list.id}`}
testID={`listTitleInput${listIdString}`}
value={list.title}
onChangeText={(text) => {
realm.write(() => {
Expand All @@ -188,7 +197,7 @@ const TestComponent: React.FC<{ testID?: string }> = ({ testID }) => {
}}
></TextInput>
<TouchableHighlight
testID={`deleteListButton${list.id}`}
testID={`deleteListButton${listIdString}`}
onPress={() => {
realm.write(() => {
realm.delete(list);
Expand All @@ -212,8 +221,15 @@ async function setupTest() {
const { getByTestId, getByText, debug } = render(<App />);
await waitFor(() => getByTestId("testContainer"));

const object = testRealm.objectForPrimaryKey(List, 1);
// In order to test that `useObject` brings the non-existing object into view when it's created,
// we do the creation after the app is rendered.
testRealm.write(() => {
testRealm.create(List, { id: parentObjectId, title: "List", items: testCollection });
});

const object = testRealm.objectForPrimaryKey(List, parentObjectId);
if (!object) throw new Error("Object not found in Realm");
await waitFor(() => getByTestId("list"));
const collection = object.items;

expect(listRenderCounter).toHaveBeenCalledTimes(1);
Expand All @@ -234,15 +250,23 @@ describe("useObject: rendering objects with a Realm.List property", () => {
it("render an object in one render cycle", async () => {
const { getByTestId } = render(<App />);

// In order to test that `useObject` brings the non-existing object into view when it's created,
// we do the creation after the app is rendered.
testRealm.write(() => {
testRealm.create(List, { id: parentObjectId, title: "List", items: testCollection });
});

await waitFor(() => getByTestId("list"));

expect(listRenderCounter).toHaveBeenCalledTimes(1);
});
it("only re-renders the changed object when a property changes", async () => {
const { getByTestId, getByText, object } = await setupTest();

const titleElement = getByTestId(`listTitle${object.id}`);
const inputComponent = getByTestId(`listTitleInput${object.id}`);
const idString = object.id.toHexString();

const titleElement = getByTestId(`listTitle${idString}`);
const inputComponent = getByTestId(`listTitleInput${idString}`);

expect(titleElement).toHaveTextContent("List");
expect(listRenderCounter).toHaveBeenCalledTimes(1);
Expand All @@ -257,7 +281,9 @@ describe("useObject: rendering objects with a Realm.List property", () => {
it("it nullifies the object when it is deleted", async () => {
const { getByTestId, object } = await setupTest();

const deleteButton = getByTestId(`deleteListButton${object.id}`);
const idString = object.id.toHexString();

const deleteButton = getByTestId(`deleteListButton${idString}`);

fireEvent.press(deleteButton);

Expand All @@ -267,17 +293,18 @@ describe("useObject: rendering objects with a Realm.List property", () => {

expect(object.isValid()).toBe(false);

expect(testRealm.objectForPrimaryKey(List, 1)).toBe(null);
expect(testRealm.objectForPrimaryKey(List, parentObjectId)).toBe(null);

const testContainer = getByTestId("testContainer");
expect(testContainer).toBeEmptyElement();

expect(listRenderCounter).toHaveBeenCalledTimes(2);
// List is now gone, so it wasn't detected as a render
expect(listRenderCounter).toHaveBeenCalledTimes(1);
});

it("test changes to linked object", async () => {
const { getByTestId } = await setupTest();
const object = testRealm.objectForPrimaryKey(List, 1);
const object = testRealm.objectForPrimaryKey(List, parentObjectId);
if (!object) throw new Error("Object not found in Realm");

await act(async () => {
Expand Down Expand Up @@ -315,6 +342,10 @@ describe("useObject: rendering objects with a Realm.List property", () => {
it("renders each visible item in the list once", async () => {
const { getByTestId } = render(<App />);

testRealm.write(() => {
testRealm.create(List, { id: parentObjectId, title: "List", items: testCollection });
});

await waitFor(() => getByTestId("list"));

expect(itemRenderCounter).toHaveBeenCalledTimes(10);
Expand All @@ -325,10 +356,12 @@ describe("useObject: rendering objects with a Realm.List property", () => {

const id = collection[0].id;

const nameElement = getByTestId(`name${id}`);
const input = getByTestId(`input${id}`);
const idString = id.toHexString();

const nameElement = getByTestId(`name${idString}`);
const input = getByTestId(`input${idString}`);

expect(nameElement).toHaveTextContent(`${id}`);
expect(nameElement).toHaveTextContent(`${idString}`);

fireEvent.changeText(input, "apple");

Expand All @@ -346,17 +379,21 @@ describe("useObject: rendering objects with a Realm.List property", () => {
const firstItem = collection[0];
const id = firstItem.id;

const idString = id.toHexString();

const nextVisible = collection[10];

const deleteButton = getByTestId(`deleteButton${id}`);
const nameElement = getByTestId(`name${id}`);
const deleteButton = getByTestId(`deleteButton${idString}`);
const nameElement = getByTestId(`name${idString}`);

expect(nameElement).toHaveTextContent(`${id}`);
expect(nameElement).toHaveTextContent(`${idString}`);
expect(itemRenderCounter).toHaveBeenCalledTimes(10);

fireEvent.press(deleteButton);

await waitFor(() => getByTestId(`name${nextVisible.id}`));
const nextIdString = nextVisible.id.toHexString();

await waitFor(() => getByTestId(`name${nextIdString}`));

expect(itemRenderCounter).toHaveBeenCalledTimes(20);
});
Expand All @@ -380,7 +417,7 @@ describe("useObject: rendering objects with a Realm.List property", () => {
it("only renders the new item when a list item is added", async () => {
const { collection } = await setupTest();
testRealm.write(() => {
collection.unshift(testRealm.create(ListItem, { id: 9999, name: "apple" }));
collection.unshift(testRealm.create(ListItem, { id: new Realm.BSON.ObjectId(), name: "apple" }));
});

// Force Realm listeners to fire rather than waiting for the text "apple"
Expand Down Expand Up @@ -422,5 +459,35 @@ describe("useObject: rendering objects with a Realm.List property", () => {

// no assertion here, just checking that the test doesn't crash
});
it("re-renders the list even if the list items have not been rendered", async () => {
const { getByTestId } = render(<App renderItems={false} />);

const list = testRealm.write(() => {
return testRealm.create(List, { id: parentObjectId, title: "List" });
});

await waitFor(() => getByTestId("list"));

expect(listRenderCounter).toHaveBeenCalledTimes(1);

testRealm.write(() => {
list.items.push(testRealm.create(ListItem, testCollection[0]));
});

await act(async () => {
forceSynchronousNotifications(testRealm);
});

expect(listRenderCounter).toHaveBeenCalledTimes(2);

testRealm.write(() => {
list.items.push(testRealm.create(ListItem, testCollection[1]));
});

await act(async () => {
forceSynchronousNotifications(testRealm);
});
expect(listRenderCounter).toHaveBeenCalledTimes(3);
});
});
});
Loading

0 comments on commit b5d36a0

Please sign in to comment.