Advanced Composables — Series 3

Preview — 3 of 10 questions

Why does this composable need to call triggerRef explicitly?

javascript
import { shallowRef, triggerRef } from 'vue'

export function useLargeDataset(loader) {
  const dataset = shallowRef(null)

  async function load() {
    const result = await loader()
    dataset.value.rows = result.rows // mutate in place, not reassign
    triggerRef(dataset)
  }

  return { dataset, load }
}
AtriggerRef is unnecessary here — shallowRef still deeply tracks nested mutations exactly like a regular ref would
BtriggerRef permanently converts dataset into a deeply-reactive ref from that point forward
CtriggerRef is only valid inside <script setup>, not inside a standalone composable function — this code throws
DshallowRef only makes reassigning .value reactive, not mutations to whatever object .value currently points to — mutating dataset.value.rows in place doesn't notify anything watching dataset, so triggerRef(dataset) is needed to manually force dependent effects (components rendering dataset.value, computeds reading it) to re-run, without paying the cost of deep-reactive conversion on a potentially huge dataset

What state does this composable expose, and why three separate refs instead of one object?

javascript
import { ref } from 'vue'

export function useAsyncData(fetcher) {
  const data = ref(null)
  const error = ref(null)
  const isLoading = ref(false)

  async function execute() {
    isLoading.value = true
    error.value = null
    try {
      data.value = await fetcher()
    } catch (e) {
      error.value = e
    } finally {
      isLoading.value = false
    }
  }

  return { data, error, isLoading, execute }
}
AThree separate refs is a mistake — they should be merged into one reactive({ data, error, isLoading }) object to avoid "prop drilling" of individual refs
BSplitting into three independent refs is deliberate: a template only needs v-if="isLoading" to react to loading state, without needing to also re-render when data itself changes for unrelated reasons — combining everything into one reactive object would still work, but grouping status flags separately from the payload is a common, readable convention for this kind of composable, letting callers destructure with toRefs (or use them directly, as returned here) and depend on only what they need
CThis pattern can't track loading state correctly — isLoading will always read false due to Vue's async batching
DReturning three refs instead of one object is required because defineExpose only accepts flat top-level refs, never nested objects

Why does boxHeight read the old height inside this watcher, not the new one?

javascript
<script setup>
import { ref, watch } from 'vue'

const items = ref([1, 2, 3])
const boxRef = ref(null)

watch(items, () => {
  // items.value just changed — but has the DOM re-rendered yet?
  console.log('box height:', boxRef.value.offsetHeight)
})

function addItem() {
  items.value.push(items.value.length + 1)
}
</script>
AIt doesn't — watch callbacks always run strictly after the DOM has been patched to reflect the change that triggered them
BboxRef.value is always null inside a watcher, regardless of timing, so this always throws
CBy default, watch's flush option is 'pre': the callback runs before the component re-renders and patches the DOM for that change, specifically so watchers can safely make further state changes without causing extra render passes. That means reading DOM measurements (like offsetHeight) inside a default-flush watcher reflects the DOM as it looked before this update, not after — { flush: 'post' } is needed to read post-update DOM state reliably
DboxHeight reads stale data only in production builds; in development, flush timing is different

Sign up free to play

Answer all 10 questions (7 more), see explanations for every answer, and track your score.