Skip to content

Commit

Permalink
feat: tanstack query implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
q-u-n committed May 18, 2024
1 parent d1e8833 commit 528757e
Show file tree
Hide file tree
Showing 22 changed files with 1,056 additions and 3 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
},
"rules": {
"import/no-unresolved": ["error", { "commonjs": true, "amd": true }],
"@typescript-eslint/no-explicit-any": "off"
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-types": "off"
},
"overrides": [
{
Expand Down
4 changes: 2 additions & 2 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@

项目中包含:

- 状态管理库 —— Zustand/Jotai/Valtio/React-Query 实现
- 状态管理库 —— Zustand/Jotai/Valtio/TanStack Query 实现
- 完整的项目配置
- 文档站
- 文档站实现
- 大量的案例代码

以及完善的注释用来学习和实践。
Expand Down
34 changes: 34 additions & 0 deletions packages/react-query/__tests__/basic.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 单元测试验证基本功能
import { render, fireEvent } from '@testing-library/react'
import { QueryClient, useQuery } from '../src'
import { renderWithClient } from './utils'

function sleep(timeout: number): Promise<void> {
return new Promise((resolve, _reject) => {
setTimeout(resolve, timeout)
})
}

describe('useQuery', () => {
const queryClient = new QueryClient()
it('basic', async () => {
function App() {
const { data = 'default' } = useQuery({
queryKey: ['key'],
queryFn: async () => {
await sleep(10)
return 'mu-mu'
},
})

return (
<div>
<h1>{data}</h1>
</div>
)
}
const { findByText } = renderWithClient(queryClient, <App />)
await findByText('default')
await findByText('mu-mu')
})
})
26 changes: 26 additions & 0 deletions packages/react-query/__tests__/gc.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { waitFor } from '@testing-library/react'

import { QueryClient } from '../src'
import { QueryCache } from '../src/queryCache'
import { QueryObserver } from '../src/queryObserver'

describe('测试垃圾回收', () => {
let queryClient: QueryClient
let queryCache: QueryCache

beforeEach(() => {
queryClient = new QueryClient()
queryCache = queryClient.getQueryCache()
})

it('正确从QueryCache中删除query', async () => {
const key = ['todos']
const observer = new QueryObserver(queryClient, {
queryKey: key,
queryFn: () => 'todos',
gcTime: 0,
})
observer.setOptions()
await waitFor(() => expect(queryCache.has(key)).toBe(false))
})
})
33 changes: 33 additions & 0 deletions packages/react-query/__tests__/hashKey.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { hashKey } from '../src/queryCache'

describe('测试 `hashKey 是否生成稳定的结果', () => {
it('测试用例1', () => {
const obj1 = ['key1', 'key2', 'key3']
const obj2 = ['key1', 'key2', 'key3']
expect(hashKey(obj1) === hashKey(obj2)).toBe(true)
})

it('测试用例2', () => {
const obj1 = ['key2', 'key1']
const obj2 = ['key1', 'key2']
expect(hashKey(obj1) === hashKey(obj2)).toBe(false)
})

it('测试用例3', () => {
const obj1 = ['key1', 'key2']
const obj2 = ['key1', 'key2', 'key3']
expect(hashKey(obj1) === hashKey(obj2)).toBe(false)
})

it('测试用例4', () => {
const obj1 = ['key1', 'key2', { key1: 'val1', key2: 'val2' }]
const obj2 = ['key1', 'key2', { key2: 'val2', key1: 'val1' }]
expect(hashKey(obj1) === hashKey(obj2)).toBe(true)
})

it('测试用例5', () => {
const obj1 = ['key1', 'key2', { key1: 'val1', key2: 'val2' }]
const obj2 = ['key1', 'key2', { key1: 'val2', key2: 'val1', key3: 'val3' }]
expect(hashKey(obj1) === hashKey(obj2)).toBe(false)
})
})
41 changes: 41 additions & 0 deletions packages/react-query/__tests__/query-cancellation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { waitFor } from '@testing-library/react'
import { QueryClient, useQuery } from '../src'
import { QueryCache } from '../src/queryCache'
import { sleep } from '../src/utils'
import { Blink, renderWithClient } from './utils'

describe('测试垃圾回收', () => {
let queryClient: QueryClient
let queryCache: QueryCache

beforeEach(() => {
queryClient = new QueryClient()
queryCache = queryClient.getQueryCache()
})
it('单元测试验证查询取消 (query cancellation) 能力', async () => {
const cancelFn = jest.fn()
const queryFn = ({ signal }: { signal?: AbortSignal }) => {
signal?.addEventListener('abort', cancelFn)
}

function Page() {
const state = useQuery({ queryKey: ['todo'], queryFn })
return (
<div>
<h1>Status: {state.status}</h1>
</div>
)
}

const rendered = renderWithClient(
queryClient,
<Blink duration={5}>
<Page />
</Blink>,
)

await waitFor(() => rendered.getByText('off'))

expect(cancelFn).toHaveBeenCalled()
})
})
32 changes: 32 additions & 0 deletions packages/react-query/__tests__/query-retries.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { QueryClient, useQuery } from '../src'
import { renderWithClient } from './utils'

const queryClient = new QueryClient()

it('单元测试验证请求重试 (query retries) 能力', async () => {
let count = 0
function App() {
const { data = 'default' } = useQuery({
queryKey: ['key'],
queryFn: async () => {
count++
if (count === 3) {
return 'mu-mu'
}
throw new Error(`error${count}`)
},
retry: 3,
retryDelay: 2,
})

return (
<div>
<h1>{data}</h1>
</div>
)
}
const { findByText } = renderWithClient(queryClient, <App />)
await findByText('default')
await findByText('mu-mu')
expect(count).toBe(3)
})
43 changes: 43 additions & 0 deletions packages/react-query/__tests__/stale.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { QueryClient } from '../src'
import { sleep } from '../src/utils'

describe('测试 `staleTime` 功能', () => {
let queryClient: QueryClient

beforeEach(() => {
queryClient = new QueryClient()
})

it('只有当超出 `staleTime` 时间才发起额外的请求', async () => {
const key = ['todos']
let count = 0
const fetchFn = () => ++count

const first = await queryClient.fetchQuery({
queryKey: key,
queryFn: fetchFn,
staleTime: 100,
})
const second = await queryClient.fetchQuery({
queryKey: key,
queryFn: fetchFn,
staleTime: 100,
})
const third = await queryClient.fetchQuery({
queryKey: key,
queryFn: fetchFn,
staleTime: 100,
})
await sleep(101)
const fourth = await queryClient.fetchQuery({
queryKey: key,
queryFn: fetchFn,
staleTime: 100,
})

expect(first).toBe(1)
expect(second).toBe(1)
expect(third).toBe(1)
expect(fourth).toBe(2)
})
})
34 changes: 34 additions & 0 deletions packages/react-query/__tests__/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { render } from '@testing-library/react'
import React, { useEffect, useState } from 'react'
import { QueryClientProvider, QueryClient } from '../src'

export function renderWithClient(
client: QueryClient,
ui: React.ReactElement,
): ReturnType<typeof render> {
const result = render(
<QueryClientProvider client={client}>{ui}</QueryClientProvider>,
)
return result
}

export function Blink({
duration,
children,
}: {
duration: number
children: React.ReactNode
}) {
const [shouldShow, setShouldShow] = useState(true)

useEffect(() => {
const timeout = setTimeout(() => {
setShouldShow(false)
}, duration)
return () => {
clearTimeout(timeout)
}
}, [duration])

return shouldShow ? <>{children}</> : <>off</>
}
32 changes: 32 additions & 0 deletions packages/react-query/src/QueryClientProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createContext, ReactNode, useContext } from 'react'

import type { QueryClient } from './queryClient'

export const QueryClientContext = createContext<QueryClient | undefined>(
undefined,
)

export const useQueryClient = () => {
const client = useContext(QueryClientContext)
if (!client) {
// 这里需要做一个判断,如果拿到的 `client` 为 undefined 就代表没被 `QueryClientProvider` 包裹,需要报错
throw new Error('未设置 QueryClient,请使用 QueryClientProvider 设置一个')
}
return client
}

export type QueryClientProviderProps = {
client: QueryClient
children?: ReactNode
}

export const QueryClientProvider = ({
client,
children,
}: QueryClientProviderProps) => {
return (
<QueryClientContext.Provider value={client}>
{children}
</QueryClientContext.Provider>
)
}
3 changes: 3 additions & 0 deletions packages/react-query/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './useQuery'
export * from './queryClient'
export * from './QueryClientProvider'
Loading

0 comments on commit 528757e

Please sign in to comment.