Skip to content

Commit c235885

Browse files
authored
Merge pull request #274 from gadget-inc/paused-hooks
Ensure hook pausing with urql works
2 parents ddc7ba1 + f12b850 commit c235885

File tree

7 files changed

+214
-5
lines changed

7 files changed

+214
-5
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import type { GadgetRecord } from "@gadgetinc/api-client-core";
2+
import { renderHook } from "@testing-library/react";
3+
import type { IsExact } from "conditional-type-checks";
4+
import { assert } from "conditional-type-checks";
5+
import { useFindFirst } from "../src/index.js";
6+
import type { ErrorWrapper } from "../src/utils.js";
7+
import { relatedProductsApi } from "./apis.js";
8+
import { MockClientWrapper, mockUrqlClient } from "./testWrappers.js";
9+
10+
describe("useFindFirst", () => {
11+
// these functions are typechecked but never run to avoid actually making API calls
12+
const _TestFindFirstReturnsTypedDataWithExplicitSelection = () => {
13+
const [{ data, fetching, error }, refresh] = useFindFirst(relatedProductsApi.user, {
14+
filter: { email: { equals: "[email protected]" } },
15+
select: { id: true, email: true },
16+
});
17+
18+
assert<IsExact<typeof fetching, boolean>>(true);
19+
assert<IsExact<typeof data, undefined | GadgetRecord<{ id: string; email: string | null }>>>(true);
20+
assert<IsExact<typeof error, ErrorWrapper | undefined>>(true);
21+
22+
// data is accessible via dot access
23+
if (data) {
24+
data.id;
25+
data.email;
26+
}
27+
28+
// hook return value includes the urql refresh function
29+
refresh();
30+
};
31+
32+
const _TestFindFirstReturnsTypedDataWithNoSelection = () => {
33+
const [{ data }] = useFindFirst(relatedProductsApi.user);
34+
35+
if (data) {
36+
data.id;
37+
data.email;
38+
}
39+
};
40+
41+
test("it can find the first record", async () => {
42+
const { result } = renderHook(() => useFindFirst(relatedProductsApi.user), {
43+
wrapper: MockClientWrapper(relatedProductsApi),
44+
});
45+
46+
expect(result.current[0].data).toBeFalsy();
47+
expect(result.current[0].fetching).toBe(true);
48+
expect(result.current[0].error).toBeFalsy();
49+
50+
expect(mockUrqlClient.executeQuery).toBeCalledTimes(1);
51+
52+
mockUrqlClient.executeQuery.pushResponse("users", {
53+
data: {
54+
users: {
55+
edges: [{ cursor: "123", node: { id: "123", email: "[email protected]" } }],
56+
pageInfo: {
57+
startCursor: "123",
58+
endCursor: "123",
59+
hasNextPage: false,
60+
hasPreviousPage: false,
61+
},
62+
},
63+
},
64+
stale: false,
65+
hasNext: false,
66+
});
67+
68+
expect(result.current[0].data!.id).toEqual("123");
69+
expect(result.current[0].data!.email).toEqual("[email protected]");
70+
expect(result.current[0].fetching).toBe(false);
71+
expect(result.current[0].error).toBeFalsy();
72+
});
73+
74+
test("it return null if the record with the given field value can't be found", async () => {
75+
const { result } = renderHook(() => useFindFirst(relatedProductsApi.user), {
76+
wrapper: MockClientWrapper(relatedProductsApi),
77+
});
78+
79+
expect(result.current[0].data).toBeFalsy();
80+
expect(result.current[0].fetching).toBe(true);
81+
expect(result.current[0].error).toBeFalsy();
82+
83+
expect(mockUrqlClient.executeQuery).toBeCalledTimes(1);
84+
85+
mockUrqlClient.executeQuery.pushResponse("users", {
86+
data: {
87+
users: {
88+
edges: [],
89+
pageInfo: {
90+
startCursor: null,
91+
endCursor: null,
92+
hasNextPage: false,
93+
hasPreviousPage: false,
94+
},
95+
},
96+
},
97+
stale: false,
98+
hasNext: false,
99+
});
100+
101+
expect(result.current[0].data).toBeFalsy();
102+
expect(result.current[0].fetching).toBe(false);
103+
expect(result.current[0].error).toBeUndefined();
104+
});
105+
106+
test("returns the same data object on rerender", async () => {
107+
const { result, rerender } = renderHook(() => useFindFirst(relatedProductsApi.user), {
108+
wrapper: MockClientWrapper(relatedProductsApi),
109+
});
110+
111+
expect(mockUrqlClient.executeQuery).toBeCalledTimes(1);
112+
113+
mockUrqlClient.executeQuery.pushResponse("users", {
114+
data: {
115+
users: {
116+
edges: [{ cursor: "123", node: { id: "123", email: "[email protected]" } }],
117+
pageInfo: {
118+
startCursor: "123",
119+
endCursor: "123",
120+
hasNextPage: false,
121+
hasPreviousPage: false,
122+
},
123+
},
124+
},
125+
stale: false,
126+
hasNext: false,
127+
});
128+
129+
const data = result.current[0].data;
130+
expect(data).toBeTruthy();
131+
132+
rerender();
133+
134+
expect(result.current[0].data).toBe(data);
135+
});
136+
137+
test("it can suspend when finding data", async () => {
138+
const { result, rerender } = renderHook(() => useFindFirst(relatedProductsApi.user, { suspense: true }), {
139+
wrapper: MockClientWrapper(relatedProductsApi),
140+
});
141+
142+
// first render never completes as the component suspends
143+
expect(result.current).toBeFalsy();
144+
expect(mockUrqlClient.executeQuery).toBeCalledTimes(1);
145+
146+
mockUrqlClient.executeQuery.pushResponse("users", {
147+
data: {
148+
users: {
149+
edges: [{ cursor: "123", node: { id: "123", email: "[email protected]" } }],
150+
pageInfo: {
151+
startCursor: "123",
152+
endCursor: "123",
153+
hasNextPage: false,
154+
hasPreviousPage: false,
155+
},
156+
},
157+
},
158+
stale: false,
159+
hasNext: false,
160+
});
161+
162+
// rerender as react would do when the suspense promise resolves
163+
rerender();
164+
expect(result.current).toBeTruthy();
165+
expect(result.current[0].data!.id).toEqual("123");
166+
expect(result.current[0].data!.email).toEqual("[email protected]");
167+
expect(result.current[0].error).toBeFalsy();
168+
169+
const beforeObject = result.current[0];
170+
rerender();
171+
expect(result.current[0]).toBe(beforeObject);
172+
});
173+
174+
test("doesn't issue a request if paused", async () => {
175+
const { result } = renderHook(() => useFindFirst(relatedProductsApi.user, { pause: true }), {
176+
wrapper: MockClientWrapper(relatedProductsApi),
177+
});
178+
179+
expect(result.current[0].data).toBeFalsy();
180+
expect(result.current[0].fetching).toBe(false);
181+
expect(result.current[0].error).toBeFalsy();
182+
183+
expect(mockUrqlClient.executeQuery).toBeCalledTimes(0);
184+
});
185+
});

packages/react/spec/useFindMany.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,4 +254,16 @@ describe("useFindMany", () => {
254254
rerender();
255255
expect(result.current[0]).toBe(beforeObject);
256256
});
257+
258+
test("doesn't issue a request if paused", async () => {
259+
const { result } = renderHook(() => useFindMany(relatedProductsApi.user, { pause: true }), {
260+
wrapper: MockClientWrapper(relatedProductsApi),
261+
});
262+
263+
expect(result.current[0].data).toBeFalsy();
264+
expect(result.current[0].fetching).toBe(false);
265+
expect(result.current[0].error).toBeFalsy();
266+
267+
expect(mockUrqlClient.executeQuery).toBeCalledTimes(0);
268+
});
257269
});

packages/react/spec/useFindOne.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,16 @@ describe("useFindOne", () => {
152152
rerender();
153153
expect(result.current[0]).toBe(beforeObject);
154154
});
155+
156+
test("doesn't issue a request if paused", async () => {
157+
const { result } = renderHook(() => useFindOne(relatedProductsApi.user, "123", { pause: true }), {
158+
wrapper: MockClientWrapper(relatedProductsApi),
159+
});
160+
161+
expect(result.current[0].data).toBeFalsy();
162+
expect(result.current[0].fetching).toBe(false);
163+
expect(result.current[0].error).toBeFalsy();
164+
165+
expect(mockUrqlClient.executeQuery).toBeCalledTimes(0);
166+
});
155167
});

packages/react/src/useFindFirst.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export const useFindFirst = <
6666
}
6767
}
6868

69-
const error = ErrorWrapper.errorIfDataAbsent(rawResult, dataPath);
69+
const error = ErrorWrapper.errorIfDataAbsent(rawResult, dataPath, options?.pause);
7070

7171
return { ...rawResult, data, error };
7272
}, [manager, rawResult]);

packages/react/src/useFindMany.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export const useFindMany = <
6464
}
6565
}
6666

67-
const error = ErrorWrapper.errorIfDataAbsent(rawResult, dataPath);
67+
const error = ErrorWrapper.errorIfDataAbsent(rawResult, dataPath, options?.pause);
6868

6969
return { ...rawResult, data, error };
7070
}, [manager, rawResult]);

packages/react/src/useFindOne.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export const useFindOne = <
6161
if (data) {
6262
data = hydrateRecord(rawResult, data);
6363
}
64-
const error = ErrorWrapper.errorIfDataAbsent(rawResult, dataPath);
64+
const error = ErrorWrapper.errorIfDataAbsent(rawResult, dataPath, options?.pause);
6565

6666
return { ...rawResult, data, error };
6767
}, [rawResult, manager]);

packages/react/src/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,10 @@ export class ErrorWrapper extends Error {
180180
});
181181
}
182182
/** @private */
183-
static errorIfDataAbsent(result: UseQueryState<any>, dataPath: string[]) {
183+
static errorIfDataAbsent(result: UseQueryState<any>, dataPath: string[], paused = false) {
184184
const nonNullableError = getNonNullableError(result, dataPath);
185185
let error = ErrorWrapper.forMaybeCombinedError(result.error);
186-
if (!error && nonNullableError) {
186+
if (!error && nonNullableError && !paused) {
187187
error = ErrorWrapper.forClientSideError(nonNullableError);
188188
}
189189
return error;

0 commit comments

Comments
 (0)