Skip to content

Vue Best Practices

Props Default Values

การกําหนดค่าเริ่มต้นให้กับ props แบบเดิมใช้ computed ซึ่งทําให้โค้ดยาวและซับซ้อน แบบใหม่ใช้ withDefaults ที่ทําให้โค้ดสั้นลงและอ่านง่ายขึ้น

❌ แบบเดิม

ts
// ใช้ computed เพื่อกําหนดค่า default
const props = defineProps<{
  orientation?: "horizontal" | "vertical";
}>();

const orientation = computed(() => props.orientation || "vertical");

✅ แบบใหม่

ts
withDefaults(
  defineProps<{
    orientation?: "horizontal" | "vertical";
  }>(),
  {
    orientation: "vertical",
  },
);

Event Handling

การจัดการ event ใน Vue มีการพัฒนาให้ type-safe มากขึ้น แบบเดิมไม่มีการระบุ type ของ event ทําให้อาจเกิดข้อผิดพลาดได้ แบบใหม่ใช้ defineEmits พร้อม type ที่ชัดเจน ทําให้ TypeScript สามารถตรวจจับ error ได้ตั้งแต่ตอน development

❌ แบบเดิม

vue
<template>
  <button @click="handleClick">Click me</button>
</template>

<script setup>
const handleClick = (event: MouseEvent) => {
  // handle click
}
</script>

Lifecycle Hooks

การจัดการ lifecycle hooks แบบเดิมมักจะแยก setup และ cleanup ออกจากกัน ทําให้ยากต่อการดูแลรักษา แบบใหม่รวม setup และ cleanup ไว้ด้วยกันใน composable function ทําให้โค้ดเป็นระเบียบและป้องกันการลืม cleanup

❌ แบบเดิม

ts
onMounted(() => {
  // setup
});
onUnmounted(() => {
  // cleanup
});

✅ แบบใหม่

ts
// composable with automatic cleanup
function useSetup() {
  onMounted(() => {
    const handler = setInterval(() => {
      // do something
    }, 1000);

    // automatic cleanup
    onUnmounted(() => {
      clearInterval(handler);
    });
  });
}

Error Handling

การจัดการ error แบบเดิมใช้ try-catch ธรรมดา ซึ่งอาจทําให้ลืมจัดการ error ในบางกรณี แบบใหม่ใช้ useAsyncData หรือ ErrorBoundary component ที่ช่วยจัดการ error ได้เป็นระบบมากขึ้น และมี type safety

❌ แบบเดิม

ts
try {
  await someAsyncOperation();
} catch (error) {
  console.error(error);
}

✅ แบบใหม่

ts
const { error } = await useAsyncData(async () => {
	return await someAsyncOperation()
})

// หรือใช้ error boundary
<template>
	<ErrorBoundary @error="handleError">
		<AsyncComponent />
	</ErrorBoundary>
</template>

Computed Properties

การใช้ computed properties แบบเดิมต้องใช้ .value เมื่อเข้าถึงค่า ref ทําให้โค้ดยาวและอ่านยาก แบบใหม่ใช้ reactive sugar syntax ($ref, $computed) ที่ช่วยลดการใช้ .value ทําให้โค้ดสั้นและอ่านง่ายขึ้น

❌ แบบเดิม

ts
const firstName = ref("");
const lastName = ref("");

const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`.trim();
});

✅ แบบใหม่

ts
// ใช้ reactive sugar syntax
const { firstName, lastName } = $ref("");

// ไม่ต้องใช้ .value
const fullName = $computed(() => `${firstName} ${lastName}`.trim());

Composables Organization

การจัดการ state และ logic แบบเดิมเขียนรวมกันในคอมโพเนนต์ แบบใหม่แยกเป็น composable ทําให้นํากลับมาใช้ใหม่ได้และทดสอบง่ายขึ้น

❌ แบบเดิม

ts
const loading = ref(false);
const error = ref(null);
const data = ref(null);

const fetchData = async () => {
  loading.value = true;
  try {
    data.value = await api.getData();
  } catch (e) {
    error.value = e;
  } finally {
    loading.value = false;
  }
};

✅ แบบใหม่

ts
function useFetch() {
  const loading = ref(false);
  const error = ref(null);
  const data = ref(null);

  async function execute() {
    loading.value = true;
    try {
      data.value = await api.getData();
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  }

  return {
    loading,
    error,
    data,
    execute,
  };
}

// การใช้งาน
const { loading, error, data, execute } = useFetch();

Props Validation

การตรวจสอบ props แบบเดิมใช้ validator function แบบใหม่ใช้ TypeScript type ทําให้ตรวจสอบได้ตั้งแต่ตอน compile

❌ แบบเดิม

ts
const props = defineProps({
  size: {
    type: String,
    validator: (value) => ["small", "medium", "large"].includes(value),
  },
});

✅ แบบใหม่

ts
const props = defineProps<{
  size: "small" | "medium" | "large";
}>();

Provide/Inject

การใช้ provide/inject แบบเดิมไม่มี type safety แบบใหม่ใช้ Symbol และ TypeScript ทําให้ type-safe

❌ แบบเดิม

ts
// parent
provide("theme", ref("dark"));

// child
const theme = inject("theme");

✅ แบบใหม่

ts
// types.ts
export interface ThemeInjection {
  theme: Ref<"light" | "dark">;
  toggleTheme: () => void;
}

export const themeKey = Symbol() as InjectionKey<ThemeInjection>;

// parent
provide(themeKey, {
  theme: ref("dark"),
  toggleTheme: () => theme.value = theme.value === "dark" ? "light" : "dark",
});

// child
const { theme, toggleTheme } = inject(themeKey)!;

✅ แบบใหม่

vue
<template>
  <button @click="(e) => emit('click', e)">Click me</button>
</template>

<script setup>
const emit = defineEmits<{
  (e: 'click', event: MouseEvent): void
}>()
</script>

Reactive Variables

การใช้ reactive variables แบบเดิมต้องใช้ .value บ่อยๆ แบบใหม่ใช้ $ syntax ทําให้โค้ดสั้นและอ่านง่ายขึ้น

❌ แบบเดิม

ts
// ต้องใช้ .value
const count = ref(0);
const doubled = computed(() => count.value * 2);

✅ แบบใหม่

ts
// ใช้ $ syntax ไม่ต้องใช้ .value
const { count, doubled } = $(useCounter());

function useCounter() {
  const count = 0;
  const doubled = computed(() => count * 2);
  return { count, doubled };
}

Template Refs

การใช้ template refs แบบเดิมต้องใช้ .value เมื่อเข้าถึงค่า ref ทําให้โค้ดยาวและอ่านยาก แบบใหม่ใช้ $ref syntax ที่ช่วยลดการใช้ .value ทําให้โค้ดสั้นและอ่านง่ายขึ้น

❌ แบบเดิม

vue
<template>
  <div ref="elementRef"></div>
</template>

<script setup>
const elementRef = ref<HTMLDivElement>()

onMounted(() => {
  console.log(elementRef.value)
})
</script>

✅ แบบใหม่

vue
<template>
  <div ref="el"></div>
</template>

<script setup>
const el = $ref<HTMLDivElement>()

// ไม่ต้องใช้ .value
console.log(el)
</script>

Watchers

การใช้ watchers แบบเดิมต้องระบุ callback function แยกต่างหาก แบบใหม่ใช้ watchEffect ที่สามารถ track dependencies อัตโนมัติ ทําให้โค้ดสั้นและเข้าใจง่ายขึ้น

❌ แบบเดิม

ts
watch(
  () => props.value,
  (newValue) => {
    // do something
  },
);

✅ แบบใหม่

ts
watchEffect(() => {
  // automatically tracks props.value
  console.log(props.value);
});

v-model

การใช้ v-model แบบเดิมต้องกําหนด props และ emit events แยกกัน แบบใหม่ใช้ defineModel ที่รวมทั้งสองส่วนเข้าด้วยกัน ทําให้โค้ดสั้นและเข้าใจง่ายขึ้น

❌ แบบเดิม

vue
<template>
	<input
		:value="modelValue"
		@input="$emit('update:modelValue', $event.target.value)"
	>
</template>

<script setup>
defineProps<{
	modelValue: string
}>()
defineEmits<{
	(e: 'update:modelValue', value: string): void
}>()
</script>

✅ แบบใหม่

vue
<template>
	<input v-model="value">
</template>

<script setup>
const value = defineModel<string>()
</script>

Async Components

การโหลด component แบบ async แบบเดิมใช้ defineAsyncComponent เพียงอย่างเดียว แบบใหม่ใช้ร่วมกับ Suspense และมีการจัดการ loading state ที่ดีกว่า

❌ แบบเดิม

ts
// โหลด component แบบ async อย่างเดียว
const AsyncComponent = defineAsyncComponent(() =>
  import("./components/AsyncComponent.vue")
);

✅ แบบใหม่

vue
<template>
  <Suspense>
    <!-- ใช้ Suspense ในการจัดการ async state -->
    <AsyncComponent />

    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from "vue";

// กําหนดค่า options เพิ่มเติมเพื่อจัดการ loading state
const AsyncComponent = defineAsyncComponent({
  loader: () => import("./components/AsyncComponent.vue"),
  loadingComponent: LoadingSpinner,
  delay: 200, // แสดง loading หลังจาก 200ms
  timeout: 3000, // timeout หลัง 3 วินาที
});
</script>