Skip to content

Commit a5c57c8

Browse files
committed
feat: created query package
1 parent 763fbae commit a5c57c8

25 files changed

+552
-25
lines changed

README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,18 @@
2727
- **Zero-config** server-side rendering capability.
2828
- **Self-contained** with no dependencies.
2929
- **Backed by a team** using VueHooks in production.
30-
- **Additional addons** such as vue-router, timeago, etc.
30+
- **Additional addons** such as vue-router, timeago, query, etc.
3131

3232
<br />
3333

3434
## :earth_americas: Ecosystem
3535

3636
<!-- omit in toc -->
3737

38-
- [@vuehooks/core](#vuehookscore)
39-
- [@vuehooks/router](#vuehooksrouter)
38+
- [@vuehooks/core](#vuehookscore) - Collection of common hooks.
39+
- [@vuehooks/query](#vuehooksquery) - Hooks for fetching, caching and updating
40+
asynchronous data.
41+
- [@vuehooks/router](#vuehooksrouter) - Hooks for make using VueRouter easier.
4042

4143
<br />
4244

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
"scripts": {
99
"test": "jest",
1010
"test:watch": "jest --watch --coverage",
11-
"build": "lerna run build --npm-client=yarn",
11+
"build": "yarn clean && lerna run build --npm-client=yarn",
1212
"bootstrap": "lerna bootstrap --npm-client=yarn",
13-
"clean": "rimraf packages/**/dist packages/**/*.tsbuildinfo",
13+
"clean": "rimraf packages/**/dist packages/**/*.tsbuildinfo packages/**/node_modules",
1414
"update": "yarn upgrade-interactive --latest",
15-
"release": "yarn clean && yarn build && lerna publish"
15+
"release": "yarn build && lerna publish"
1616
},
1717
"dependencies": {},
1818
"peerDependencies": {

packages/core/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ export { useTimeout } from './useTimeout'
77
export { useTimeoutFn } from './useTimeoutFn'
88
export { useLongPress } from './useLongPress'
99
export { useBus } from './useBus'
10+
11+
export { Emitter } from './utils/emitter'

packages/core/src/useHover.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Ref } from '@vue/composition-api'
2-
import { useEventListenerElement } from './useEventListenerElement'
3-
import useToggle from './useToggle'
2+
import { useEvent } from './useEvent'
3+
import { useToggle } from './useToggle'
44

55
export function useHover(target: Ref<HTMLElement | null>) {
66
const { on: hovering, set } = useToggle(false)
77

8-
useEventListenerElement(target, 'mouseenter', () => set(true))
9-
useEventListenerElement(target, 'mouseleave', () => set(false))
8+
useEvent(target, 'mouseenter', () => set(true))
9+
useEvent(target, 'mouseleave', () => set(false))
1010

1111
return hovering
1212
}

packages/core/src/useMouse.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ref } from '@vue/composition-api'
2-
import { useEventListener } from './useEventListener'
2+
// import { useEvent } from './useEvent'
33

44
interface Options {
55
initial?: { x: number; y: number }
@@ -26,14 +26,14 @@ export function useMouse({ initial = { x: 0, y: 0 } }: Options = {}) {
2626
const x = ref(initial.x)
2727
const y = ref(initial.y)
2828

29-
const set = (event: TouchEvent | MouseEvent) => {
30-
const [newX, newY] = getXY(event)
31-
x.value = newX
32-
y.value = newY
33-
}
29+
// const set = (event: TouchEvent | MouseEvent) => {
30+
// const [newX, newY] = getXY(event)
31+
// x.value = newX
32+
// y.value = newY
33+
// }
3434

35-
useEventListener('mousemove', set)
36-
useEventListener('touchmove', set)
35+
// useEvent('mousemove', set)
36+
// useEvent('touchmove', set)
3737

3838
return { x, y }
3939
}

packages/core/src/useMouseElement.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Ref, ref } from '@vue/composition-api'
2-
import { useEventListenerElement } from './useEventListenerElement'
2+
import { useEvent } from './useEvent'
33
import { getXY } from './useMouse'
44

55
export function useMouseInElement(target: Ref<HTMLElement | null>) {
@@ -29,8 +29,8 @@ export function useMouseInElement(target: Ref<HTMLElement | null>) {
2929
elementWidth.value = width
3030
}
3131

32-
useEventListenerElement(target, 'mousemove', move)
33-
useEventListenerElement(target, 'touchmove', move)
32+
useEvent(target, 'mousemove', move)
33+
useEvent(target, 'touchmove', move)
3434

3535
return {
3636
x,

packages/query/README.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<h1 align="center">
2+
VueHooks Query
3+
</h1>
4+
<h4 align="center">Collection of utility composition functions for Vue</h4>
5+
6+
Hooks for fetching, caching and updating asynchronous data in Vue
7+
8+
<p align="center">
9+
<a href="https://www.npmjs.com/package/@vuehooks/query" target="__blank">
10+
<img src="https://img.shields.io/npm/v/@vuehooks/query?color=1abc9c" alt="NPM version" /></a>
11+
<a href="https://www.npmjs.com/package/@vuehooks/query" target="__blank"><img alt="NPM Downloads" src="https://img.shields.io/npm/dm/@vuehooks/query?color=34495e"/></a>
12+
13+
## `@vuehooks/query`
14+
15+
## :rocket: Features
16+
17+
- **Agnostic** data fetching (GraphQL, RESt, promises, whatever!).
18+
- **Auto caching** and updating across components.
19+
20+
## :fire: Functions
21+
22+
- `useQuery` - Resolves asynchronous data using advanced features.
23+
- `useQueryCache` - Returns the active in-memory cache.

packages/query/package.json

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "@vuehooks/query",
3+
"version": "0.0.5",
4+
"description": "Hooks for fetching, caching and updating asynchronous data in Vue",
5+
"keywords": [
6+
"vue3"
7+
],
8+
"author": "Justin Brooks <[email protected]>",
9+
"homepage": "https://github.com/jsbroks/vuehooks#readme",
10+
"license": "MIT",
11+
"main": "dist/index.js",
12+
"directories": {
13+
"lib": "lib",
14+
"test": "__tests__"
15+
},
16+
"files": [
17+
"lib"
18+
],
19+
"publishConfig": {
20+
"registry": "https://registry.yarnpkg.com"
21+
},
22+
"repository": {
23+
"type": "git",
24+
"url": "git+https://github.com/jsbroks/vuehooks.git"
25+
},
26+
"scripts": {
27+
"test": "echo \"Error: run tests from root\" && exit 1"
28+
},
29+
"bugs": {
30+
"url": "https://github.com/jsbroks/vuehooks/issues"
31+
},
32+
"dependencies": {
33+
"@vuehooks/core": "^0.0.5"
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { defaultQueryKeySerializerFn } from '../queryKey'
2+
3+
describe('defaultQuerySerializationFunction', () => {
4+
it('serializes booleans', () => {
5+
const [aHash, a] = defaultQueryKeySerializerFn(true)
6+
const [bHash, b] = defaultQueryKeySerializerFn(false)
7+
8+
expect(a).toEqual([true])
9+
expect(b).toEqual([false])
10+
expect(aHash).toEqual('[true]')
11+
expect(bHash).not.toEqual([false])
12+
expect(bHash).toEqual('[false]')
13+
})
14+
15+
test.each([
16+
['boolean', true],
17+
['number', 0],
18+
['string', 'test']
19+
])('serializes single %s array to match primitives', (_, input) => {
20+
const [aHash, a] = defaultQueryKeySerializerFn(input)
21+
const [bHash, b] = defaultQueryKeySerializerFn([input])
22+
23+
expect(a).toEqual(b)
24+
expect(aHash).toEqual(bHash)
25+
})
26+
27+
it('serializes nested objects', () => {
28+
const a = { a: 'hello', b: 20, c: { d: true, g: [{}, false] } }
29+
const b = { b: 20, c: { g: [{}, false], d: true }, a: 'hello' }
30+
31+
const [aHash, aKey] = defaultQueryKeySerializerFn(a)
32+
const [bHash, bKey] = defaultQueryKeySerializerFn(b)
33+
34+
expect(aHash).toEqual(bHash)
35+
expect(aKey).toEqual(bKey)
36+
})
37+
38+
it('does not accept functions', () => {
39+
const fn = () => defaultQueryKeySerializerFn(() => {})
40+
expect(fn).toThrowError()
41+
})
42+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useQuery } from '../'
2+
3+
describe('useQuery', () => {
4+
const fetch = () =>
5+
Promise.resolve({
6+
idk: [{ id: 1 }, { id: 2 }]
7+
})
8+
9+
it('', async () => {
10+
const { data } = useQuery('fetch', fetch)
11+
expect(data).toBe(await fetch())
12+
})
13+
})

packages/query/src/getQuery.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { isObject } from './utils'
2+
import { QueryConfig, QueryFunction } from './query'
3+
import { QueryKey } from './queryKey'
4+
5+
export function getQueryArgs<TResult, TError, TOptions = undefined>(
6+
args: any[]
7+
): [QueryKey, QueryConfig<TResult, TError>, TOptions] {
8+
let queryKey: QueryKey
9+
let queryFn: QueryFunction<TResult> | undefined
10+
let config: QueryConfig<TResult, TError> | undefined
11+
let options: TOptions
12+
13+
if (isObject(args[0])) {
14+
queryKey = args[0].queryKey
15+
queryFn = args[0].queryFn
16+
config = args[0].config
17+
options = args[1]
18+
} else if (isObject(args[1])) {
19+
queryKey = args[0]
20+
config = args[1]
21+
options = args[2]
22+
} else {
23+
queryKey = args[0]
24+
queryFn = args[1]
25+
config = args[2]
26+
options = args[3]
27+
}
28+
29+
config = config ?? {}
30+
if (queryFn) config = { ...config, queryFn }
31+
32+
return [queryKey, config, options]
33+
}

packages/query/src/getQueryCache.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { defaultCache } from './queryCache'
2+
3+
export function getQueryCache() {
4+
return defaultCache
5+
}

packages/query/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { useQuery } from './useQuery'
2+
export { getQueryCache } from './getQueryCache'

packages/query/src/query.ts

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { deepEqual } from './utils/equals'
2+
import { QueryKeyArray } from './queryKey'
3+
import { createEmitter, Emitter } from '@vuehooks/core'
4+
5+
export enum QueryStatus {
6+
Idle = 'idle',
7+
Loading = 'loading',
8+
Error = 'error',
9+
Success = 'success'
10+
}
11+
12+
export type QueryFunction<TResult> = (
13+
...args: any[]
14+
) => TResult | Promise<TResult>
15+
16+
export type RetryFunction<TError> = (
17+
failureCount: number,
18+
error: TError
19+
) => boolean
20+
21+
/**
22+
* Query Configs
23+
*/
24+
export interface BaseQueryConfig<TResult, TError = unknown> {
25+
queryFn?: QueryFunction<TResult>
26+
isDataEqual?: (newData: unknown, oldData: unknown) => boolean
27+
retry?: boolean | number | RetryFunction<TError>
28+
}
29+
30+
export interface QueryConfig<TResult, TError = unknown>
31+
extends BaseQueryConfig<TResult, TError> {}
32+
33+
export interface QueryInitConfig<TResult, TError = unknown> {
34+
queryKey: QueryKeyArray
35+
queryHash: string
36+
config: QueryConfig<TResult, TError>
37+
}
38+
39+
export interface QueryState<TResult, TError = unknown> {
40+
data: TResult | undefined
41+
status: QueryStatus
42+
error?: TError
43+
}
44+
45+
export type SubscriptionHandler<TResult, TError> = (
46+
status: QueryState<TResult, TError>
47+
) => void
48+
49+
export class Query<TResult, TError> {
50+
readonly queryKey: QueryKeyArray
51+
readonly queryHash: string
52+
readonly config: QueryConfig<TResult, TError>
53+
54+
state: QueryState<TResult, TError>
55+
56+
private emitter: Emitter
57+
private promise?: Promise<TResult | undefined>
58+
59+
constructor(init: QueryInitConfig<TResult, TError>) {
60+
this.config = { isDataEqual: deepEqual, ...init.config }
61+
this.queryKey = init.queryKey
62+
this.queryHash = init.queryHash
63+
this.emitter = createEmitter()
64+
65+
// Default state
66+
this.state = { data: undefined, status: QueryStatus.Idle }
67+
}
68+
69+
async fetch(): Promise<TResult | undefined> {
70+
let queryFn = this.config.queryFn
71+
72+
if (!queryFn) return
73+
if (this.promise) return this.promise
74+
75+
this.promise = (async () => {
76+
try {
77+
this.dispatch({ status: QueryStatus.Loading })
78+
const data = await queryFn!()
79+
80+
if (!this.config.isDataEqual!(this.state.data, data))
81+
this.dispatch({ status: QueryStatus.Success, data })
82+
delete this.promise
83+
84+
return data
85+
} catch (error) {
86+
this.dispatch({ status: QueryStatus.Error })
87+
delete this.promise
88+
throw error
89+
}
90+
})()
91+
92+
return this.promise
93+
}
94+
95+
private dispatch(state: Partial<QueryState<TResult, TError>>) {
96+
this.state = { ...this.state, ...state }
97+
this.emitter.emit('*', { state: this.state })
98+
}
99+
100+
subscribe(handler: SubscriptionHandler<TResult, TError>) {
101+
this.emitter.on('*', handler)
102+
}
103+
104+
unsubscribe(handler: SubscriptionHandler<TResult, TError>) {
105+
this.emitter.off('*', handler)
106+
}
107+
}

0 commit comments

Comments
 (0)