Skip to content

Commit

Permalink
Fix for the #4375
Browse files Browse the repository at this point in the history
Make use of setImmediate to ensure event listeners are added after
write transactions have completed.
Update useQueryHook tests to give the event loop time to finish and add a listener.
Fix a test in useObject render to allow event listners to fire after writes.
  • Loading branch information
takameyer committed Apr 21, 2022
1 parent a3fdf37 commit 61e8e2a
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 26 deletions.
13 changes: 13 additions & 0 deletions packages/realm-react/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
x.x.x Release notes (yyyy-MM-dd)
=============================================================
### Enhancements
* None.

### Fixed
* Adding event listeners while in a write transaction ([#4375](https://github.com/realm/realm-js/issues/4375))
### Compatibility
* None.

### Internal
* None.

0.2.1 Release notes (2022-03-24)
=============================================================
### Enhancements
Expand Down
3 changes: 3 additions & 0 deletions packages/realm-react/src/__tests__/useObjectRender.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ describe("useObject: rendering objects with a Realm.List property", () => {
testRealm.write(() => {
object.favoriteItem = object.items[0];
});
forceSynchronousNotifications(testRealm);
});

expect(getByTestId(`favoriteItem-${object.items[0].name}`)).toBeTruthy();
Expand All @@ -291,6 +292,7 @@ describe("useObject: rendering objects with a Realm.List property", () => {
testRealm.write(() => {
object.favoriteItem = object.items[1];
});
forceSynchronousNotifications(testRealm);
});

expect(getByTestId(`favoriteItem-${object.items[1].name}`)).toBeTruthy();
Expand All @@ -299,6 +301,7 @@ describe("useObject: rendering objects with a Realm.List property", () => {
testRealm.write(() => {
object.items[1].name = "apple";
});
forceSynchronousNotifications(testRealm);
});

expect(getByTestId(`favoriteItem-${object.items[1].name}`)).toBeTruthy();
Expand Down
13 changes: 10 additions & 3 deletions packages/realm-react/src/__tests__/useQueryHook.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ const useRealm = () => {

const useQuery = createUseQuery(useRealm);

const awaitEventLoop = () => new Promise((resolve) => setTimeout(resolve, 0));

const testDataSet = [
{ _id: 1, name: "Vincent", color: "black and white", gender: "male", age: 4 },
{ _id: 2, name: "River", color: "brown", gender: "female", age: 12 },
Expand All @@ -78,11 +80,14 @@ describe("useQueryHook", () => {
});
realm.close();
});

afterEach(() => {
Realm.clearTestState();
});
it("can retrieve collections using useQuery", () => {

it("can retrieve collections using useQuery", async () => {
const { result } = renderHook(() => useQuery<IDog>("dog"));
await awaitEventLoop();
const collection = result.current;

const [dog1, dog2, dog3] = testDataSet;
Expand All @@ -93,16 +98,18 @@ describe("useQueryHook", () => {
expect(collection[1]).toMatchObject(dog2);
expect(collection[2]).toMatchObject(dog3);
});
it("returns the same collection reference if there are no changes", () => {
it("returns the same collection reference if there are no changes", async () => {
const { result } = renderHook(() => useQuery<IDog>("dog"));
await awaitEventLoop();
const collection = result.current;

expect(collection).not.toBeNull();
expect(collection.length).toBe(6);
expect(collection[0]).toEqual(collection?.[0]);
});
it("should return undefined indexes that are out of bounds", () => {
it("should return undefined indexes that are out of bounds", async () => {
const { result } = renderHook(() => useQuery<IDog>("dog"));
await awaitEventLoop();
const collection = result.current;

expect(collection[99]).toBe(undefined);
Expand Down
61 changes: 40 additions & 21 deletions packages/realm-react/src/__tests__/useQueryRender.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ enum QueryType {
normal,
}

const App = ({ queryType = QueryType.normal }) => {
const App = ({ queryType = QueryType.normal, useUseObject = false }) => {
return (
<RealmProvider>
<SetupComponent>
<View testID="testContainer">
<TestComponent queryType={queryType} />
<TestComponent queryType={queryType} useUseObject={useUseObject} />
</View>
</SetupComponent>
</RealmProvider>
Expand All @@ -113,13 +113,20 @@ const SetupComponent = ({ children }: { children: JSX.Element }): JSX.Element |
return children;
};

const UseObjectItemComponent: React.FC<{ item: Item & Realm.Object }> = React.memo(({ item }) => {
// Testing that useObject also works and properly handles renders
const localItem = useObject(Item, item.id);
if (!localItem) {
return null;
}
return <ItemComponent item={localItem}></ItemComponent>;
});

const ItemComponent: React.FC<{ item: Item & Realm.Object }> = React.memo(({ item }) => {
itemRenderCounter();
const realm = useRealm();
const renderItem = useCallback(({ item }) => <TagComponent tag={item} />, []);

const keyExtractor = useCallback((item) => `tag-${item.id}`, []);
const localItem = useObject(Item, item.id);

return (
<View testID={`result${item.id}`}>
Expand Down Expand Up @@ -170,7 +177,7 @@ const TagComponent: React.FC<{ tag: Tag & Realm.Object }> = React.memo(({ tag })
const FILTER_ARGS: [string] = ["id < 20"];
const SORTED_ARGS: [string, boolean] = ["id", true];

const TestComponent = ({ queryType }: { queryType: QueryType }) => {
const TestComponent = ({ queryType, useUseObject }: { queryType: QueryType; useUseObject: boolean }) => {
const collection = useQuery(Item);

const result = useMemo(() => {
Expand All @@ -184,7 +191,10 @@ const TestComponent = ({ queryType }: { queryType: QueryType }) => {
}
}, [queryType, collection]);

const renderItem = useCallback(({ item }) => <ItemComponent item={item} />, []);
const renderItem = useCallback(
({ item }) => (useUseObject ? <UseObjectItemComponent item={item} /> : <ItemComponent item={item} />),
[useUseObject],
);

const keyExtractor = useCallback((item) => item.id, []);

Expand All @@ -202,20 +212,25 @@ function getTestCollection(queryType: QueryType) {
}
}

async function setupTest(queryType: QueryType) {
const renderResult = render(<App queryType={queryType} />);
type setupOptions = {
queryType?: QueryType;
useUseObject?: boolean;
};

const setupTest = async ({ queryType = QueryType.normal, useUseObject = false }: setupOptions) => {
const renderResult = render(<App queryType={queryType} useUseObject={useUseObject} />);
await waitFor(() => renderResult.getByTestId("testContainer"));

const collection = getTestCollection(queryType);
expect(itemRenderCounter).toHaveBeenCalledTimes(10);

return { ...renderResult, collection };
}
};

describe.each`
queryTypeName | queryType
${"filtered"} | ${QueryType.filtered}
${"normal"} | ${QueryType.normal}
${"filtered"} | ${QueryType.filtered}
${"sorted"} | ${QueryType.sorted}
`("useQueryRender: $queryTypeName", ({ queryType }) => {
beforeEach(() => {
Expand All @@ -235,7 +250,7 @@ describe.each`
expect(itemRenderCounter).toHaveBeenCalledTimes(10);
});
it("change to data will rerender", async () => {
const { getByTestId, getByText, collection } = await setupTest(queryType);
const { getByTestId, getByText, collection } = await setupTest({ queryType });

const firstItem = collection[0];
const id = firstItem.id;
Expand All @@ -247,9 +262,10 @@ describe.each`

fireEvent.changeText(input as ReactTestInstance, "apple");

// TODO: This line throws an `act` warning. Keep an eye on this issue and see if there's a solution
// https://github.com/callstack/react-native-testing-library/issues/379
await waitFor(() => getByText("apple"));
// Wait for change events to finish their callbacks
await act(async () => {
forceSynchronousNotifications(testRealm);
});

expect(nameElement).toHaveTextContent("apple");
expect(itemRenderCounter).toHaveBeenCalledTimes(11);
Expand All @@ -258,7 +274,7 @@ describe.each`
// TODO: This is a known issue that we have to live with until it is possible
// to retrieve the objectId from a deleted object in a listener callback
it.skip("handles deletions", async () => {
const { getByTestId, collection } = await setupTest(queryType);
const { getByTestId, collection } = await setupTest({ queryType });

const firstItem = collection[0];
const id = firstItem.id;
Expand All @@ -278,7 +294,7 @@ describe.each`
expect(itemRenderCounter).toHaveBeenCalledTimes(11);
});
it("an implicit update to an item in the FlatList view area causes a rerender", async () => {
const { collection } = await setupTest(queryType);
const { collection } = await setupTest({ queryType });

testRealm.write(() => {
collection[0].name = "apple";
Expand All @@ -293,7 +309,7 @@ describe.each`
});

it("does not rerender if the update is out of the FlatList view area", async () => {
const { collection } = await setupTest(queryType);
const { collection } = await setupTest({ queryType });

testRealm.write(() => {
const lastIndex = collection.length - 1;
Expand All @@ -307,7 +323,7 @@ describe.each`
expect(itemRenderCounter).toHaveBeenCalledTimes(10);
});
it("collection objects rerender on changes to their linked objects", async () => {
const { collection, getByText, queryByText, debug } = await setupTest(queryType);
const { collection, getByText, queryByText } = await setupTest({ queryType });

// Insert some tags into visible Items
testRealm.write(() => {
Expand Down Expand Up @@ -355,8 +371,9 @@ describe.each`

expect(queryByText("756c")).toBeNull();
});
it.only("will handle multiple async transactions", async () => {
const { getByTestId } = await setupTest(queryType);
// This replicates the issue https://github.com/realm/realm-js/issues/4375
it("will handle multiple async transactions", async () => {
const { queryByTestId } = await setupTest({ queryType, useUseObject: true });
const asyncEffect = async () => {
testRealm.write(() => {
testRealm.deleteAll();
Expand All @@ -377,9 +394,11 @@ describe.each`
i++;
}
};

await act(async () => {
await asyncEffect();
});
await waitFor(() => getByTestId(`name${109}`));

await waitFor(() => queryByTestId(`name${109}`));
});
});
5 changes: 4 additions & 1 deletion packages/realm-react/src/cachedCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ export function createCachedCollection<T extends Realm.Object>({
};

if (!isDerived) {
cachedCollectionResult.addListener(listenerCallback);
// Add this on the next tick, in case there is a write transaction occuring immediately after creation of this collection
setImmediate(() => {
cachedCollectionResult.addListener(listenerCallback);
});
}

const tearDown = () => {
Expand Down
8 changes: 7 additions & 1 deletion packages/realm-react/src/cachedObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,13 @@ export function createCachedObject<T extends Realm.Object>({
}
};

object.addListener(listenerCallback);
// Add this on the next tick, in case there is a write transaction occuring immediately after creation of this object
// see https://github.com/realm/realm-js/issues/4375
setImmediate(() => {
if (object.isValid()) {
object.addListener(listenerCallback);
}
});

const tearDown = () => {
object.removeListener(listenerCallback);
Expand Down

0 comments on commit 61e8e2a

Please sign in to comment.