Skip to content

Commit 1ec90e5

Browse files
authored
enhance: Experimental fetcher resolves before react render (#1046)
1 parent 7a63412 commit 1ec90e5

File tree

6 files changed

+114
-18
lines changed

6 files changed

+114
-18
lines changed

packages/core/src/state/NetworkManager.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,28 @@ export default class NetworkManager implements Manager {
9999
protected handleFetch(action: FetchAction, dispatch: Dispatch<any>) {
100100
const fetch = action.payload;
101101
const { key, throttle, resolve, reject } = action.meta;
102-
const deferedFetch = () =>
103-
fetch()
102+
103+
const deferedFetch = () => {
104+
let promise = fetch();
105+
const resolvePromise = (
106+
promise: Promise<string | number | object | null>,
107+
) =>
108+
promise
109+
.then(data => {
110+
resolve(data);
111+
return data;
112+
})
113+
.catch(error => {
114+
reject(error);
115+
throw error;
116+
});
117+
// schedule non-throttled resolutions in a microtask before receive
118+
// this enables users awaiting their fetch to trigger any react updates needed to deal
119+
// with upcoming changes because of the fetch (for instance avoiding suspense if something is deleted)
120+
if (!throttle && action.endpoint) {
121+
promise = resolvePromise(promise);
122+
}
123+
promise = promise
104124
.then(data => {
105125
// does this throw if the reducer fails?
106126
dispatch(
@@ -123,14 +143,20 @@ export default class NetworkManager implements Manager {
123143
);
124144
throw error;
125145
});
126-
let promise;
146+
// legacy behavior schedules resolution after dispatch
147+
if (!throttle && !action.endpoint) {
148+
promise = resolvePromise(promise);
149+
}
150+
return promise;
151+
};
152+
127153
if (throttle) {
128-
promise = this.throttle(key, deferedFetch);
154+
return this.throttle(key, deferedFetch)
155+
.then(data => resolve(data))
156+
.catch(error => reject(error));
129157
} else {
130-
promise = deferedFetch();
158+
return deferedFetch().catch(() => {});
131159
}
132-
promise.then(data => resolve(data)).catch(error => reject(error));
133-
return promise;
134160
}
135161

136162
/** Called when middleware intercepts a receive action.

packages/core/src/state/__tests__/networkManager.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ describe('NetworkManager', () => {
9292
body: { id: 5, title: 'hi' },
9393
throttle: false,
9494
});
95+
(fetchRejectAction.meta.promise as any).catch(e => {});
9596

9697
let NM: NetworkManager;
9798
let middleware: Middleware;
@@ -278,7 +279,10 @@ describe('NetworkManager', () => {
278279
...fetchRejectAction,
279280
meta: {
280281
...fetchRejectAction.meta,
281-
options: { errorExpiryLength: undefined },
282+
options: {
283+
...fetchRejectAction.meta.options,
284+
errorExpiryLength: undefined,
285+
},
282286
},
283287
});
284288
} catch (error) {

packages/core/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
Schema,
88
FetchFunction,
99
EndpointExtraOptions,
10+
EndpointInterface,
1011
} from '@rest-hooks/endpoint';
1112
import { ErrorableFSAWithPayloadAndMeta } from '@rest-hooks/core/fsa';
1213
import { FetchShape } from '@rest-hooks/core/endpoint/index';
@@ -130,6 +131,7 @@ export interface FetchAction<
130131
FetchMeta<any, any>
131132
> {
132133
meta: FetchMeta<Payload, S>;
134+
endpoint?: EndpointInterface;
133135
}
134136

135137
export interface SubscribeAction

packages/experimental/src/__tests__/useFetcher.ts

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import nock from 'nock';
33
import { useCache, useResource, ResolveType } from '@rest-hooks/core';
44

55
// relative imports to avoid circular dependency in tsconfig references
6+
import { act } from '@testing-library/react-hooks';
7+
68
import {
79
makeRenderRestHook,
810
makeCacheProvider,
@@ -102,10 +104,13 @@ for (const makeProvider of [makeCacheProvider, makeExternalCacheProvider]) {
102104
};
103105
});
104106
expect(result.current.data).toBeUndefined();
105-
const response = await result.current.fetch(
106-
FutureArticleResource.detail(),
107-
payload.id,
108-
);
107+
let response;
108+
await act(async () => {
109+
response = await result.current.fetch(
110+
FutureArticleResource.detail(),
111+
payload.id,
112+
);
113+
});
109114
expect(result.current.data).toBeDefined();
110115
expect(result.current.data?.content).toEqual(payload.content);
111116
expect(response).toEqual(payload);
@@ -153,11 +158,64 @@ for (const makeProvider of [makeCacheProvider, makeExternalCacheProvider]) {
153158
{ resolverFixtures },
154159
);
155160
await waitForNextUpdate();
156-
await result.current.fetch(FutureArticleResource.create(), {
157-
id: 1,
161+
await act(async () => {
162+
await result.current.fetch(FutureArticleResource.create(), {
163+
id: 1,
164+
});
158165
});
166+
159167
expect(result.current.articles.map(({ id }) => id)).toEqual([1, 5, 3]);
160168
}
161169
});
170+
171+
it('should not suspend once deleted and redirected at same time', async () => {
172+
const temppayload = {
173+
...payload,
174+
id: 1234,
175+
};
176+
mynock
177+
.get(`/article-cooler/${temppayload.id}`)
178+
.reply(200, temppayload)
179+
.delete(`/article-cooler/${temppayload.id}`)
180+
.reply(204, '');
181+
const throws: Promise<any>[] = [];
182+
const { result, waitForNextUpdate, rerender } = renderRestHook(
183+
({ id }) => {
184+
try {
185+
return [
186+
useResource(FutureArticleResource.detail(), id) ?? null,
187+
useFetcher(),
188+
] as const;
189+
} catch (e) {
190+
if (typeof e.then === 'function') {
191+
if (e !== throws[throws.length - 1]) {
192+
throws.push(e);
193+
}
194+
}
195+
throw e;
196+
}
197+
},
198+
{ initialProps: { id: temppayload.id as number | null } },
199+
);
200+
expect(result.current).toBeUndefined();
201+
await waitForNextUpdate();
202+
const [data, fetch] = result.current;
203+
expect(data).toBeInstanceOf(FutureArticleResource);
204+
expect(data?.title).toBe(temppayload.title);
205+
expect(throws.length).toBe(1);
206+
207+
mynock
208+
.persist()
209+
.get(`/article-cooler/${temppayload.id}`)
210+
.reply(200, { ...temppayload, title: 'othertitle' });
211+
212+
expect(throws.length).toBe(1);
213+
await act(async () => {
214+
await fetch(FutureArticleResource.delete(), temppayload.id);
215+
rerender({ id: null });
216+
});
217+
expect(throws.length).toBe(1);
218+
expect(result.current[0]).toBe(null);
219+
});
162220
});
163221
}

packages/experimental/src/createFetch.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export default function createFetch<
4747
type: actionTypes.FETCH_TYPE,
4848
payload: () => endpoint(...args),
4949
meta,
50+
endpoint,
5051
};
5152
}
5253

packages/experimental/src/rest/__tests__/Resource.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import nock from 'nock';
22
import { useResource } from '@rest-hooks/core';
3+
import { act } from '@testing-library/react-hooks';
34

45
import Resource from '../Resource';
56
import useFetcher from '../../useFetcher';
@@ -124,8 +125,10 @@ describe('Resource', () => {
124125
return { articles, nextPage, fetch };
125126
});
126127
await waitForNextUpdate();
127-
await result.current.fetch(PaginatedArticleResource.listPage(), {
128-
cursor: 2,
128+
await act(async () => {
129+
await result.current.fetch(PaginatedArticleResource.listPage(), {
130+
cursor: 2,
131+
});
129132
});
130133
expect(
131134
result.current.articles.map(
@@ -180,8 +183,10 @@ describe('Resource', () => {
180183
};
181184

182185
mynock.get(`/complex-thing/5`).reply(200, secondResponse);
183-
await result.current.fetch(ComplexResource.detail(), {
184-
id: '5',
186+
await act(async () => {
187+
await result.current.fetch(ComplexResource.detail(), {
188+
id: '5',
189+
});
185190
});
186191
expect(result.current.article).toEqual({ ...secondResponse, extra: 'hi' });
187192
});

0 commit comments

Comments
 (0)