Skip to content

Commit f3ba70c

Browse files
authored
Merge pull request #219 from atomic-state/enhancements/decoupled-fetch-state
Enhancements/decoupled fetch state
2 parents b3cf4c1 + 3ab2abe commit f3ba70c

7 files changed

Lines changed: 127 additions & 172 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "http-react",
3-
"version": "3.8.8",
3+
"version": "3.9.0",
44
"description": "React hooks for data fetching",
55
"main": "dist/index.js",
66
"scripts": {

src/hooks/use-fetch.ts

Lines changed: 69 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import {
6464
notNull,
6565
queue,
6666
serialize,
67+
setQueryParams,
6768
setURLParams,
6869
windowExists
6970
} from '../utils/shared'
@@ -325,61 +326,6 @@ export function useFetch<
325326
)
326327
)
327328

328-
const setData = useCallback((v: any) => {
329-
setFetchState(p => {
330-
if (isFunction(v)) {
331-
const newVal = v(p.data)
332-
if (!jsonCompare(p.data, newVal)) {
333-
return {
334-
...p,
335-
data: newVal
336-
}
337-
}
338-
} else {
339-
if (!jsonCompare(p.data, v)) {
340-
return {
341-
...p,
342-
data: v
343-
}
344-
}
345-
}
346-
return p
347-
})
348-
}, [])
349-
350-
// This helps pass default values to other useFetch calls using the same id
351-
useEffect(() => {
352-
if (isDefined(optionsConfig.default)) {
353-
if (!fetcherDefaults.has(resolvedKey)) {
354-
if (url !== '') {
355-
if (!isDefined(cacheProvider.get(resolvedDataKey))) {
356-
fetcherDefaults.set(resolvedKey, optionsConfig.default)
357-
}
358-
} else {
359-
if (!isDefined(cacheProvider.get(resolvedDataKey))) {
360-
requestsProvider.emit(resolvedKey, {
361-
requestCallId,
362-
data: optionsConfig.default
363-
})
364-
}
365-
}
366-
}
367-
} else {
368-
if (fetcherDefaults.has(resolvedKey)) {
369-
if (!isDefined(cacheProvider.get(resolvedDataKey))) {
370-
setData(fetcherDefaults.get(resolvedKey))
371-
}
372-
}
373-
}
374-
}, [
375-
resolvedKey,
376-
resolvedDataKey,
377-
optionsConfig.default,
378-
url,
379-
requestCallId,
380-
setData
381-
])
382-
383329
const def = optionsConfig?.default ?? fetcherDefaults.get(resolvedKey)
384330

385331
useEffect(() => {
@@ -397,24 +343,28 @@ export function useFetch<
397343

398344
const hasInitialOrFallbackData = isDefined(initialDataValue)
399345

400-
const [fetchState, setFetchState] = useState({
401-
data: initialDataValue,
402-
online: true,
403-
loading: auto
346+
const [data, setData] = useState(initialDataValue)
347+
const [online, setOnline] = useState(true)
348+
const [loading, setLoading] = useState(
349+
auto
404350
? isPending(resolvedKey) ||
405-
(revalidateOnMount
406-
? !jsonCompare(
407-
JSON.parse(previousConfig.get(resolvedKey) || '{}'),
408-
optionsConfig
409-
)
410-
: !jsonCompare(
411-
JSON.parse(previousConfig.get(resolvedKey) || '{}'),
412-
optionsConfig
413-
))
414-
: false,
415-
error: (hasErrors.get(resolvedDataKey) || false) as boolean,
416-
completedAttempts: 0
417-
})
351+
(revalidateOnMount
352+
? !jsonCompare(
353+
JSON.parse(previousConfig.get(resolvedKey) || '{}'),
354+
optionsConfig
355+
)
356+
: !jsonCompare(
357+
JSON.parse(previousConfig.get(resolvedKey) || '{}'),
358+
optionsConfig
359+
))
360+
: false
361+
)
362+
363+
const [error, setError] = useState(
364+
(hasErrors.get(resolvedDataKey) || false) as boolean
365+
)
366+
367+
const [completedAttempts, setCompletedAttempts] = useState(0)
418368

419369
const thisDeps = useRef({
420370
data: false,
@@ -428,8 +378,6 @@ export function useFetch<
428378
return thisDeps[k]
429379
}
430380

431-
const { data, loading, online, error, completedAttempts } = fetchState
432-
433381
const thisCache =
434382
cacheProvider.get(resolvedDataKey) ??
435383
cacheProvider.get(resolvedKey) ??
@@ -442,93 +390,11 @@ export function useFetch<
442390
const loadingFirst =
443391
!(hasData.get(resolvedDataKey) || hasData.get(resolvedKey)) && isLoading
444392

445-
const setOnline = useCallback((v: any) => {
446-
setFetchState(p => {
447-
if (isFunction(v)) {
448-
const newVal = v(p.online)
449-
if (newVal !== p.online) {
450-
return { ...p, online: newVal }
451-
}
452-
} else {
453-
if (v !== p.online) {
454-
return { ...p, online: v }
455-
}
456-
}
457-
return p
458-
})
459-
}, [])
460-
461393
const requestHeaders = {
462394
...ctx.headers,
463395
...config.headers
464396
}
465397

466-
const setError = useCallback((v: any) => {
467-
setFetchState(p => {
468-
if (isFunction(v)) {
469-
const newErroValue = v(p.error)
470-
if (newErroValue !== p.error) {
471-
return {
472-
...p,
473-
error: newErroValue
474-
}
475-
}
476-
} else {
477-
if (v !== p.error) {
478-
return {
479-
...p,
480-
error: v
481-
}
482-
}
483-
}
484-
return p
485-
})
486-
}, [])
487-
488-
const setLoading = useCallback((v: any) => {
489-
setFetchState(p => {
490-
if (isFunction(v)) {
491-
const newLoadingValue = v(p.loading)
492-
if (newLoadingValue !== p.loading) {
493-
return {
494-
...p,
495-
loading: newLoadingValue
496-
}
497-
}
498-
} else {
499-
if (v !== p.loading) {
500-
return {
501-
...p,
502-
loading: v
503-
}
504-
}
505-
}
506-
return p
507-
})
508-
}, [])
509-
510-
const setCompletedAttempts = useCallback((v: any) => {
511-
setFetchState(p => {
512-
if (isFunction(v)) {
513-
const newCompletedAttempts = v(p.completedAttempts)
514-
if (newCompletedAttempts !== p.completedAttempts) {
515-
return {
516-
...p,
517-
completedAttempts: newCompletedAttempts
518-
}
519-
}
520-
} else {
521-
if (v !== p.completedAttempts) {
522-
return {
523-
...p,
524-
completedAttempts: v
525-
}
526-
}
527-
}
528-
return p
529-
})
530-
}, [])
531-
532398
const requestAbortController: AbortController =
533399
abortControllers.get(resolvedKey) ?? new AbortController()
534400

@@ -795,7 +661,8 @@ export function useFetch<
795661
hasData.set(resolvedDataKey, false)
796662
hasData.set(resolvedKey, false)
797663
}
798-
setFetchState(previous => {
664+
665+
setData((previous: any) => {
799666
const newData = {
800667
...previous,
801668
variables: (optionsConfig as any)?.variables,
@@ -817,6 +684,7 @@ export function useFetch<
817684

818685
return previous
819686
})
687+
820688
if (handleError) {
821689
if (!resolvedOnErrorCalls.get(resolvedKey)) {
822690
resolvedOnErrorCalls.set(resolvedKey, actionError ?? true)
@@ -947,7 +815,8 @@ export function useFetch<
947815
}
948816
},
949817
[
950-
// No longer depends on data
818+
data,
819+
error,
951820
canRevalidate,
952821
ctx.auto,
953822
stringDeps,
@@ -959,11 +828,45 @@ export function useFetch<
959828
memory,
960829
def,
961830
loadingFirst,
831+
loading,
962832
setError,
963833
setLoading
964834
]
965835
)
966836

837+
// This helps pass default values to other useFetch calls using the same id
838+
useEffect(() => {
839+
if (isDefined(optionsConfig.default)) {
840+
if (!fetcherDefaults.has(resolvedKey)) {
841+
if (url !== '') {
842+
if (!isDefined(cacheProvider.get(resolvedDataKey))) {
843+
fetcherDefaults.set(resolvedKey, optionsConfig.default)
844+
}
845+
} else {
846+
if (!isDefined(cacheProvider.get(resolvedDataKey))) {
847+
requestsProvider.emit(resolvedKey, {
848+
requestCallId,
849+
data: optionsConfig.default
850+
})
851+
}
852+
}
853+
}
854+
} else {
855+
if (fetcherDefaults.has(resolvedKey)) {
856+
if (!isDefined(cacheProvider.get(resolvedDataKey))) {
857+
setData(fetcherDefaults.get(resolvedKey))
858+
}
859+
}
860+
}
861+
}, [
862+
resolvedKey,
863+
resolvedDataKey,
864+
optionsConfig.default,
865+
url,
866+
requestCallId,
867+
setData
868+
])
869+
967870
useEffect(() => {
968871
const { signal } = requestAbortController || {}
969872
// Run onAbort callback
@@ -1059,6 +962,7 @@ export function useFetch<
1059962
if (isMutating) {
1060963
if (!jsonCompare($data, cacheForMutation.get(resolvedKey))) {
1061964
cacheForMutation.set(idString, $data)
965+
forceMutate($data)
1062966

1063967
if (isMutating) {
1064968
if (handleMutate) {
@@ -1390,13 +1294,11 @@ export function useFetch<
13901294
} else {
13911295
d = def
13921296
// It means a url is not passed
1393-
setFetchState(prev => ({
1394-
...prev,
1395-
loading: false,
1396-
error:
1397-
hasErrors.get(resolvedDataKey) || hasErrors.get(resolvedKey),
1398-
completedAttempts: prev.completedAttempts
1399-
}))
1297+
1298+
setLoading(false)
1299+
setError(
1300+
hasErrors.get(resolvedDataKey) || hasErrors.get(resolvedKey)
1301+
)
14001302
}
14011303
} else {
14021304
d = def
@@ -1409,6 +1311,7 @@ export function useFetch<
14091311
})
14101312
},
14111313
[
1314+
data,
14121315
fetchData,
14131316
canRevalidate,
14141317
url,
@@ -1540,7 +1443,7 @@ Learn more: https://httpr.vercel.app/docs/api#suspense
15401443
},
15411444
body: config.body,
15421445
baseUrl: ctx.baseUrl || config.baseUrl,
1543-
url: configUrl?.realUrl?.replace('?', ''),
1446+
url: setQueryParams(configUrl?.realUrl?.replace('?', ''), query),
15441447
rawUrl: configUrl?.rawUrl,
15451448
query: {
15461449
...reqQuery,

test/json/config.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,22 @@ import useFetch from '../../'
33
import mocks from '../mocks'
44

55
test('Config is modified by AtomicState provider', async () => {
6-
global.fetch = jest.fn().mockImplementation((url, config) =>
6+
// 1. Define the mock fetch function
7+
const mockFetch = jest.fn().mockImplementation((url, config) =>
78
Promise.resolve({
89
json: () => mocks[config.method]
910
})
1011
)
1112

13+
// 2. Define a simple mock function for 'preconnect'
14+
const mockPreconnect = jest.fn()
15+
16+
// 3. Attach the 'preconnect' mock to the mock fetch function
17+
// The 'as typeof fetch' assertion is crucial for satisfying TypeScript
18+
global.fetch = Object.assign(mockFetch, {
19+
preconnect: mockPreconnect
20+
}) as typeof fetch
21+
1222
let r: any
1323

1424
await act(async () => {
@@ -26,6 +36,7 @@ test('Config is modified by AtomicState provider', async () => {
2636

2737
r = result
2838
})
39+
2940
await waitFor(async () => {
3041
expect(r.current.config.baseUrl).toBe('test-url')
3142
})

0 commit comments

Comments
 (0)