packages/guidelines.md
Here are the guidelines for VueUse functions. You could also take them as a reference for authoring your own composable functions or apps.
You can also find some reasons for those design decisions and also some tips for writing composable functions with Anthony Fu's talk about VueUse:
"vue"ref instead of reactive whenever possibleshallowRef over ref whenever possibledeepRef instead of refconfigurableWindow (etc.) when using global variables like window to be flexible when working with multi-windows, testing mocks, and SSR.isSupported flagwatch or watchEffect internally, also make the immediate and flush options configurable whenever possibletryOnScopeDispose to clear the side-effects gracefullyRead also: Best Practice
Use shallowRef instead of ref when wrapping large amounts of data.
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
// use `shallowRef` to prevent deep reactivity
const data = shallowRef<T | undefined>()
const error = shallowRef<Error | undefined>()
fetch(toValue(url))
.then(r => r.json())
.then(r => data.value = r)
.catch(e => error.value = e)
/* ... */
}
When using global variables like window or document, support configurableWindow or configurableDocument in the options interface to make the function flexible when for scenarios like multi-windows, testing mocks, and SSR.
Learn more about the implementation: _configurable.ts
import type { ConfigurableWindow } from '../_configurable'
import { defaultWindow } from '../_configurable'
import { useEventListener } from '../useEventListener'
export function useActiveElement<T extends HTMLElement>(
options: ConfigurableWindow = {},
) {
const {
// defaultWindow = isClient ? window : undefined
window = defaultWindow,
} = options
let el: T
// skip when in Node.js environment (SSR)
if (window) {
useEventListener(window, 'blur', () => {
el = window?.document.activeElement
}, true)
}
/* ... */
}
Usage example:
// in iframe and bind to the parent window
useActiveElement({ window: window.parent })
When using watch or watchEffect internally, also make the immediate and flush options configurable whenever possible. For example watchDebounced:
import type { WatchOptions } from 'vue'
// extend the watch options
export interface WatchDebouncedOptions extends WatchOptions {
debounce?: number
}
export function watchDebounced(
source: any,
cb: any,
options: WatchDebouncedOptions = {},
): WatchHandle {
return watch(
source,
() => { /* ... */ },
options, // pass watch options
)
}
We use the controls option allowing users to use functions with a single return for simple usages, while being able to have more controls and flexibility when needed. Read more: #362.
controls optionref oruseTimestamp, useInterval,// common usage
const timestamp = useTimestamp()
// more controls for flexibility
const { timestamp, pause, resume } = useTimestamp({ controls: true })
Refer to useTimestamp's source code for the implementation of proper TypeScript support.
controls optionuseRafFn, useRefHistory,const { pause, resume } = useRafFn(() => {})
isSupported FlagWhen involved with Web APIs that are not yet implemented by the browser widely, also outputs isSupported flag.
For example useShare:
export function useShare(
shareOptions: MaybeRef<ShareOptions> = {},
options: ConfigurableNavigator = {},
) {
const { navigator = defaultNavigator } = options
const isSupported = useSupported(() => navigator && 'canShare' in navigator)
const share = async (overrideOptions) => {
if (isSupported.value) {
/* ...implementation */
}
}
return {
isSupported,
share,
}
}
When a composable is asynchronous, like useFetch, it is a good idea to return a PromiseLike object from the composable
so the user is able to await the function. This is especially useful in the case of Vue's <Suspense> api.
ref to determine when the function should resolve e.g. isFinishedUseFetchReturn & PromiseLike<UseFetchReturn>export function useFetch<T>(url: MaybeRefOrGetter<string>): UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>> {
const data = shallowRef<T | undefined>()
const error = shallowRef<Error | undefined>()
const isFinished = ref(false)
fetch(toValue(url))
.then(r => r.json())
.then(r => data.value = r)
.catch(e => error.value = e)
.finally(() => isFinished.value = true)
// Store the return state in a variable
const state: UseFetchReturn<T> = {
data,
error,
isFinished,
}
return {
...state,
// Adding `then` to an object allows it to be awaited.
then(onFulfilled, onRejected) {
return new Promise<UseFetchReturn<T>>((resolve, reject) => {
until(isFinished)
.toBeTruthy()
.then(() => resolve(state))
.then(() => reject(state))
}).then(onFulfilled, onRejected)
},
}
}
reactive to easily pass them as props to the slotimport type { MouseOptions } from '@vueuse/core'
import { useMouse } from '@vueuse/core'
import { defineComponent, reactive } from 'vue'
export const UseMouse = defineComponent<MouseOptions>({
name: 'UseMouse',
props: ['touch', 'resetOnTouchEnds', 'initialValue'] as unknown as undefined,
setup(props, { slots }) {
const data = reactive(useMouse(props))
return () => {
if (slots.default)
return slots.default(data)
}
},
})
Sometimes a function may have multiple parameters, in that case, you maybe need to create a new interface to merge all the interfaces into a single interface for the component props.
import type { TimeAgoOptions } from '@vueuse/core'
import { useTimeAgo } from '@vueuse/core'
interface UseTimeAgoComponentOptions extends Omit<TimeAgoOptions<true>, 'controls'> {
time: MaybeRef<Date | number | string>
}
export const UseTimeAgo = defineComponent<UseTimeAgoComponentOptions>({ /* ... */ })