Skip to content

Commit 747dd8f

Browse files
promer94ekkoxu
authored and
ekkoxu
committed
wip: useSWRAggregator
1 parent 8f0288b commit 747dd8f

14 files changed

+421
-10
lines changed

agregator/index.tsx

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

agregator/package.json

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "swr-agregator",
3+
"version": "0.0.1",
4+
"main": "./dist/index.js",
5+
"module": "./dist/index.esm.js",
6+
"types": "./dist/agregator",
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+
}

agregator/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+
}

agregator/types.ts

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

examples/queries/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/queries/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/queries/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/queries/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)