Skip to content

Commit

Permalink
docs: add docs for Dependencies Collection
Browse files Browse the repository at this point in the history
  • Loading branch information
vikiboss committed Aug 15, 2024
1 parent a648b44 commit 62f71cb
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 0 deletions.
90 changes: 90 additions & 0 deletions docs/docs/en/docs/features/dependencies-collection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# 🕸 Dependencies Collection

`@shined/react-use` serves as a fundamental utility library where performance is paramount. To ensure flexibility and a rich set of features while minimizing performance overhead as much as possible, we've introduced the concept of Dependencies Collection.

> The idea of Dependency Collection originally came from [SWR](https://swr.vercel.app/docs/advanced/performance#dependency-collection). We have made some improvements and optimizations on this basis.
## TL; DR {#tl-dr}

- To provide flexible and rich features, many Hooks internally need to manage and return numerous states
- Users may not need to consume all of the states, only a portion, but any state change can lead to re-rendering
- To reduce unnecessary rendering overhead, we introduced the concept of "Dependencies Collection"
- Only the states that are used and changed will trigger rendering, which is imperceptible to users but can significantly improve performance

## Motivation

Inside `@shined/react-use`, there's a `useQuery` that returns various states including, but not limited to, `data`, `loading`, `error` for implementing multiple features. Normally, regardless of whether the returned states are used or not, any change in the corresponding states would invoke operations like `setState(loading)` to update the state.

This operational logic is not problematic in itself. The issue arises when updates are still made even if certain states are not actually used by the user, leading to unnecessary rendering overhead and greatly decreasing rendering performance.

```tsx
// The user actually only needs the run operation and the loading state, not caring about error or data
const { run, data } = useQuery(requestSomething, options)

// The user performs the run operation
run()

// When data, error, loading changes, internal `setState` operation will cause the component to re-render
```

In some scenarios (like above), we might only need one of the states, such as `data`, and not the others. To achieve this, we introduced the concept of dependency collection, allowing users to inform `@shined/react-use` which states they care about and which they do not through the passing of dependencies.

```tsx
// Users take what they need from the return values
const { run, data } = useQuery(requestSomething, options)

// The user performs the run operation
run()

// When data changes, internal `setState` operation will cause the component to re-render

// As for error, loading, even if they change, it will not trigger re-rendering because the user did not actually use them
```

A component that originally needed to render 4~5 times may now only need to render 1~2 times, which undoubtedly represents a substantial improvement in performance.

## Hooks That Implemented Dependency Collection {#hooks-that-implemented-dependencies-collection}

- [useAsyncFn](/reference/use-async-fn)
- [useClipboard](/reference/use-clipboard)
- [useClipboardItems](/reference/use-clipboard-items)
- [useLoadingSlowFn](/reference/use-loading-slow-fn)
- [useQuery](/reference/use-query)
- [~~useRequest (Deprecated, please use useQuest)~~](/reference/use-request)

Due to the substantial amount of work involved, we've only optimized some Hooks for dependency collection so far. We plan to gradually optimize other Hooks in the future.

## Implementation {#implementation}

The implementation principle is actually quite simple. It only requires maintaining a `ref` internally while marking the returned state through `getter` functions during exposure. When a user uses a certain state, mark that state as "used". When changing, determine whether rerendering should be triggered.

Below is a simple pseudocode to demonstrate the underlying principle of dependency collection:

```tsx
// Use useRef instead of useState to define status because we want to manually control the update of the state
const stateRef = useRef({
name: { used: false, value: 'react-use' },
age: { used: false, value: 18 },
})

// Use the setState function to manually update the status, while determining whether rerendering is required
function setState(key: string, value: any) {
if(stateRef.current[key].value === value) return
stateRef.current[key].value = value
if (stateRef.current[key].used) render()
}

// The return value is exposed through a getter function, which also marks whether the state is used in the getter function
const returnedState = {
get name() {
stateRef.current.name.used = true
return stateRef.current.name.value
},
get age() {
stateRef.current.age.used = true
return stateRef.current.age.value
},
}
```

Through such means, we can implement a simple dependency collection system, where only the states that are used by the user will trigger re-rendering. Internally in `@shine/react-use`, this whole logic is integrated into the `useTrackedRefState()` Hook (not yet exposed externally), used to implement the functionality of dependency collection.
90 changes: 90 additions & 0 deletions docs/docs/zh-cn/docs/features/dependencies-collection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# 🕸 依赖收集(Dependencies Collection) {#dependencies-collection}

`@shined/react-use` 作为基础的底层工具库,性能是重中之重,为了在确保灵活性、功能丰富的前提下尽可能地减少性能开销,我们引入了依赖收集(Dependencies Collection)的概念。

> 依赖收集的想法最初来源于 [SWR](https://swr.vercel.app/docs/advanced/performance#dependency-collection),在此基础上我们进行了一些改进和优化。
## 简而言之 {#tl-dr}

- 为了提供灵活、丰富的功能,许多 Hooks 内部需要管理和返回许多状态(State)
- 用户可能并不需要消费所有的状态,只用到了其中的一部分,但是任意状态变化都会导致重渲染
- 为了减少不必要的渲染开销,我们引入了「依赖收集(Dependencies Collection)」的概念
- 只有用到的状态发生变化时才会触发渲染,这对用户来说是无感的,但是可以显著提升性能

## 动机

`@shined/react-use` 内部有一个 `useQuery`,它为了实现诸多功能特性,返回了包括但不限于 `data``loading``error` 等多个状态。正常情况下,无论是否使用了返回的状态,只要对应状态发生了变更,内部就会调用类似 `setState(loading)` 的操作来更新状态。

这个操作逻辑本身并没有问题,问题在于,当用户实际上并未使用某些状态时仍然会进行更新,导致了不必要的渲染开销,极大降低了渲染性能。

```tsx
// 用户实际上只需要 run 操作和 loading 这一个状态,对于 error、data 其实并不关心
const { run, data } = useQuery(requestSomething, options)

// 用户执行 run 操作
run()

// 当 data、error、loading 改变时,内部会触发 `setState` 操作导致组件重新渲染
```

在某些场景下(如上),我们可能只需要其中的一个状态,比如 `data`,而不需要其他状态。为了实现这一点,我们引入了依赖收集的概念,用户可以通过传入依赖项的方式告诉 `@shined/react-use` 哪些状态是他们关心的,哪些是不关心的。

```tsx
// 用户需要什么,就从返回值里面取什么
const { run, data } = useQuery(requestSomething, options)

// 用户执行 run 操作
run()

// 当 data 改变时,内部会触发 `setState` 操作导致组件重新渲染

// 而对于 error、loading,即使改变了也不会触发重新渲染,因为用户并没有实际用到
```

原本需要渲染 4~5 次的组件,现在可能只需要渲染 1~2 次,这对于性能提升来说,无疑是相当巨大的。

## 实现了依赖收集的 Hooks {#hooks-that-implemented-dependencies-collection}

- [useAsyncFn](/reference/use-async-fn)
- [useClipboard](/reference/use-clipboard)
- [useClipboardItems](/reference/use-clipboard-items)
- [useLoadingSlowFn](/reference/use-loading-slow-fn)
- [useQuery](/reference/use-query)
- [~~useRequest(已弃用,请使用 useQuest)~~](/reference/use-request)

由于工作量巨大,我们目前只对部分 Hooks 进行了依赖收集的优化,未来会逐步对其他 Hooks 进行优化。

## 实现原理 {#implementation}

实现原理其实很简单,只需要在内部维护一个 `ref`,同时在暴露返回值时,通过 `getter` 函数进行标记,当用户使用到某个状态时,将这个状态标记为「已使用」,在变更时判断是否需要触发重新渲染。

下面是一个简单的伪代码来演示依赖收集的底层原理:

```tsx
// 使用 useRef 而不是 useState 来定义状态,因为我们希望手动控制状态的更新
const stateRef = useRef({
name: { used: false, value: 'react-use' },
age: { used: false, value: 18 },
})

// 通过 setState 函数来手动更新状态,同时判断是否需要触发重新渲染
function setState(key: string, value: any) {
if(stateRef.current[key].value === value) return
stateRef.current[key].value = value
if (stateRef.current[key].used) render()
}

// 返回值通过 getter 函数暴露,同时在 getter 函数中标记状态是否被使用
const returnedState = {
get name() {
stateRef.current.name.used = true
return stateRef.current.name.value
},
get age() {
stateRef.current.age.used = true
return stateRef.current.age.value
},
}
```

通过这样的方式,我们可以实现一个简单的依赖收集系统,只有当用户使用到了某个状态时才会触发重新渲染。在 `@shine/react-use` 内部,这一整套逻辑被整合到 `useTrackedRefState()` Hook (目前尚未对外暴露)当中,用于实现依赖收集的功能。
8 changes: 8 additions & 0 deletions docs/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export const sidebar = {
{
text: '✨ Features',
items: [
{
text: '🕸 Dependencies Collection',
link: '/docs/features/dependencies-collection',
},
{
text: '🎯 ElementTarget',
link: '/docs/features/element-target',
Expand Down Expand Up @@ -80,6 +84,10 @@ export const sidebar = {
{
text: '✨ 功能特性',
items: [
{
text: '🕸 依赖收集(Dependencies Collection)',
link: `/${langSlug.zhCN}/docs/features/dependencies-collection`,
},
{
text: '🎯 元素目标(ElementTarget)',
link: `/${langSlug.zhCN}/docs/features/element-target`,
Expand Down

0 comments on commit 62f71cb

Please sign in to comment.