charging-cabinet/software/h5-uniapp/.agents/skills/vue-best-practices/reference/prefer-props-emit-over-component-refs.md
12451 b553c4e519 feat: API 统一 + 设备详情补全 + H5 导航优化
- API 路由统一: 移除 /api/h5/,统一 /api/pms/ 入口
- 二进制改名: api-server -> pms-api(与服务名一致)
- nginx: 合并 /api/ + /pms/api/ 为 /api/pms/
- 新增 docs/API.md 接口文档
- 后端: CabinetDetail 增加 rssi/pow_fail/address/soc 等字段
- Redis 管道批量读取仓体实时数据(SOC/电压/电流/功率)
- DB: 所有 TIMESTAMP 列改为 DATETIME(修复类型不匹配)
- 用户管理: 新增创建时间/修改时间列,状态改用 Switch
- 角色管理/用户管理: 操作按钮改为图标
- PC 设备详情: 基本信息补全 RSSI/ICCID/电源/地址
- PC 卡片: 通道统计改用 Lucide 图标,颜色统一
- PC 二维码: canvas 渲染,含编号文字,支持右键下载
- H5: 按 uni-app 规范修正导航(navigateTo 替代 reLaunch)
- H5: 详情页加返回导航条
- H5: 扫码支持摄像头(jsQR)+ ?id= 参数
- H5: 底部 tabBar 在所有页面常显
- H5: 首页统计改为 6 项(设备/通道/空闲/充电/充满/故障)
- H5: 下拉刷新 / 上拉加载
- 产品首页: 添加业务联系
- 文档: 清理过时文件,补充缺失规范章节
2026-07-04 04:03:02 +08:00

5.3 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Prefer Props and Emit Over Component Refs MEDIUM Component refs create tight coupling and break component abstraction best-practice
vue3
component-refs
props
emit
component-design
architecture

Prefer Props and Emit Over Component Refs

Impact: MEDIUM - Using template refs to access child component internals creates tight coupling between parent and child. This makes components harder to maintain, refactor, and reuse. Props and emit provide a cleaner contract-based API that preserves component encapsulation.

Component refs should be reserved for imperative actions (focus, scroll, animations) that can't be expressed declaratively.

Task Checklist

  • Use props for passing data down to children
  • Use emit for communicating events up to parents
  • Only use component refs for imperative DOM operations
  • If using refs, expose a minimal, documented API via defineExpose
  • Consider if the interaction can be expressed declaratively

Incorrect:

<!-- ParentComponent.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import UserForm from './UserForm.vue'

const formRef = ref(null)

// WRONG: Reaching into child's internals
function submitForm() {
  // Tight coupling - parent knows child's internal structure
  if (formRef.value.isValid) {
    const data = formRef.value.formData
    formRef.value.setSubmitting(true)
    api.submit(data).then(() => {
      formRef.value.setSubmitting(false)
      formRef.value.reset()
    })
  }
}

// WRONG: Parent managing child's state
function prefillForm(userData) {
  formRef.value.formData.name = userData.name
  formRef.value.formData.email = userData.email
}
</script>

<template>
  <UserForm ref="formRef" />
  <button @click="submitForm">Submit</button>
</template>
<!-- UserForm.vue - exposing too much -->
<script setup>
import { ref, reactive } from 'vue'

const formData = reactive({ name: '', email: '' })
const isValid = ref(false)
const isSubmitting = ref(false)

function setSubmitting(value) {
  isSubmitting.value = value
}

function reset() {
  formData.name = ''
  formData.email = ''
}

// WRONG: Exposing internal state details
defineExpose({
  formData,
  isValid,
  isSubmitting,
  setSubmitting,
  reset
})
</script>

Correct:

<!-- ParentComponent.vue -->
<script setup>
import { ref } from 'vue'
import UserForm from './UserForm.vue'

const initialData = ref({ name: '', email: '' })
const isSubmitting = ref(false)

// CORRECT: Child communicates via events
function handleSubmit(formData) {
  isSubmitting.value = true
  api.submit(formData).finally(() => {
    isSubmitting.value = false
  })
}

function handleValidChange(isValid) {
  console.log('Form validity:', isValid)
}
</script>

<template>
  <!-- CORRECT: Props down, events up -->
  <UserForm
    :initial-data="initialData"
    :submitting="isSubmitting"
    @submit="handleSubmit"
    @valid-change="handleValidChange"
  />
</template>
<!-- UserForm.vue - clean props/emit interface -->
<script setup>
import { reactive, computed, watch } from 'vue'

const props = defineProps({
  initialData: { type: Object, default: () => ({}) },
  submitting: { type: Boolean, default: false }
})

const emit = defineEmits(['submit', 'valid-change'])

const formData = reactive({ ...props.initialData })

const isValid = computed(() => {
  return formData.name.length > 0 && formData.email.includes('@')
})

watch(isValid, (valid) => {
  emit('valid-change', valid)
})

function handleSubmit() {
  if (isValid.value) {
    emit('submit', { ...formData })
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="formData.name" :disabled="submitting" />
    <input v-model="formData.email" :disabled="submitting" />
    <button type="submit" :disabled="!isValid || submitting">
      {{ submitting ? 'Submitting...' : 'Submit' }}
    </button>
  </form>
</template>

When Component Refs ARE Appropriate

<!-- CORRECT: Refs for imperative DOM operations -->
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

const inputRef = ref(null)

// Imperative focus action - good use of refs
function focusInput() {
  inputRef.value?.focus()
}
</script>

<template>
  <CustomInput ref="inputRef" v-model="text" />
  <button @click="focusInput">Focus Input</button>
</template>
<!-- CustomInput.vue - minimal imperative API -->
<script setup>
import { ref } from 'vue'

const inputEl = ref(null)

// Only expose imperative methods
defineExpose({
  focus: () => inputEl.value?.focus(),
  blur: () => inputEl.value?.blur(),
  select: () => inputEl.value?.select()
})
</script>

<template>
  <input ref="inputEl" v-bind="$attrs" />
</template>

Summary

Use Case Approach
Pass data to child Props
Child notifies parent Emit events
Two-way binding v-model (props + emit)
Focus, scroll, animate Component ref with minimal expose
Access child internal state Refactor to use props/emit

Reference