Files
agent-skills/skills/vue-best-practices/reference/ts-shallowref-for-dynamic-components.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06:00

5.9 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Use shallowRef for Dynamic Component References MEDIUM Storing components in reactive() or ref() triggers Vue warnings and can cause performance issues gotcha
typescript
shallowRef
dynamic-components
reactivity
performance

Use shallowRef for Dynamic Component References

Impact: MEDIUM - When storing Vue components in reactive state for dynamic rendering, using ref() or reactive() causes Vue warnings and unnecessary reactivity overhead. Use shallowRef() instead.

Task Checklist

  • Use shallowRef for storing component references
  • Use markRaw when storing components in reactive objects
  • Avoid wrapping component definitions with deep reactivity
  • Check console for "[Vue warn]: Vue received a Component that was made a reactive object"

The Problem

Vue components are objects with internal properties that should not be made reactive. When you store a component in ref() or reactive(), Vue traverses all properties deeply, which:

  1. Triggers a console warning
  2. Creates unnecessary reactive proxies
  3. Can cause subtle bugs with component identity
  4. Impacts performance

Incorrect - Using ref() for components:

import { ref } from 'vue'
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

// BAD: Vue will warn about making component reactive
const currentComponent = ref(ComponentA)

function switchComponent() {
  currentComponent.value = ComponentB
}

Console warning:

[Vue warn]: Vue received a Component that was made a reactive object.
This can lead to unnecessary performance overhead and should be avoided
by marking the component with `markRaw` or using `shallowRef` instead of `ref`.

Solution 1: Use shallowRef

shallowRef only makes the .value reference reactive, not the contents:

import { shallowRef, type Component } from 'vue'
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

// CORRECT: shallowRef doesn't deep-proxy the component
const currentComponent = shallowRef<Component>(ComponentA)

function switchComponent() {
  currentComponent.value = ComponentB
}
<template>
  <component :is="currentComponent" />
</template>

Solution 2: Use markRaw in Reactive Objects

When components are part of a larger reactive object:

import { reactive, markRaw, type Component } from 'vue'
import TabHome from './TabHome.vue'
import TabProfile from './TabProfile.vue'
import TabSettings from './TabSettings.vue'

interface Tab {
  name: string
  component: Component
}

// CORRECT: markRaw prevents reactivity on component objects
const tabs = reactive<Tab[]>([
  { name: 'Home', component: markRaw(TabHome) },
  { name: 'Profile', component: markRaw(TabProfile) },
  { name: 'Settings', component: markRaw(TabSettings) }
])

const activeTab = shallowRef<Tab>(tabs[0])
<template>
  <div class="tabs">
    <button
      v-for="tab in tabs"
      :key="tab.name"
      @click="activeTab = tab"
    >
      {{ tab.name }}
    </button>
  </div>
  <component :is="activeTab.component" />
</template>

TypeScript Typing

For proper TypeScript support with dynamic components:

import { shallowRef, type Component, type DefineComponent } from 'vue'

// Generic component type
const currentComponent = shallowRef<Component | null>(null)

// Or more specific with props
interface MyComponentProps {
  title: string
}

const currentComponent = shallowRef<DefineComponent<MyComponentProps> | null>(null)

Dynamic Import with shallowRef

When using dynamic imports for code splitting:

import { shallowRef, defineAsyncComponent, type Component } from 'vue'

const currentComponent = shallowRef<Component | null>(null)

async function loadComponent(name: string) {
  const component = defineAsyncComponent(
    () => import(`./components/${name}.vue`)
  )
  currentComponent.value = component
}

Component Registry Pattern

For tab systems or wizard-like interfaces:

import { shallowRef, markRaw, type Component } from 'vue'

// Type-safe component registry
const componentRegistry = {
  home: markRaw(defineAsyncComponent(() => import('./Home.vue'))),
  about: markRaw(defineAsyncComponent(() => import('./About.vue'))),
  contact: markRaw(defineAsyncComponent(() => import('./Contact.vue')))
} as const

type ComponentKey = keyof typeof componentRegistry

const currentView = shallowRef<ComponentKey>('home')

// Computed to get current component
const currentComponent = computed(() => componentRegistry[currentView.value])
<template>
  <component :is="currentComponent" />
</template>

When to Use Each Approach

Scenario Solution
Single dynamic component reference shallowRef
Component in reactive array/object markRaw on component
Component map/registry markRaw each component
Async components defineAsyncComponent + shallowRef

Common Mistakes

Mistake 1: Using computed with ref

// BAD: Still triggers warning
const components = ref([ComponentA, ComponentB])
const current = computed(() => components.value[index.value])

// GOOD: Use shallowRef for the array
const components = shallowRef([ComponentA, ComponentB])

Mistake 2: Forgetting markRaw in map

// BAD: Components in map become reactive
const routes = reactive(new Map([
  ['home', HomeComponent],
  ['about', AboutComponent]
]))

// GOOD: Mark each component as raw
const routes = reactive(new Map([
  ['home', markRaw(HomeComponent)],
  ['about', markRaw(AboutComponent)]
]))

Reference