swr

swr是一个用于数据请求的React Hooks 库

Repeat Code

image_%286%29.png

首先,我们需要创建一个 user 状态,初始值为空,通过useEffect钩子获取 userData ,通过 setUser 更新user的值。
通常,我们会将所有的数据请求都保存在顶级组件中,并为树深处的每个组件 添加props。子组件接收user值并在jsx中引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 页面组件

function Page() {
const [user, setUser] = useState(null)

// 请求数据
useEffect(() => {
fetch("/api/user")
.then((res) => res.json())
.then((data) => setUser(data))
}, [])

// 全局加载状态
if (!user) return <Spinner />

return <div>
<Navbar user={user} />
<Content user={user} />
</div>
}

// 子组件

function Navbar({ user }) {
return <div>
...
<Avatar user={user} />
</div>
}

function Content({ user }) {
return <h1>Welcome back, {user.name}</h1>
}

function Avatar({ user }) {
return <Image src={user.avatar} alt={user.name} />
}

useSWR

swr.vercel.app

现在数据已 绑定 到需要该数据的组件上,并且所有组件都是相互 独立 的。所有的父组件都不需要关心关于数据或数据传递的任何信息。它们只是渲染。现在代码更简单,更易于维护了。
最棒的是,只会有 1 个请求 发送到 API,因为它们使用相同的 SWR key,因此请求会被自动 去除重复缓存共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 页面组件

function Page() {
return <div>
<Navbar />
<Content />
</div>
}

// 子组件

function Navbar() {
return <div>
...
<Avatar />
</div>
}

function useUser () {
return useSWR('/api/user', fetcher)
}

function Content() {
const { user, isLoading } = useUser()
if (isLoading) return <Spinner />
return <h1>Welcome back, {user.name}</h1>
}

function Avatar() {
const { user, isLoading } = useUser()
if (isLoading) return <Spinner />
return <Image src={user.avatar} alt={user.name} />
}

iShot_2024-01-28_23.23.04_%281%29.png

Cache

1
2
3
4
5
6
7
8
9
interface Cache<Data> {
get(key: string): Data | undefined
set(key: string, value: Data): void
delete(key: string): void
keys(): IterableIterator<string>
}
// Default cache provider
const [cache, mutate] = initCache(new Map()) as [Cache<any>, ScopedMutator<any>]

serialize key

1
2
3
4
5
6
7
8
9
type ArgumentsTuple = [any, ...unknown[]] | readonly [any, ...unknown[]]
export type Arguments =
| string
| ArgumentsTuple
| Record<any, any>
| null
| undefined
| false
export type Key = Arguments | (() => Arguments)

Key支持string、function、array等,swr内部会序列号该key,对复杂对象做哈希转换,最终输出字符串作为Cache的key值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// A stable hash implementation that supports:
// - Fast and ensures unique hash properties
// - Handles unserializable values
// - Handles object key ordering
// - Generates short results
//
// This is not a serialization function, and the result is not guaranteed to be
// parsable.
export const stableHash = (arg: any): string => {
const type = typeof arg
const constructor = arg && arg.constructor
const isDate = constructor == Date

let result: any
let index: any

if (OBJECT(arg) === arg && !isDate && constructor != RegExp) {
result = table.get(arg)
if (result) return result
result = ++counter + '~'
table.set(arg, result)

if (constructor == Array) {
result = '@'
for (index = 0; index < arg.length; index++) {
result += stableHash(arg[index]) + ','
}
table.set(arg, result)
}
if (constructor == OBJECT) {
result = '#'
const keys = OBJECT.keys(arg).sort()
while (!isUndefined((index = keys.pop() as string))) {
if (!isUndefined(arg[index])) {
result += index + ':' + stableHash(arg[index]) + ','
}
}
table.set(arg, result)
}
} else {
result = isDate
? arg.toJSON()
: type == 'symbol'
? arg.toString()
: type == 'string'
? JSON.stringify(arg)
: '' + arg
}

return result
}

createCacheHelper

1
2
const [getCache, setCache, subscribeCache, getInitialCache] =
createCacheHelper(cache, key)

useSyncExternalStore

react18版本升级了渲染架构,自身API支持Concurrent,针对react库开发者的store订阅需求提供的api。

https://github.com/reactwg/react-18/discussions/70

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Get the current state that SWR should return.
const cached = useSyncExternalStore(
useCallback(
(callback: () => void) =>
subscribeCache(
key,
(current: State<Data, any>, prev: State<Data, any>) => {
if (!isEqual(prev, current)) callback()
}
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[cache, key]
),
getSnapshot[0],
getSnapshot[1]
)

revalidate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// After mounted or key changed.
useIsomorphicLayoutEffect(() => {
if (!key) return;

const softRevalidate = revalidate.bind(UNDEFINED, { dedupe: true });


// Trigger a revalidation.
if (shouldDoInitialRevalidation) {
if (isUndefined(data) || IS_SERVER) {
// Revalidate immediately.
softRevalidate();
} else {
// Delay the revalidate if we have data to return so we won't block
// rendering.
rAF(softRevalidate);
}
}

return () => {

};
}, [key]);

// The revalidation function is a carefully crafted wrapper of the original
// `fetcher`, to correctly handle the many edge cases.
const revalidate = useCallback(
async (revalidateOpts?: RevalidatorOptions): Promise<boolean> => {
// If there is no ongoing concurrent request, or `dedupe` is not set, a
// new request should be initiated.
const shouldStartNewRequest = !FETCH[key] || !opts.dedupe;

try {
if (shouldStartNewRequest) {
setCache(initialState);

// Start the request and save the timestamp.
// Key must be truthy if entering here.
FETCH[key] = [currentFetcher(_key), getTimestamp()];
}

// Wait until the ongoing request is done. Deduplication is also
// considered here.
[newData, startAt] = FETCH[key];
newData = await newData;

if (shouldStartNewRequest) {
// If the request isn't interrupted, clean it up after the
// deduplication interval.
setTimeout(cleanupState, config.dedupingInterval);
}

// If there're other ongoing request(s), started after the current one,
// we need to ignore the current one to avoid possible race conditions:
// req1------------------>res1 (current one)
// req2---------------->res2
// the request that fired later will always be kept.
// The timestamp maybe be `undefined` or a number
if (!FETCH[key] || FETCH[key][1] !== startAt) {
return false;
}

// Clear error.
finalState.error = UNDEFINED;

// Deep compare with the latest state to avoid extra re-renders.
// For local state, compare and assign.
const cacheData = getCache().data;

// Since the compare fn could be custom fn
// cacheData might be different from newData even when compare fn returns True
finalState.data = compare(cacheData, newData) ? cacheData : newData;
} catch (err: any) {
cleanupState();
}

// Update the current hook's state.
setCache(finalState)

return true
},
[key, cache]
);



// Polyfill requestAnimationFrame
export const rAF = (
f: (...args: any[]) => void
): number | ReturnType<typeof setTimeout> =>
hasRequestAnimationFrame()
? window['requestAnimationFrame'](f)
: setTimeout(f, 1)

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser.
export const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect

当组件挂载完毕或者Key值发生变更时,useIsomorphicLayoutEffect函数开始执行。如果key值为空,直接return。判断是否需要发出新的请求,记录请求发出时的时间戳,用于后续新的请求对比,请求结束后的2s清除时间戳的状态。如果2s内(默认配置)有新的请求发起,时间戳不一致的情况下,则废弃当前的请求。

Performance

Deep Comparison

和serailizeKey保持一致

1
2
const compare = (currentData: any, newData: any) =>
stableHash(currentData) == stableHash(newData)

Dependency Collection

引用的值会被收集,实际渲染次数取决于引用的值。当缓存数据变更时,订阅缓存函数会比较当前快照。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
export const useSWRHandler = <Data = any, Error = any>(
_key: Key,
fetcher: Fetcher<Data> | null,
config: FullConfiguration & SWRConfiguration<Data, Error>
) => {


// .....

return {
mutate: boundMutate,
get data() {
stateDependencies.data = true
return returnedData
},
get error() {
stateDependencies.error = true
return error
},
get isValidating() {
stateDependencies.isValidating = true
return isValidating
},
get isLoading() {
stateDependencies.isLoading = true
return isLoading
}
} as SWRResponse<Data, Error>
}

const isEqual = (prev: State<Data, any>, current: State<Data, any>) => {
let equal = true
for (const _ in stateDependencies) {
const t = _ as keyof StateDependencies
if (t === 'data') {
if (!compare(current[t], prev[t])) {
if (isUndefined(prev[t])) {
if (!compare(current[t], returnedData)) {
equal = false
}
} else {
equal = false
}
}
} else {
if (current[t] !== prev[t]) {
equal = false
}
}
}
return equal
}

And more…

  • useRequest、react-query和swr其实类似,但后两者更为激进,将url path作为key值,并且要求key值必传。useRequest比较适合搭配antd使用,useRequest提供的分页属性是无缝接入分页组件的。
  • useSWRConfig 请求合并
  • useSWRMutation 适用于用户交互触发POST 请求
  • polling 轮询
  • infinite 分页

image


swr
http://yoursite.com/front-end/
作者
north
发布于
2024年1月28日
许可协议