Skip to content

refactor: add @__NO_SIDE_EFFECTS__ annotations to all pure functions #4907

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: main
Choose a base branch
from

Conversation

serkodev
Copy link

@serkodev serkodev commented Jul 23, 2025

Before submitting the PR, please make sure you do the following

  • Read the Contributing Guidelines.
  • Read the Pull Request Guidelines.
  • Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
  • Provide a description in this PR that addresses what the PR is solving, or reference the issue that it solves (e.g. fixes #123).
  • Ideally, include relevant tests that fail without this PR but pass with it.

Description

Resolve #4906

Side Effects Notes

Symbol Meaning
🟢 No side effects
🤔🟢 No side effects in most situations
🟠 Potentially side effects
🔴 Side effects

I will add annotations to all functions that has "No side effects 🟢" only.
For other cases, I’d like to gather feedback from the community first.

resolve getter (🤔🟢)

The function resolves getter from parameter immediately. This may contains side effects inside the getter closure.

const foo = computed(() => {
  someSideEffects()
  return 1
})

useRefHistory(foo)

object param (🟢)

The param contains object. This may contains side effects inside the object getter.

const options = {
  get initialValue() {
    someSideEffects()
    return '#000'
  }
}

useEyeDropper(options)

State Pure Notes
createGlobalState 🟢
createInjectionState 🟢
createSharedComposable 🟢
injectLocal 🟢
provideLocal 🔴 provide data
useAsyncState 🔴 callbacks
useDebouncedRefHistory 🤔🟢 used useRefHistory
useLastChanged 🤔🟢 resolve getter source (by watch)
useLocalStorage 🔴 create storage when init
useManualRefHistory 🤔🟢 resolve getter source (by _createHistoryRecord)
useRefHistory 🤔🟢 resolve getter source (by watchIgnorable)
useSessionStorage 🔴 create storage when init
useStorage 🔴 create storage when init
useStorageAsync 🔴 create storage when init
useThrottledRefHistory 🤔🟢 used useRefHistory
Elements Pure Notes
useActiveElement 🟢
useDocumentVisibility 🟢 object param (options)
useDraggable 🔴 make elements draggable
useDropZone 🔴 add listener to element
useElementBounding 🤔🟢 resolve getter target (by watch)
useElementSize 🤔🟢 resolve getter target (by watch)
useElementVisibility 🤔🟢 resolve getter element (by useIntersectionObserver watch )
useIntersectionObserver 🔴 callback
useMouseInElement 🤔🟢 resolve getter target (by useResizeObserver)
closure param (options.eventFilter)
useMutationObserver 🔴 callback
useParentElement 🤔🟢 resolve getter element (by watch)
useResizeObserver 🔴 callback
useWindowFocus 🟢 object param (options)
useWindowScroll 🔴 callback (options.onScroll)
useWindowSize 🟢 object param (options)
Browser Pure Notes
useBluetooth 🟢 run after requestDevice
useBreakpoints 🟢 object param (options)
useBroadcastChannel 🤔🟢 create BroadcastChannel
useBrowserLocation 🟢
useClipboard 🟢
useClipboardItems 🟢
useColorMode 🔴 possibly use useStorage
useCssVar 🔴 possibly set css var
useDark 🔴 possibly use useStorage
useEventListener 🔴 callback
useEyeDropper 🟢 object param (options)
useFavicon 🔴 set favicon
useFileDialog 🤔🟢 resolve getter options.input (by unrefElement)
useFileSystemAccess 🤔🟢 resolve getter options.dataType (by watch)
useFullscreen 🤔🟢 resolve getter target (by unrefElement)
useGamepad 🟢 called navigator.getGamepads()
useImage 🔴 request Image
useMediaControls 🔴 set media src
useMediaQuery 🤔🟢 resolve getter query (by watchEffect)
useMemory 🟢
useObjectUrl 🤔🟢 resolve getter object (by watch)
usePerformanceObserver 🔴 callback
usePermission 🟢 object param (options)
usePreferredColorScheme 🟢 object param (options)
usePreferredContrast 🟢 object param (options)
usePreferredDark 🟢 object param (options)
usePreferredLanguages 🟢 object param (options)
usePreferredReducedMotion 🟢 object param (options)
usePreferredReducedTransparency 🟢 object param (options)
useScreenOrientation 🟢 object param (options)
useScreenSafeArea 🟠 created --vueuse-safe-area-* CSS var
useScriptTag 🔴 callback (onLoaded)
useShare 🟢 object param (options)
useSSRWidth 🟢
useStyleTag 🔴 create style tag
useTextareaAutosize 🔴 callback (options.onResize)
useTextDirection 🟢 object param (options)
useTitle 🔴 set title immediately
useUrlSearchParams 🤔🟢 closure param (options.stringify)
useVibrate 🟢 object param (options)
useWakeLock 🟢
useWebNotification 🔴 request permission immediately
useWebWorker 🔴 load worker immediately
useWebWorkerFn 🔴 terminate worker immediately
Sensors Pure Notes
onClickOutside 🔴 callback
onElementRemoval 🔴 callback
onKeyStroke 🔴 callback
onLongPress 🔴 callback
onStartTyping 🔴 callback
useBattery 🟢 object param (options)
useDeviceMotion 🤔🟢 closure param (options.eventFilter)
useDeviceOrientation 🟢 object param (options)
useDevicePixelRatio 🟢 object param (options)
useDevicesList 🔴 possible requestPermissions immediately
useDisplayMedia 🤔🟢 resolve getter enabled (by watch)
useElementByPoint 🤔🟢 resolve getter multiple (by toValue)
useElementHover 🤔🟢 resolve getter el (by useEventListener)
useFocus 🤔🟢 resolve getter target (by useEventListener)
useFocusWithin 🤔🟢 resolve getter target (by useEventListener)
useFps 🟢 object param (options)
useGeolocation 🔴 grant permission
useIdle 🤔🟢 closure param (options.eventFilter)
useInfiniteScroll 🔴 make element infinite scroll
useKeyModifier 🟢 object param (options)
useMagicKeys 🟠 callback (onEventFired)
useMouse 🤔🟢 resolve getter target (by useEventListener)
useMousePressed 🔴 callback (onPressed)
useNavigatorLanguage 🟢 object param (options)
useNetwork 🟢 object param (options)
useOnline 🟢 object param (options)
usePageLeave 🟢 object param (options)
useParallax 🤔🟢 resolve getter target (by useMouseInElement)
usePointer 🤔🟢 resolve getter target (by useEventListener)
usePointerLock 🟢 object param (options)
usePointerSwipe 🔴 callback (onSwipe)
useScroll 🔴 possible set scroll behavior
useScrollLock 🤔🟢 resolve getter element (by watch)
useSpeechRecognition 🤔🟢 resolve getter lang (by watch)
useSpeechSynthesis 🤔🟢 resolve getter text (by watch > utterance)
useSwipe 🔴 callback (onSwipe)
useTextSelection 🟢 object param (options)
useUserMedia 🤔🟢 resolve getter enabled (by watch)
Network Pure Notes
useEventSource 🔴 connect to EventSource
useFetch 🔴 fetch url
useWebSocket 🔴 connect to WebSocket
Animation Pure Notes
useAnimate 🔴 callback (onReady)
useInterval 🔴 callback
useIntervalFn 🔴 callback
useNow 🟢 object param (options)
useRafFn 🔴 callback
useTimeout 🔴 callback
useTimeoutFn 🔴 callback
useTimestamp 🔴 callback
useTransition 🔴 callback
Component Pure Notes
computedInject 🤔🟢 callback (getter)
createReusableTemplate 🟢 object param (options)
createTemplatePromise 🟢 object param (options)
templateRef 🟢
tryOnBeforeMount 🔴 callback
tryOnBeforeUnmount 🔴 callback
tryOnMounted 🔴 callback
tryOnScopeDispose 🔴 callback
tryOnUnmounted 🔴 callback
unrefElement 🤔🟢 resolve getter elRef (by toValue)
useCurrentElement 🤔🟢 resolve getter rootComponent (by unrefElement)
useMounted 🟢
useTemplateRefsList 🟢
useVirtualList 🤔🟢 resolve getter list (by useWatchForSizes)
useVModel 🟢 object param (options)
useVModels 🟢 object param (options)
Watch Pure Notes
until 🤔🟢 resolve getter r: WatchSource (by watch)
watchArray 🔴 callback
watchAtMost 🔴 callback
watchDebounced 🔴 callback
watchDeep 🔴 callback
watchIgnorable 🔴 callback
watchImmediate 🔴 callback
watchOnce 🔴 callback
watchPausable 🔴 callback
watchThrottled 🔴 callback
watchTriggerable 🔴 callback
watchWithFilter 🔴 callback
whenever 🔴 callback
Reactivity Pure Notes
computedAsync 🔴 callback (onError)
computedEager 🤔🟢 resolve getter fn (by watchEffect)
computedWithControl 🤔🟢 resolve getter source (by watch)
createRef 🟢
extendRef 🔴 modify ref by Object.defineProperty
reactify 🟢
reactifyObject 🟢 object param (options)
reactiveComputed 🤔🟢 resolve getter fn (by reactive)
reactiveOmit 🤔🟢 closure param (predicate)
reactivePick 🤔🟢 closure param (predicate)
refAutoReset 🤔🟢 resolve getter defaultValue (by toValue)
refDebounced 🤔🟢 resolve getter value (by watch)
refDefault 🟢
refThrottled 🤔🟢 resolve getter value (by watch)
refWithControl 🟢
syncRef 🔴
syncRefs 🔴
toReactive 🤔🟢 resolve getter objectRef by (unref)
toRef 🤔🟢 resolve getter r (by vueToRef)
toRefs 🤔🟢 resolve getter r (by _toRefs)
toValue 🤔🟢 resolve getter
Array Pure Notes
useArrayDifference 🟢
useArrayEvery 🟢
useArrayFilter 🟢
useArrayFind 🟢
useArrayFindIndex 🟢
useArrayFindLast 🟢
useArrayIncludes 🟢
useArrayJoin 🟢
useArrayMap 🟢
useArrayReduce 🟢
useArraySome 🟢
useArrayUnique 🟢
useSorted 🔴 dirty mode modify source
Time Pure Notes
useCountdown 🔴 callback (onComplete)
useDateFormat 🟢 object param (options)
useTimeAgo 🟢 object param (options)
Utilities Pure Notes
createEventHook 🟢
createUnrefFn 🟢
get 🤔🟢 resolve getter obj (by unref)
isDefined 🤔🟢 resolve getter obj (by unref)
makeDestructurable 🟢 object param (obj)
set 🔴 set ref value
useAsyncQueue 🔴 executes async task
useBase64 🤔🟢 resolve getter target (by watch)
useCached 🤔🟢 resolve getter refValue (by watch)
useCloned 🤔🟢 resolve getter source (by watch)
useConfirmDialog 🟢 ref param revealed
useCounter 🤔🟢 resolve getter initialValue (by unref)
useCycleList 🤔🟢 resolve getter list (by watch)
useDebounceFn 🟢 object param (options)
useEventBus 🟢
useMemoize 🟢
useOffsetPagination 🔴 callback (onPageChange)
usePrevious 🤔🟢 resolve getter value (by watch)
useStepper 🟢
useSupported 🟢
useThrottleFn 🟢
useTimeoutPoll 🔴 callback (fn)
useToggle 🟢
useToNumber 🟢
useToString 🟢
@Electron Pure Notes
useIpcRenderer 🟢
useIpcRendererInvoke 🔴 invoke channel
useIpcRendererOn 🔴 callback
useZoomFactor 🔴 possible set init zoom factor
useZoomLevel 🔴 possible set init zoom level
@Firebase Pure Notes
useAuth 🟢 object param (auth)
useFirestore 🤔🟢 resolve getter maybeDocRef (by watch)
useRTDB 🟠 listen for docRef (by onValue)
@Integrations Pure Notes
useAsyncValidator 🔴 call validate
useAxios 🔴 fetch request
useChangeCase 🟢
useCookies 🟢
useDrauu 🤔🟢 resolve getter target (by watch)
useFocusTrap 🤔🟢 resolve getter target (by watch)
useFuse 🤔🟢 resolve getter options (by watch)
useIDBKeyval 🔴 callback (onError)
useJwt 🔴 callback (onError)
useNProgress 🔴 set progress
useQRCode 🤔🟢 resolve getter text (by watch)
useSortable 🔴 start Sortable
@Math Pure Notes
createGenericProjection 🟢
createProjection 🟢
logicAnd 🟢
logicNot 🟢
logicOr 🟢
useAbs 🟢
useAverage 🟢
useCeil 🟢
useClamp 🟢
useFloor 🟢
useMath 🟢
useMax 🟢
useMin 🟢
usePrecision 🟢
useProjection 🟢
useRound 🟢
useSum 🟢
useTrunc 🟢
@router Pure Notes
useRouteHash 🟢
useRouteParams 🤔🟢 closure param (options.transform)
useRouteQuery 🤔🟢 closure param (options.transform)
@rxjs Pure Notes
from 🟢
toObserver 🟢
useExtractedObservable 🤔🟢 resolve getter source (by watch)
useObservable 🔴 callback (onError)
useSubject 🔴 callback (onError)
useSubscription 🔴 auto unsubscribe
watchExtractedObservable 🔴 callback

@serkodev serkodev changed the title refactor: add #__NO_SIDE_EFFECTS__ to all pure functions refactor: add #__NO_SIDE_EFFECTS__ annotations to all pure functions Jul 23, 2025
@@ -25,6 +25,7 @@ export interface UseActiveElementOptions extends ConfigurableWindow, Configurabl
* @see https://vueuse.org/useActiveElement
* @param options
*/
/* #__NO_SIDE_EFFECTS__ */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer use @__NO_SIDE_EFFECTS__ and it should be part of the JSdocs above to avoid breaking JSDocs.

Copy link
Author

@serkodev serkodev Jul 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, this also allows __NO_SIDE_EFFECTS__ to appear in JSdocs and in the "Type Declarations" sections of the web documentation.

However, I found that when using TypeScript function overloads, if I place the annotation inside the JSdocs block, it won’t be preserved by the bundler.

Take usePermission as an example:

/**
 * Reactive Permissions API.
 *
 * @see https://vueuse.org/usePermission
 *
+ * @__NO_SIDE_EFFECTS__
 */
export function usePermission(
  permissionDesc: GeneralPermissionDescriptor | GeneralPermissionDescriptor['name'],
  options?: UsePermissionOptions<false>
): UsePermissionReturn
export function usePermission(
  permissionDesc: GeneralPermissionDescriptor | GeneralPermissionDescriptor['name'],
  options: UsePermissionOptions<true>,
): UsePermissionReturnWithControls
export function usePermission(
  permissionDesc: GeneralPermissionDescriptor | GeneralPermissionDescriptor['name'],
  options: UsePermissionOptions<boolean> = {},
): UsePermissionReturn | UsePermissionReturnWithControls {
  • __NO_SIDE_EFFECTS__ gets stripped in index.mjs by the bundler

If I instead move the entire JSdoc block to the last overload implementation:

export function usePermission(
  permissionDesc: GeneralPermissionDescriptor | GeneralPermissionDescriptor['name'],
  options?: UsePermissionOptions<false>
): UsePermissionReturn
export function usePermission(
  permissionDesc: GeneralPermissionDescriptor | GeneralPermissionDescriptor['name'],
  options: UsePermissionOptions<true>,
): UsePermissionReturnWithControls
+ /**
+  * Reactive Permissions API.
+  *
+  * @see https://vueuse.org/usePermission
+  *
+  * @__NO_SIDE_EFFECTS__
+  */
export function usePermission(
  permissionDesc: GeneralPermissionDescriptor | GeneralPermissionDescriptor['name'],
  options: UsePermissionOptions<boolean> = {},
): UsePermissionReturn | UsePermissionReturnWithControls {
  • __NO_SIDE_EFFECTS__ is correctly preserved in index.mjs
  • ❌ But the JSdoc is missing from index.d.mts

Therefore, in the case of function overloads, to ensure both the JSdocs and the annotation are retained properly, I’m considering placing __NO_SIDE_EFFECTS__ in both the JSdoc and on the final overload implementation.

+ /**
+  * Reactive Permissions API.
+  *
+  * @see https://vueuse.org/usePermission
+  *
+  * @__NO_SIDE_EFFECTS__ <- 🧑‍💻 for human read
+  */
export function usePermission(
  permissionDesc: GeneralPermissionDescriptor | GeneralPermissionDescriptor['name'],
  options?: UsePermissionOptions<false>
): UsePermissionReturn
export function usePermission(
  permissionDesc: GeneralPermissionDescriptor | GeneralPermissionDescriptor['name'],
  options: UsePermissionOptions<true>,
): UsePermissionReturnWithControls
+ /* @__NO_SIDE_EFFECTS__ */ <- 🤖 for bundler read
export function usePermission(
  permissionDesc: GeneralPermissionDescriptor | GeneralPermissionDescriptor['name'],
  options: UsePermissionOptions<boolean> = {},
): UsePermissionReturn | UsePermissionReturnWithControls {

Is there anything else I might have overlooked?

@serkodev serkodev marked this pull request as ready for review July 24, 2025 15:06
@dosubot dosubot bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jul 24, 2025
@serkodev serkodev changed the title refactor: add #__NO_SIDE_EFFECTS__ annotations to all pure functions refactor: add @__NO_SIDE_EFFECTS__ annotations to all pure functions Jul 24, 2025
@ilyaliao ilyaliao requested a review from antfu July 24, 2025 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size:M This PR changes 30-99 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Ensure unused values created by pure functions (like createGlobalState) are tree-shakeable
2 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy