Skip to content

Commit d21a39a

Browse files
authored
Merge pull request #96 from MattCCC/fix-response-parser
Improve response parser and retry logic
2 parents dc6387c + d7608a1 commit d21a39a

File tree

3 files changed

+171
-3
lines changed

3 files changed

+171
-3
lines changed

src/response-parser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ export async function parseResponseData<
3333
>(
3434
response: FetchResponse<ResponseData, RequestBody, QueryParams, PathParams>,
3535
): Promise<any> {
36-
// Bail early for HEAD requests or status codes, or any requests that never have a body
37-
if (!response || !response.body) {
36+
// Bail early if response is null or undefined
37+
if (!response) {
3838
return null;
3939
}
4040

src/retry-handler.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,10 @@ export async function withRetry<
140140
}
141141
}
142142

143-
output = await requestFn(true, attempt); // isStaleRevalidation=false, isFirstAttempt=attempt===0
143+
// Performance optimization: Call the request function with the current attempt number
144+
// If this is the first attempt, we pass `isStaleRevalidation` as `false`,
145+
// otherwise we pass `true` to indicate that this is a stale revalidation (no cache hit).
146+
output = await requestFn(attempt > 0, attempt);
144147
const error = output.error;
145148

146149
// Check if we should retry based on successful response
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
import '@testing-library/jest-dom';
5+
import { useState } from 'react';
6+
import {
7+
render,
8+
screen,
9+
fireEvent,
10+
waitFor,
11+
act,
12+
} from '@testing-library/react';
13+
import { mockFetchResponse } from '../../utils/mockFetchResponse';
14+
import { useFetcher } from '../../../src/react/index';
15+
16+
describe('POST body cache key update and refetch', () => {
17+
it('should not update cache key but use new body when refetch is called after body changes and retries are active', async () => {
18+
mockFetchResponse('/api/user', {
19+
ok: true,
20+
status: 200,
21+
body: { echoed: { name: 'Alice' } },
22+
});
23+
24+
function TestComponent() {
25+
const [name, setName] = useState('Alice');
26+
const { data, refetch, isLoading, isFetching, config } = useFetcher(
27+
'/api/user',
28+
{
29+
method: 'POST',
30+
body: { name },
31+
cacheKey: '/api/user',
32+
immediate: true,
33+
retry: { retries: 5, delay: 1000, backoff: 2 },
34+
},
35+
);
36+
37+
return (
38+
<div>
39+
<div data-testid="result">{data?.echoed?.name}</div>
40+
<div data-testid="config">{JSON.stringify(config)}</div>
41+
<div data-testid="loading">{isLoading ? 'loading' : 'idle'}</div>
42+
<div data-testid="fetching">{isFetching ? 'fetching' : 'idle'}</div>
43+
<button onClick={() => setName('Bob')}>Change Name</button>
44+
<button onClick={() => refetch()}>Refetch</button>
45+
</div>
46+
);
47+
}
48+
49+
render(<TestComponent />);
50+
51+
// Initial fetch with name "Alice"
52+
await waitFor(() =>
53+
expect(screen.getByTestId('result').textContent).toBe('Alice'),
54+
);
55+
56+
mockFetchResponse('/api/user', {
57+
ok: true,
58+
status: 200,
59+
body: { echoed: { name: 'Bob' } },
60+
});
61+
62+
// Change name to "Bob"
63+
fireEvent.click(screen.getByText('Change Name'));
64+
65+
// Refetch with new body
66+
fireEvent.click(screen.getByText('Refetch'));
67+
68+
// Should show loading state
69+
expect(screen.getByTestId('loading').textContent).toBe('loading');
70+
expect(screen.getByTestId('fetching').textContent).toBe('fetching');
71+
72+
// Wait for fetch to complete and check new body is used
73+
await waitFor(() =>
74+
expect(screen.getByTestId('result').textContent).toBe('Bob'),
75+
);
76+
77+
// Should show idle state after fetch completes
78+
expect(screen.getByTestId('loading').textContent).toBe('idle');
79+
expect(screen.getByTestId('fetching').textContent).toBe('idle');
80+
81+
// Check if the body used in fetch is updated
82+
expect(screen.getByTestId('config').textContent).toContain(
83+
'"body":"{\\"name\\":\\"Bob\\"}"',
84+
);
85+
86+
// Check the cache key is updated
87+
expect(screen.getByTestId('config').textContent).toContain(
88+
'"cacheKey":"/api/user"',
89+
);
90+
});
91+
92+
it('should regenerate cache key and use updated body when POST body changes and refetch is called', async () => {
93+
const testUrl = '/api/post-body-cache-key';
94+
const initialBody = { value: 'first' };
95+
const updatedBody = { value: 'second' };
96+
let currentBody = initialBody;
97+
98+
// Mock fetch to echo back the request body
99+
global.fetch = jest.fn().mockImplementation((_url, config) => {
100+
const parsedBody =
101+
config && config.body ? JSON.parse(config.body) : undefined;
102+
return Promise.resolve({
103+
ok: true,
104+
status: 200,
105+
data: parsedBody,
106+
body: parsedBody,
107+
json: () => Promise.resolve(parsedBody),
108+
});
109+
});
110+
111+
// React state simulation
112+
let setBody: (b: typeof initialBody) => void = () => {};
113+
function BodyComponent() {
114+
const [body, _setBody] = useState(currentBody);
115+
setBody = _setBody;
116+
const { data, refetch, isLoading } = useFetcher(testUrl, {
117+
method: 'POST',
118+
body,
119+
});
120+
return (
121+
<div>
122+
<div data-testid="data">
123+
{data ? JSON.stringify(data) : 'No Data'}
124+
</div>
125+
<div data-testid="loading">
126+
{isLoading ? 'Loading...' : 'Not Loading'}
127+
</div>
128+
<button data-testid="refetch-btn" onClick={() => refetch(true)}>
129+
Refetch
130+
</button>
131+
</div>
132+
);
133+
}
134+
135+
render(<BodyComponent />);
136+
137+
// Wait for initial fetch
138+
await waitFor(() => {
139+
expect(screen.getByTestId('data')).toHaveTextContent('No Data');
140+
});
141+
142+
// Act: update the body asynchronously
143+
act(() => {
144+
currentBody = updatedBody;
145+
setBody(updatedBody);
146+
});
147+
148+
// Refetch with new body
149+
fireEvent.click(screen.getByTestId('refetch-btn'));
150+
151+
// Assert: data should match updated body
152+
await waitFor(() => {
153+
expect(screen.getByTestId('data')).toHaveTextContent('second');
154+
});
155+
156+
// Also check that fetch was called with the updated body
157+
expect(global.fetch).toHaveBeenLastCalledWith(
158+
testUrl,
159+
expect.objectContaining({
160+
method: 'POST',
161+
body: JSON.stringify(updatedBody),
162+
}),
163+
);
164+
});
165+
});

0 commit comments

Comments
 (0)