Skip to content

Commit

Permalink
wip: useSWRAggregator
Browse files Browse the repository at this point in the history
chroe: update
  • Loading branch information
promer94 authored and ekkoxu committed Jun 17, 2022
1 parent 8f0288b commit bb3ea77
Show file tree
Hide file tree
Showing 15 changed files with 415 additions and 10 deletions.
166 changes: 166 additions & 0 deletions aggregator/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import React, { useRef, useMemo, memo } from 'react'
import useSWR, { unstable_serialize } from 'swr'
import {
createCacheHelper,
SWRHook,
Middleware,
withMiddleware,
isUndefined,
useIsomorphicLayoutEffect,
mergeObjects,
MutatorOptions,
MutatorCallback,
Arguments,
RevalidatorOptions,
SWRGlobalState,
getTimestamp,
GlobalState,
BareFetcher,
defaultConfig
} from 'swr/_internal'

import type {
SWRItemProps,
SWRAggregatorConfiguration,
SWRAggregator,
SWRCollection
} from './types'

const defaultChildren = () => {
return null
}

export const aggregator = (<Data, Error, Key extends Arguments = Arguments>(
useSWRNext: SWRHook
) =>
(
_keys: Key[],
fetcher: BareFetcher<Data>,
config: typeof defaultConfig & SWRAggregatorConfiguration<Data, Error, Key>
) => {
if (!Array.isArray(_keys)) throw new Error('not array')
const { cache, compare, mutate: _internalMutate } = config
const fetcherRef = useRef(fetcher)
const configRef = useRef(config)
const swrkeys = unstable_serialize(_keys)
// eslint-disable-next-line react-hooks/exhaustive-deps
const keys = useMemo(() => _keys.map(v => unstable_serialize(v)), [swrkeys])
const cacheHelpers = useMemo(
() =>
keys.map(key => {
const [get] = createCacheHelper<Data>(cache, key)
return {
get
}
}),
[keys, cache]
)
const fetch = async (revalidateOpts?: RevalidatorOptions): Promise<any> => {
const revalidate = async (index: number) => {
let newData: Data
let startAt: number
const opts = revalidateOpts || {}
const key = keys[index]
const _key = _keys[index]
const { get } = cacheHelpers[index]
const [_, MUTATION, FETCH] = SWRGlobalState.get(cache) as GlobalState
// If there is no ongoing concurrent request, or `dedupe` is not set, a
// new request should be initiated.
const shouldStartNewRequest = !FETCH[key] || !opts.dedupe

const cleanupState = () => {
// Check if it's still the same request before deleting.
const requestInfo = FETCH[key]
if (requestInfo && requestInfo[1] === startAt) {
delete FETCH[key]
}
}
const currentFetcher = fetcherRef.current
try {
if (shouldStartNewRequest) {
FETCH[key] = [currentFetcher(_key), getTimestamp()]
}
;[newData, startAt] = FETCH[key]
newData = await newData

if (shouldStartNewRequest) {
setTimeout(cleanupState, config.dedupingInterval)
}
const mutationInfo = MUTATION[key]
if (
!isUndefined(mutationInfo) &&
// case 1
(startAt <= mutationInfo[0] ||
// case 2
startAt <= mutationInfo[1] ||
// case 3
mutationInfo[1] === 0)
) {
return mergeObjects({}, { data: get().data, error: get().error })
}
if (!compare(newData, get().data)) {
await _internalMutate(_key, newData, false)
}
// eslint-disable-next-line no-empty
} catch {
cleanupState()
}
return mergeObjects({}, { data: get().data, error: get().error })
}
return Promise.all(keys.map((___, i) => revalidate(i)))
}
const swr = useSWRNext(_keys, () => fetch({ dedupe: true }), config)
const SWRAggregatorItem = useMemo(() => {
const Component = memo(({ index }: { index: number }) => {
const item = useSWR(_keys[index], async () => {
const currentFetcher = fetcherRef.current
const data = await currentFetcher(_keys[index])
swr.mutate()
return data
})
const children = configRef.current.children || defaultChildren
return children(item, swr, index)
})
Component.displayName = 'SWRAggregatorItem'
return Component
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [keys, swr])
const item = (key: any, index: number) => (
<SWRAggregatorItem key={key} index={index} />
)
useIsomorphicLayoutEffect(() => {
fetcherRef.current = fetcher
configRef.current = config
})
return {
items: keys.map(item),
mutate: (
data: Data[] | Promise<Data[]> | MutatorCallback<Data[]> = () =>
fetch({ dedupe: false }),
opt: boolean | MutatorOptions<Data[]> = false
) => swr.mutate(data, opt),
get data() {
return swr.data?.map((v: any, i: number) =>
mergeObjects(v, {
key: keys[i],
originKey: _keys[i]
})
)
},
get isLoading() {
return swr.isLoading
},
get isValidating() {
return swr.isValidating
}
}
}) as unknown as Middleware

export default withMiddleware(useSWR, aggregator) as unknown as SWRAggregator

export {
SWRItemProps,
SWRAggregatorConfiguration,
SWRAggregator,
SWRCollection
}
20 changes: 20 additions & 0 deletions aggregator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "swr-aggregator",
"version": "0.0.1",
"main": "./dist/index.js",
"module": "./dist/index.esm.js",
"types": "./dist/aggregator",
"exports": "./dist/index.mjs",
"private": true,
"scripts": {
"watch": "bunchee index.tsx --no-sourcemap -w",
"build": "bunchee index.tsx --no-sourcemap",
"types:check": "tsc --noEmit",
"clean": "rimraf dist"
},
"peerDependencies": {
"swr": "*",
"react": "*",
"use-sync-external-store": "*"
}
}
8 changes: 8 additions & 0 deletions aggregator/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "..",
"outDir": "./dist"
},
"include": ["./*.tsx"]
}
85 changes: 85 additions & 0 deletions aggregator/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
SWRConfiguration,
Arguments,
BareFetcher,
KeyedMutator,
Fetcher,
SWRResponse
} from 'swr/_internal'

export type Keys<T extends Arguments = Arguments> = T[]

export interface SWRCollection<
Data = any,
Error = any,
Key extends Arguments = Arguments
> {
mutate: KeyedMutator<Data[]>
data?: Array<{
data?: Data
error?: Error
key: string
originKey: Key
}>
isValidating: boolean
isLoading: boolean
}

export interface SWRItemProps<Data = any, Key extends Arguments = Arguments> {
keys: Key[]
fetcher: BareFetcher<Data>
index: number
}

export interface SWRAggregatorConfiguration<
Data = any,
Error = any,
OriginKey extends Arguments = Arguments
> extends SWRConfiguration<Data, Error> {
children: (
items: SWRResponse<Data, Error>,
collection: SWRCollection<Data, Error, OriginKey>,
index: number
) => React.ReactElement<any, any> | null
}

interface AggregatorResult<
Data = any,
Error = any,
Key extends Arguments = Arguments
> extends SWRCollection<Data, Error, Key> {
items: Array<JSX.Element | null>
}

export interface SWRAggregator {
<Data = any, Error = any, Key extends Arguments = Arguments>(
key: Keys<Key>
): AggregatorResult<Data, Error>
<Data = any, Error = any, Key extends Arguments = Arguments>(
key: Keys<Key>,
fetcher: Fetcher<Data, Key> | null
): AggregatorResult<Data, Error>
<Data = any, Error = any, Key extends Arguments = Arguments>(
key: Keys<Key>,
config: SWRAggregatorConfiguration<Data, Error, Key> | undefined
): AggregatorResult<Data, Error>
<Data = any, Error = any, Key extends Arguments = Arguments>(
key: Keys<Key>,
fetcher: Fetcher<Data, Key> | null,
config: SWRAggregatorConfiguration<Data, Error, Key> | undefined
): AggregatorResult<Data, Error>
<Data = any, Error = any>(key: Keys): AggregatorResult<Data, Error>
<Data = any, Error = any>(
key: Keys,
fetcher: BareFetcher<Data> | null
): AggregatorResult<Data, Error>
<Data = any, Error = any>(
key: Keys,
config: SWRAggregatorConfiguration<Data, Error> | undefined
): AggregatorResult<Data, Error>
<Data = any, Error = any>(
key: Keys,
fetcher: BareFetcher<Data> | null,
config: SWRAggregatorConfiguration<Data, Error> | undefined
): AggregatorResult<Data, Error>
}
30 changes: 30 additions & 0 deletions examples/aggregate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Basic

## One-Click Deploy

Deploy your own SWR project with Vercel.

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?s=https://github.com/vercel/swr/tree/main/examples/basic)

## How to Use

Download the example:

```bash
curl https://codeload.github.com/vercel/swr/tar.gz/main | tar -xz --strip=2 swr-main/examples/basic
cd basic
```

Install it and run:

```bash
yarn
yarn dev
# or
npm install
npm run dev
```

## The Idea behind the Example

Show a basic usage of SWR fetching data from an API in two different pages.
4 changes: 4 additions & 0 deletions examples/aggregate/libs/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default async function fetcher(...args) {
const res = await fetch(...args)
return res.json()
}
16 changes: 16 additions & 0 deletions examples/aggregate/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "queries",
"private": true,
"license": "MIT",
"dependencies": {
"next": "latest",
"react": "18.1.0",
"react-dom": "18.1.0",
"swr": "latest"
},
"scripts": {
"dev": "next",
"start": "next start",
"build": "next build"
}
}
14 changes: 14 additions & 0 deletions examples/aggregate/pages/api/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const projects = [
'facebook/flipper',
'vuejs/vuepress',
'rust-lang/rust',
'vercel/next.js'
]

export default async function api(req, res) {
if (req.query.id) {
return new Promise(resolve => {
setTimeout(() => resolve(projects[req.query.id]), 1500)
}).then(v => res.json(v))
}
}
Loading

0 comments on commit bb3ea77

Please sign in to comment.