Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Vue 3 debugging reference for reactivity issues, computed errors, watcher bugs, async failures, and SSR hydration problems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/composable-avoid-hidden-side-effects.md
1---2title: Avoid Hidden Side Effects in Composables3impact: HIGH4impactDescription: Side effects hidden in composables make debugging difficult and create implicit coupling between components5type: best-practice6tags: [vue3, composables, composition-api, side-effects, provide-inject, global-state]7---89# Avoid Hidden Side Effects in Composables1011**Impact: HIGH** - Composables should encapsulate stateful logic, not hide side effects that affect things outside their scope. Hidden side effects like modifying global state, using provide/inject internally, or manipulating the DOM directly make composables unpredictable and hard to debug.1213When a composable has unexpected side effects, consumers can't reason about what calling it will do. This leads to bugs that are difficult to trace and composables that can't be safely reused.1415## Task Checklist1617- [ ] Avoid using provide/inject inside composables (make dependencies explicit)18- [ ] Don't modify Pinia/Vuex store state internally (accept store as parameter instead)19- [ ] Don't manipulate DOM directly (use template refs passed as arguments)20- [ ] Document any unavoidable side effects clearly21- [ ] Keep composables focused on returning reactive state and methods2223**Incorrect:**24```javascript25// WRONG: Hidden provide/inject dependency26export function useTheme() {27// Consumer has no idea this depends on a provided theme28const theme = inject('theme') // What if nothing provides this?2930const isDark = computed(() => theme?.mode === 'dark')31return { isDark }32}3334// WRONG: Modifying global store internally35import { useUserStore } from '@/stores/user'3637export function useLogin() {38const userStore = useUserStore()3940async function login(credentials) {41const user = await api.login(credentials)42// Hidden side effect: modifying global state43userStore.setUser(user)44userStore.setToken(user.token)45// Consumer doesn't know the store was modified!46}4748return { login }49}5051// WRONG: Hidden DOM manipulation52export function useFocusTrap() {53onMounted(() => {54// Which element? Consumer has no control55document.querySelector('.modal')?.focus()56})57}5859// WRONG: Hidden provide that affects descendants60export function useFormContext() {61const form = reactive({ values: {}, errors: {} })62// Components calling this have no idea it provides something63provide('form-context', form)64return form65}66```6768**Correct:**69```javascript70// CORRECT: Explicit dependency injection71export function useTheme(injectedTheme) {72// If no theme passed, consumer must handle it73const theme = injectedTheme ?? { mode: 'light' }7475const isDark = computed(() => theme.mode === 'dark')76return { isDark }77}7879// Usage - dependency is explicit80const theme = inject('theme', { mode: 'light' })81const { isDark } = useTheme(theme)8283// CORRECT: Return actions, let consumer decide when to call them84export function useLogin() {85const user = ref(null)86const token = ref(null)87const isLoading = ref(false)88const error = ref(null)8990async function login(credentials) {91isLoading.value = true92error.value = null93try {94const response = await api.login(credentials)95user.value = response.user96token.value = response.token97return response98} catch (e) {99error.value = e100throw e101} finally {102isLoading.value = false103}104}105106return { user, token, isLoading, error, login }107}108109// Consumer decides what to do with the result110const { user, token, login } = useLogin()111const userStore = useUserStore()112113async function handleLogin(credentials) {114await login(credentials)115// Consumer explicitly updates the store116userStore.setUser(user.value)117userStore.setToken(token.value)118}119120// CORRECT: Accept element as parameter121export function useFocusTrap(targetRef) {122onMounted(() => {123targetRef.value?.focus()124})125126onUnmounted(() => {127// Cleanup focus trap128})129}130131// Usage - consumer controls which element132const modalRef = ref(null)133useFocusTrap(modalRef)134135// CORRECT: Separate composable from provider136export function useFormContext() {137const form = reactive({ values: {}, errors: {} })138return form139}140141// In parent component - explicit provide142const form = useFormContext()143provide('form-context', form)144```145146## Acceptable Side Effects (With Documentation)147148Some side effects are acceptable when they're the core purpose of the composable:149150```javascript151/**152* Tracks mouse position globally.153*154* SIDE EFFECTS:155* - Adds 'mousemove' event listener to window (cleaned up on unmount)156*157* @returns {Object} Mouse coordinates { x, y }158*/159export function useMouse() {160const x = ref(0)161const y = ref(0)162163// This side effect is the whole point of the composable164// and is properly cleaned up165onMounted(() => window.addEventListener('mousemove', update))166onUnmounted(() => window.removeEventListener('mousemove', update))167168function update(event) {169x.value = event.pageX170y.value = event.pageY171}172173return { x, y }174}175```176177## Pattern: Dependency Injection for Flexibility178179```javascript180// Composable accepts its dependencies181export function useDataFetcher(apiClient, cache = null) {182const data = ref(null)183184async function fetch(url) {185if (cache) {186const cached = cache.get(url)187if (cached) {188data.value = cached189return190}191}192193data.value = await apiClient.get(url)194cache?.set(url, data.value)195}196197return { data, fetch }198}199200// Usage - dependencies are explicit and testable201const apiClient = inject('apiClient')202const cache = inject('cache', null)203const { data, fetch } = useDataFetcher(apiClient, cache)204```205206## Reference207- [Vue.js Composables](https://vuejs.org/guide/reusability/composables.html)208- [Common Mistakes Creating Composition Functions](https://www.telerik.com/blogs/common-mistakes-creating-composition-functions-vue)209