Vue 3 で導入された Composition API は、React Hooks に続く、フロントエンドフレームワークにおけるもう一つの重要な関数型プログラミングの実践だ。開発者は関数単位でコンポーネントのロジックを整理でき、Options API が抱える「複雑なコンポーネントでロジックが散在する」という問題を解決する。本記事では複数の観点から両者の API スタイルを比較し、移行のアドバイスを提示する。
Options APIの問題点
Options API では、1つの機能のロジックが data・computed・methods・watch などの複数のオプションに分散してしまう:
vue
<script>
export default {
data() {
return {
// 用户搜索相关
searchQuery: '',
searchResults: [],
isSearching: false,
// 分页相关
currentPage: 1,
pageSize: 10,
total: 0,
// 选中项
selectedItems: [],
};
},
computed: {
// 搜索相关
hasResults() {
return this.searchResults.length > 0;
},
// 分页相关
totalPages() {
return Math.ceil(this.total / this.pageSize);
},
pageInfo() {
return `第 ${this.currentPage} / ${this.totalPages} 页`;
},
// 选中项相关
selectedCount() {
return this.selectedItems.length;
},
},
methods: {
// 搜索相关
async handleSearch() {
this.isSearching = true;
try {
const result = await api.search(this.searchQuery, {
page: this.currentPage,
pageSize: this.pageSize,
});
this.searchResults = result.items;
this.total = result.total;
} finally {
this.isSearching = false;
}
},
// 分页相关
goToPage(page) {
this.currentPage = page;
this.handleSearch();
},
// 选中项相关
toggleSelect(item) {
const index = this.selectedItems.findIndex(i => i.id === item.id);
if (index > -1) {
this.selectedItems.splice(index, 1);
} else {
this.selectedItems.push(item);
}
},
},
watch: {
searchQuery() {
this.currentPage = 1;
this.handleSearch();
},
},
};
</script>
問題は明らかだ。検索機能のロジックが複数のオプションに散らばっており、全体の流れを理解するにはあちこち行き来する必要がある。コンポーネントが大きくなるほどこの問題は深刻になる。
Composition APIへのリファクタリング
Composition API を使えば、同じ機能のロジックをまとめて整理できる:
vue
<script>
import { ref, computed, watch } from 'vue';
// 搜索逻辑封装为 composable
function useSearch() {
const searchQuery = ref('');
const searchResults = ref([]);
const isSearching = ref(false);
const total = ref(0);
const hasResults = computed(() => searchResults.value.length > 0);
async function search(page = 1, pageSize = 10) {
isSearching.value = true;
try {
const result = await api.search(searchQuery.value, { page, pageSize });
searchResults.value = result.items;
total.value = result.total;
} finally {
isSearching.value = false;
}
}
return {
searchQuery,
searchResults,
isSearching,
total,
hasResults,
search,
};
}
// 分页逻辑封装为 composable
function usePagination() {
const currentPage = ref(1);
const pageSize = ref(10);
const totalPages = computed(() =>
Math.ceil(usePagination.total?.value / pageSize.value)
);
const pageInfo = computed(() =>
`第 ${currentPage.value} / ${totalPages.value} 页`
);
function goToPage(page) {
currentPage.value = page;
}
return {
currentPage,
pageSize,
totalPages,
pageInfo,
goToPage,
};
}
// 选中逻辑封装为 composable
function useSelection() {
const selectedItems = ref([]);
const selectedCount = computed(() => selectedItems.value.length);
function toggleSelect(item) {
const index = selectedItems.value.findIndex(i => i.id === item.id);
if (index > -1) {
selectedItems.value.splice(index, 1);
} else {
selectedItems.value.push(item);
}
}
function clearSelection() {
selectedItems.value = [];
}
return {
selectedItems,
selectedCount,
toggleSelect,
clearSelection,
};
}
export default {
setup() {
const {
searchQuery,
searchResults,
isSearching,
total,
hasResults,
search,
} = useSearch();
const {
currentPage,
pageSize,
pageInfo,
goToPage,
} = usePagination();
const {
selectedItems,
selectedCount,
toggleSelect,
} = useSelection();
// 组合逻辑
watch(searchQuery, () => {
currentPage.value = 1;
search(currentPage.value, pageSize.value);
});
function onPageChange(page) {
goToPage(page);
search(page, pageSize.value);
}
return {
searchQuery,
searchResults,
isSearching,
hasResults,
pageInfo,
selectedItems,
selectedCount,
toggleSelect,
onPageChange,
};
},
};
</script>
各機能のロジックは1つの関数に集約され、見通しがよく再利用もしやすい。
再利用ロジックの比較
Options API での再利用:Mixins
js
// mixins/searchMixin.js
export default {
data() {
return {
searchQuery: '',
searchResults: [],
};
},
methods: {
async search() { /* ... */ },
},
};
// 使用例
export default {
mixins: [searchMixin, paginationMixin],
// 问题:
// 1. 命名冲突
// 2. 数据来源不清晰
// 3. mixin 之间不能传递参数
};
Composition API での再利用:Composables
js
// composables/useSearch.js
import { ref } from 'vue';
export function useSearch(apiEndpoint) {
// 可以接受参数
const query = ref('');
const results = ref([]);
async function search() {
const response = await fetch(`${apiEndpoint}?q=${query.value}`);
results.value = await response.json();
}
return { query, results, search };
}
// 使用例
import { useSearch } from './composables/useSearch';
import { usePagination } from './composables/usePagination';
export default {
setup() {
// 每次调用创建独立实例,互不干扰
const userSearch = useSearch('/api/users');
const postSearch = useSearch('/api/posts');
const pagination = usePagination();
// 命名完全由开发者控制,不会冲突
return {
userQuery: userSearch.query,
userResults: userSearch.results,
postQuery: postSearch.query,
postResults: postSearch.results,
};
},
};
型推論の比較
TypeScript のサポートも Composition API のもう一つの強みだ:
ts
// Options API 的类型推导较弱
export default Vue.extend({
data() {
return {
count: 0, // 推导为 any(在 Vue 2 中)
};
},
methods: {
increment() {
this.count; // 类型推导不完整
},
},
});
// Composition API 天然支持 TypeScript
import { ref, computed, Ref } from 'vue';
function useCounter(initialValue: number = 0) {
const count: Ref<number> = ref(initialValue);
const doubled = computed((): number => count.value * 2);
function increment(): void {
count.value++;
}
function setCount(value: number): void {
count.value = value;
}
return {
count,
doubled,
increment,
setCount,
};
}
// 使用例时获得完整的类型提示
const { count, doubled, increment } = useCounter();
// count.value: number
// doubled.value: number
// increment: () => void
Options APIとComposition APIの使い分け
Options API に向いている場面
- シンプルな表示専用コンポーネント — ロジックが単純で再利用も不要
- Vue に習熟したチーム — Options API の方が理解しやすく制約も明確
- TypeScript が不要 — Options API で十分な JS プロジェクト
Composition API に向いている場面
- 複雑なコンポーネント — ロジックが絡み合い、機能ごとに整理したい
- ロジックの再利用 — 複数のコンポーネント間でロジックを共有したい
- TypeScript プロジェクト — より良い型推論を得られる
- 関数型志向 — チームが関数型スタイルに慣れている
両方の API は共存可能
vue
<script>
import { ref, computed, setup } from 'vue';
import { useSearch } from './composables/useSearch';
export default {
// Options API 部分
props: {
initialPage: { type: Number, default: 1 },
},
// Composition API 部分
setup(props) {
const { query, results, search } = useSearch();
// 可以访问 props
const page = ref(props.initialPage);
return { query, results, search, page };
},
// 仍然可以使用 Options API 的其他选项
created() {
console.log('created hook');
},
methods: {
// 可以在 methods 中调用 setup 暴露的值
// 通过 this 访问
},
};
</script>
まとめ
- Composition API 通过
setup()函数和组合式函数组织逻辑,解决了 Options API 中逻辑分散的问题 - Composable 函数(类似 React Hooks)提供了比 Mixins 更好的逻辑复用方式
- Composition API 天然支持 TypeScript,类型推导完整
- 两种 API 可以在同一项目中共存,不需要强制迁移
- 简单组件使用 Options API,复杂/需要复用的组件使用 Composition API
- Vue 3 的 Composition API 设计借鉴了 React Hooks,但基于响应式系统而非闭包,避免了 hooks 的一些陷阱(如 stale closure)
