Skip to content

Commit bb3ea77

Browse files
promer94ekkoxu
authored and
ekkoxu
committed
wip: useSWRAggregator
chroe: update
1 parent 8f0288b commit bb3ea77

15 files changed

+415
-10
lines changed

aggregator/index.tsx

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import React, { useRef, useMemo, memo } from 'react'
2+
import useSWR, { unstable_serialize } from 'swr'
3+
import {
4+
createCacheHelper,
5+
SWRHook,
6+
Middleware,
7+
withMiddleware,
8+
isUndefined,
9+
useIsomorphicLayoutEffect,
10+
mergeObjects,
11+
MutatorOptions,
12+
MutatorCallback,
13+
Arguments,
14+
RevalidatorOptions,
15+
SWRGlobalState,
16+
getTimestamp,
17+
GlobalState,
18+
BareFetcher,
19+
defaultConfig
20+
} from 'swr/_internal'
21+
22+
import type {
23+
SWRItemProps,
24+
SWRAggregatorConfiguration,
25+
SWRAggregator,
26+
SWRCollection
27+
} from './types'
28+
29+
const defaultChildren = () => {
30+
return null
31+
}
32+
33+
export const aggregator = (<Data, Error, Key extends Arguments = Arguments>(
34+
useSWRNext: SWRHook
35+
) =>
36+
(
37+
_keys: Key[],
38+
fetcher: BareFetcher<Data>,
39+
config: typeof defaultConfig & SWRAggregatorConfiguration<Data, Error, Key>
40+
) => {
41+
if (!Array.isArray(_keys)) throw new Error('not array')
42+
const { cache, compare, mutate: _internalMutate } = config
43+
const fetcherRef = useRef(fetcher)
44+
const configRef = useRef(config)
45+
const swrkeys = unstable_serialize(_keys)
46+
// eslint-disable-next-line react-hooks/exhaustive-deps
47+
const keys = useMemo(() => _keys.map(v => unstable_serialize(v)), [swrkeys])
48+
const cacheHelpers = useMemo(
49+
() =>
50+
keys.map(key => {
51+
const [get] = createCacheHelper<Data>(cache, key)
52+
return {
53+
get
54+
}
55+
}),
56+
[keys, cache]
57+
)
58+
const fetch = async (revalidateOpts?: RevalidatorOptions): Promise<any> => {
59+
const revalidate = async (index: number) => {
60+
let newData: Data
61+
let startAt: number
62+
const opts = revalidateOpts || {}
63+
const key = keys[index]
64+
const _key = _keys[index]
65+
const { get } = cacheHelpers[index]
66+
const [_, MUTATION, FETCH] = SWRGlobalState.get(cache) as GlobalState
67+
// If there is no ongoing concurrent request, or `dedupe` is not set, a
68+
// new request should be initiated.
69+
const shouldStartNewRequest = !FETCH[key] || !opts.dedupe
70+
71+
const cleanupState = () => {
72+
// Check if it's still the same request before deleting.
73+
const requestInfo = FETCH[key]
74+
if (requestInfo && requestInfo[1] === startAt) {
75+
delete FETCH[key]
76+
}
77+
}
78+
const currentFetcher = fetcherRef.current
79+
try {
80+
if (shouldStartNewRequest) {
81+
FETCH[key] = [currentFetcher(_key), getTimestamp()]
82+
}
83+
;[newData, startAt] = FETCH[key]
84+
newData = await newData
85+
86+
if (shouldStartNewRequest) {
87+
setTimeout(cleanupState, config.dedupingInterval)
88+
}
89+
const mutationInfo = MUTATION[key]
90+
if (
91+
!isUndefined(mutationInfo) &&
92+
// case 1
93+
(startAt <= mutationInfo[0] ||
94+
// case 2
95+
startAt <= mutationInfo[1] ||
96+
// case 3
97+
mutationInfo[1] === 0)
98+
) {
99+
return mergeObjects({}, { data: get().data, error: get().error })
100+
}
101+
if (!compare(newData, get().data)) {
102+
await _internalMutate(_key, newData, false)
103+
}
104+
// eslint-disable-next-line no-empty
105+
} catch {
106+
cleanupState()
107+
}
108+
return mergeObjects({}, { data: get().data, error: get().error })
109+
}
110+
return Promise.all(keys.map((___, i) => revalidate(i)))
111+
}
112+
const swr = useSWRNext(_keys, () => fetch({ dedupe: true }), config)
113+
const SWRAggregatorItem = useMemo(() => {
114+
const Component = memo(({ index }: { index: number }) => {
115+
const item = useSWR(_keys[index], async () => {
116+
const currentFetcher = fetcherRef.current
117+
const data = await currentFetcher(_keys[index])
118+
swr.mutate()
119+
return data
120+
})
121+
const children = configRef.current.children || defaultChildren
122+
return children(item, swr, index)
123+
})
124+
Component.displayName = 'SWRAggregatorItem'
125+
return Component
126+
// eslint-disable-next-line react-hooks/exhaustive-deps
127+
}, [keys, swr])
128+
const item = (key: any, index: number) => (
129+
<SWRAggregatorItem key={key} index={index} />
130+
)
131+
useIsomorphicLayoutEffect(() => {
132+
fetcherRef.current = fetcher
133+
configRef.current = config
134+
})
135+
return {
136+
items: keys.map(item),
137+
mutate: (
138+
data: Data[] | Promise<Data[]> | MutatorCallback<Data[]> = () =>
139+
fetch({ dedupe: false }),
140+
opt: boolean | MutatorOptions<Data[]> = false
141+
) => swr.mutate(data, opt),
142+
get data() {
143+
return swr.data?.map((v: any, i: number) =>
144+
mergeObjects(v, {
145+
key: keys[i],
146+
originKey: _keys[i]
147+
})
148+
)
149+
},
150+
get isLoading() {
151+
return swr.isLoading
152+
},
153+
get isValidating() {
154+
return swr.isValidating
155+
}
156+
}
157+
}) as unknown as Middleware
158+
159+
export default withMiddleware(useSWR, aggregator) as unknown as SWRAggregator
160+
161+
export {
162+
SWRItemProps,
163+
SWRAggregatorConfiguration,
164+
SWRAggregator,
165+
SWRCollection
166+
}

aggregator/package.json

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "swr-aggregator",
3+
"version": "0.0.1",
4+
"main": "./dist/index.js",
5+
"module": "./dist/index.esm.js",
6+
"types": "./dist/aggregator",
7+
"exports": "./dist/index.mjs",
8+
"private": true,
9+
"scripts": {
10+
"watch": "bunchee index.tsx --no-sourcemap -w",
11+
"build": "bunchee index.tsx --no-sourcemap",
12+
"types:check": "tsc --noEmit",
13+
"clean": "rimraf dist"
14+
},
15+
"peerDependencies": {
16+
"swr": "*",
17+
"react": "*",
18+
"use-sync-external-store": "*"
19+
}
20+
}

aggregator/tsconfig.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "../tsconfig.json",
3+
"compilerOptions": {
4+
"rootDir": "..",
5+
"outDir": "./dist"
6+
},
7+
"include": ["./*.tsx"]
8+
}

aggregator/types.ts

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
SWRConfiguration,
3+
Arguments,
4+
BareFetcher,
5+
KeyedMutator,
6+
Fetcher,
7+
SWRResponse
8+
} from 'swr/_internal'
9+
10+
export type Keys<T extends Arguments = Arguments> = T[]
11+
12+
export interface SWRCollection<
13+
Data = any,
14+
Error = any,
15+
Key extends Arguments = Arguments
16+
> {
17+
mutate: KeyedMutator<Data[]>
18+
data?: Array<{
19+
data?: Data
20+
error?: Error
21+
key: string
22+
originKey: Key
23+
}>
24+
isValidating: boolean
25+
isLoading: boolean
26+
}
27+
28+
export interface SWRItemProps<Data = any, Key extends Arguments = Arguments> {
29+
keys: Key[]
30+
fetcher: BareFetcher<Data>
31+
index: number
32+
}
33+
34+
export interface SWRAggregatorConfiguration<
35+
Data = any,
36+
Error = any,
37+
OriginKey extends Arguments = Arguments
38+
> extends SWRConfiguration<Data, Error> {
39+
children: (
40+
items: SWRResponse<Data, Error>,
41+
collection: SWRCollection<Data, Error, OriginKey>,
42+
index: number
43+
) => React.ReactElement<any, any> | null
44+
}
45+
46+
interface AggregatorResult<
47+
Data = any,
48+
Error = any,
49+
Key extends Arguments = Arguments
50+
> extends SWRCollection<Data, Error, Key> {
51+
items: Array<JSX.Element | null>
52+
}
53+
54+
export interface SWRAggregator {
55+
<Data = any, Error = any, Key extends Arguments = Arguments>(
56+
key: Keys<Key>
57+
): AggregatorResult<Data, Error>
58+
<Data = any, Error = any, Key extends Arguments = Arguments>(
59+
key: Keys<Key>,
60+
fetcher: Fetcher<Data, Key> | null
61+
): AggregatorResult<Data, Error>
62+
<Data = any, Error = any, Key extends Arguments = Arguments>(
63+
key: Keys<Key>,
64+
config: SWRAggregatorConfiguration<Data, Error, Key> | undefined
65+
): AggregatorResult<Data, Error>
66+
<Data = any, Error = any, Key extends Arguments = Arguments>(
67+
key: Keys<Key>,
68+
fetcher: Fetcher<Data, Key> | null,
69+
config: SWRAggregatorConfiguration<Data, Error, Key> | undefined
70+
): AggregatorResult<Data, Error>
71+
<Data = any, Error = any>(key: Keys): AggregatorResult<Data, Error>
72+
<Data = any, Error = any>(
73+
key: Keys,
74+
fetcher: BareFetcher<Data> | null
75+
): AggregatorResult<Data, Error>
76+
<Data = any, Error = any>(
77+
key: Keys,
78+
config: SWRAggregatorConfiguration<Data, Error> | undefined
79+
): AggregatorResult<Data, Error>
80+
<Data = any, Error = any>(
81+
key: Keys,
82+
fetcher: BareFetcher<Data> | null,
83+
config: SWRAggregatorConfiguration<Data, Error> | undefined
84+
): AggregatorResult<Data, Error>
85+
}

examples/aggregate/README.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Basic
2+
3+
## One-Click Deploy
4+
5+
Deploy your own SWR project with Vercel.
6+
7+
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/basic)
8+
9+
## How to Use
10+
11+
Download the example:
12+
13+
```bash
14+
curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/basic
15+
cd basic
16+
```
17+
18+
Install it and run:
19+
20+
```bash
21+
yarn
22+
yarn dev
23+
# or
24+
npm install
25+
npm run dev
26+
```
27+
28+
## The Idea behind the Example
29+
30+
Show a basic usage of SWR fetching data from an API in two different pages.

examples/aggregate/libs/fetch.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default async function fetcher(...args) {
2+
const res = await fetch(...args)
3+
return res.json()
4+
}

examples/aggregate/package.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "queries",
3+
"private": true,
4+
"license": "MIT",
5+
"dependencies": {
6+
"next": "latest",
7+
"react": "18.1.0",
8+
"react-dom": "18.1.0",
9+
"swr": "latest"
10+
},
11+
"scripts": {
12+
"dev": "next",
13+
"start": "next start",
14+
"build": "next build"
15+
}
16+
}

examples/aggregate/pages/api/data.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const projects = [
2+
'facebook/flipper',
3+
'vuejs/vuepress',
4+
'rust-lang/rust',
5+
'vercel/next.js'
6+
]
7+
8+
export default async function api(req, res) {
9+
if (req.query.id) {
10+
return new Promise(resolve => {
11+
setTimeout(() => resolve(projects[req.query.id]), 1500)
12+
}).then(v => res.json(v))
13+
}
14+
}

0 commit comments

Comments
 (0)