サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Vue + <script setup> + TypeScript: TodoMVC 09 コンポーネントからロジックをコンポーザブル関数に切り分ける


Vue + <script setup> + TypeScriptの構文で、公式サイト「TodoMVC」の例をつくるシリーズ第9回は、番外編その2です。ルートコンポーネントのロジックを、別モジュールに切り分けます。そのときに用いるのが、「コンポーザブル」と呼ばれる関数です。アプリケーションの組み立ては変えるものの、処理はこれまでと違いません。そこで、モジュールのコードは細かく分けず、まとめてご説明しましょう。

01 ロジックをコンポーザブル関数に切り分ける

「『コンポーザブル(composable)』とは、Vue の Composition API を活用して状態を持つロジックをカプセル化して再利用するための関数です」(「『コンポーザブル』とは?」)。状態の変数(オブジェクトの参照)とそれらを操作するための関数(メソッド)が備わります。コンポーザブルを呼び出したコンポーネントが戻り値として受け取るのはこうした変数や関数です。すると、コンポーネントは状態から切り離されて、ロジックが分けられます。

ルートコンポーネントからロジックを切り出して新たに定めたのが、つぎのコード001のモジュールsrc/hooks/useTodoApp.tsです。コンポーザブル関数の外枠の定めは少し異なるものの、本体のコードはほぼルートコンポーネントと変わりません。戻り値のオブジェクトに収めた変数や関数の識別子(名前)ももとのままです。

コード001■コンポーザブルのモジュール

src/hooks/useTodoApp.ts

import { computed, ref, watchEffect } from 'vue';
import type { Todo } from '../App.vue';
import { fetch, getNewId, save } from '../TodoStorage';

export const useTodoApp = () => {
	const todos = ref(fetch());
	const visibility = ref('all');
	const remaining = computed(() => getActive(todos.value).length);
	const allDone = computed(() => remaining.value === 0);
	const filteredTodos = computed((): Todo[] => {
		switch (visibility.value) {
			case 'all':
				return todos.value;
			case 'active':
				return todos.value.filter((todo) => !todo.completed);
			case 'completed':
				return todos.value.filter((todo) => todo.completed);
			default:
				return todos.value;
		}
	});
	watchEffect(() => save(todos.value));
	const addTodo = (todoTitle: string) => {
		if (!todoTitle) return;
		todos.value.push({
			id: getNewId(),
			title: todoTitle,
			completed: false,
		});
	};
	const removeTodo = (todo: Todo) => {
		todos.value = todos.value.filter((item) => item !== todo);
	};
	const done = (todo: Todo, completed: boolean) => {
		todo.completed = completed;
	};
	const getActive = (todos: Todo[]) => {
		return todos.filter((todo) => !todo.completed);
	};
	const onHashChange = () => {
		visibility.value = window.location.hash.replace(/#\/?/, '');
	};
	const removeCompleted = () => {
		todos.value = getActive(todos.value);
	};
	const toggleAll = (checked: boolean) => {
		todos.value.forEach((todo) => (todo.completed = checked));
	};
	onMounted(() => {
		window.addEventListener('hashchange', onHashChange);
	});
	return {
		allDone,
		filteredTodos,
		remaining,
		todos,
		visibility,
		addTodo,
		done,
		onHashChange,
		removeCompleted,
		removeTodo,
		toggleAll,
	};
};

ノート01■コンポーザブルとReactのフック

コンポーザブルが、コンポーネントから状態とともにロジックを切り分けるという役割は、Reactのフックと同じです。そして、コンポーザブル関数には、Reactフックと同じく"use"ではじまるキャメルケースの名前をつけるのが慣例とされます(「慣例とベストプラクティス」)。

けれど、依存関係を定めなくて済むむなど、内部的な扱いには違いがあるようです(「React Hooksとの比較」参照)。詳しくは、「Composition API に関するよくある質問」をお読みください。

02 コンポーザブルの呼び出しから戻り値を受け取る

さて、ルートモジュールsrc/App.vueは、つぎのコード002のとおり、状態を含めたロジックがすっかりなくなりました。かわりに、コンポーザブル関数(useTodoApp)の呼び出しから、戻り値を受け取ります。戻り値のオブジェクトに収めた変数や関数の名前はもとと変わっていません。したがって、オブジェクトの分割代入により、それらの参照を受け取るだけでよいのです。

コード002■ルートモジュール

src/App.vue

<script setup lang="ts">
import { onMounted } from 'vue';
import TodoInput from './components/TodoInput.vue';
import TodoList from './components/TodoList.vue';
import TodoController from './components/TodoController.vue';
import { useTodoApp } from './hooks/useTodoApp';

export interface Todo {
	id: number;
	title: string;
	completed: boolean;
}
const {
	allDone,
	filteredTodos,
	remaining,
	todos,
	visibility,
	addTodo,
	done,
	removeCompleted,
	removeTodo,
	toggleAll,
} = useTodoApp();
</script>

<template>
	<section id="app" class="todoapp">
		<header class="header">
			<Transition appear name="todo-head">
				<h1>todos</h1>
			</Transition>
			<TodoInput @addTodo="addTodo" />
		</header>
		<TodoList
			:allDone="allDone"
			:filteredTodos="filteredTodos"
			:todos="todos"
			@removeTodo="removeTodo"
			@done="done"
			@toggleAll="toggleAll"
		/>
		<TodoController
			:todos="todos"
			:remaining="remaining"
			:visibility="visibility"
			@removeCompleted="removeCompleted"
		/>
	</section>
</template>

<style scoped>
.todo-head-enter-active {
	transition: 1s ease-in;
}
.todo-head-enter-from {
	opacity: 0;
	transform: translateY(-40px);
}
</style>

<style>
@import url("https://unpkg.com/todomvc-app-css@2.4.2/index.css");
</style>

Vue + <script setup> + TypeScriptの構文で、公式サイト「TodoMVC」の例をつくるチュートリアルシリーズはこれで完結です。Vue 3のComposition APIの最新構文で、コードを具体的にどう書くのかご説明できたなら幸いです。GitHubのコードはつぎのソース01に公開しています。

ソース01■TodoMVC 09 コンポーネントからロジックをコンポーザブル関数に切り分ける

Vue + <script setup> + TypeScript: TodoMVCシリーズ


作成者: 野中文雄
作成日: 2023年01月16日


Copyright © 2001-2020 Fumio Nonaka.  All rights reserved.