diff --git a/src/client/components/index.ts b/src/client/components/index.ts
index 87547599a9..f71816d904 100644
--- a/src/client/components/index.ts
+++ b/src/client/components/index.ts
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import { App } from 'vue';
 
 import mfm from './misskey-flavored-markdown.vue';
 import acct from './acct.vue';
@@ -12,14 +12,16 @@ import loading from './loading.vue';
 import error from './error.vue';
 import streamIndicator from './stream-indicator.vue';
 
-Vue.component('mfm', mfm);
-Vue.component('mk-acct', acct);
-Vue.component('mk-avatar', avatar);
-Vue.component('mk-emoji', emoji);
-Vue.component('mk-user-name', userName);
-Vue.component('mk-ellipsis', ellipsis);
-Vue.component('mk-time', time);
-Vue.component('mk-url', url);
-Vue.component('mk-loading', loading);
-Vue.component('mk-error', error);
-Vue.component('stream-indicator', streamIndicator);
+export default function(app: App) {
+	app.component('mfm', mfm);
+	app.component('mk-acct', acct);
+	app.component('mk-avatar', avatar);
+	app.component('mk-emoji', emoji);
+	app.component('mk-user-name', userName);
+	app.component('mk-ellipsis', ellipsis);
+	app.component('mk-time', time);
+	app.component('mk-url', url);
+	app.component('mk-loading', loading);
+	app.component('mk-error', error);
+	app.component('stream-indicator', streamIndicator);
+}
diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts
index af2651533e..f78947a914 100644
--- a/src/client/components/mfm.ts
+++ b/src/client/components/mfm.ts
@@ -1,4 +1,4 @@
-import Vue, { VNode } from 'vue';
+import { VNode, defineComponent, h } from 'vue';
 import { MfmForest } from '../../mfm/prelude';
 import { parse, parsePlain } from '../../mfm/parse';
 import MkUrl from './url.vue';
@@ -10,7 +10,7 @@ import MkCode from './code.vue';
 import MkGoogle from './google.vue';
 import { host } from '../config';
 
-export default Vue.component('misskey-flavored-markdown', {
+export default defineComponent({
 	props: {
 		text: {
 			type: String,
@@ -41,7 +41,7 @@ export default Vue.component('misskey-flavored-markdown', {
 		},
 	},
 
-	render(createElement) {
+	render() {
 		if (this.text == null || this.text == '') return;
 
 		const ast = (this.plain ? parsePlain : parse)(this.text);
@@ -53,7 +53,7 @@ export default Vue.component('misskey-flavored-markdown', {
 
 					if (!this.plain) {
 						const x = text.split('\n')
-							.map(t => t == '' ? [createElement('br')] : [this._v(t), createElement('br')]); // NOTE: this._vはHACK SEE: https://github.com/syuilo/misskey/pull/6399#issuecomment-632820283
+							.map(t => t == '' ? [h('br')] : [this._v(t), h('br')]); // NOTE: this._vはHACK SEE: https://github.com/syuilo/misskey/pull/6399#issuecomment-632820283
 						x[x.length - 1].pop();
 						return x;
 					} else {
@@ -62,15 +62,15 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'bold': {
-					return [createElement('b', genEl(token.children))];
+					return [h('b', genEl(token.children))];
 				}
 
 				case 'strike': {
-					return [createElement('del', genEl(token.children))];
+					return [h('del', genEl(token.children))];
 				}
 
 				case 'italic': {
-					return (createElement as any)('i', {
+					return h('i', {
 						attrs: {
 							style: 'font-style: oblique;'
 						},
@@ -78,7 +78,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'big': {
-					return (createElement as any)('strong', {
+					return h('strong', {
 						attrs: {
 							style: `display: inline-block; font-size: 150%;`
 						},
@@ -90,7 +90,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'small': {
-					return [createElement('small', {
+					return [h('small', {
 						attrs: {
 							style: 'opacity: 0.7;'
 						},
@@ -98,7 +98,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'center': {
-					return [createElement('div', {
+					return [h('div', {
 						attrs: {
 							style: 'text-align:center;'
 						}
@@ -106,7 +106,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'motion': {
-					return (createElement as any)('span', {
+					return h('span', {
 						attrs: {
 							style: 'display: inline-block;'
 						},
@@ -124,7 +124,7 @@ export default Vue.component('misskey-flavored-markdown', {
 						'normal';
 					const style = this.$store.state.device.animatedMfm
 						? `animation: spin 1.5s linear infinite; animation-direction: ${direction};` : '';
-					return (createElement as any)('span', {
+					return h('span', {
 						attrs: {
 							style: 'display: inline-block;' + style
 						},
@@ -132,7 +132,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'jump': {
-					return (createElement as any)('span', {
+					return h('span', {
 						attrs: {
 							style: this.$store.state.device.animatedMfm ? 'display: inline-block; animation: jump 0.75s linear infinite;' : 'display: inline-block;'
 						},
@@ -140,7 +140,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'flip': {
-					return (createElement as any)('span', {
+					return h('span', {
 						attrs: {
 							style: 'display: inline-block; transform: scaleX(-1);'
 						},
@@ -148,7 +148,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'url': {
-					return [createElement(MkUrl, {
+					return [h(MkUrl, {
 						key: Math.random(),
 						props: {
 							url: token.node.props.url,
@@ -158,7 +158,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'link': {
-					return [createElement(MkLink, {
+					return [h(MkLink, {
 						key: Math.random(),
 						props: {
 							url: token.node.props.url,
@@ -168,7 +168,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'mention': {
-					return [createElement(MkMention, {
+					return [h(MkMention, {
 						key: Math.random(),
 						props: {
 							host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host,
@@ -178,7 +178,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'hashtag': {
-					return [createElement('router-link', {
+					return [h('router-link', {
 						key: Math.random(),
 						attrs: {
 							to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`,
@@ -188,7 +188,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'blockCode': {
-					return [createElement(MkCode, {
+					return [h(MkCode, {
 						key: Math.random(),
 						props: {
 							code: token.node.props.code,
@@ -198,7 +198,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'inlineCode': {
-					return [createElement(MkCode, {
+					return [h(MkCode, {
 						key: Math.random(),
 						props: {
 							code: token.node.props.code,
@@ -210,13 +210,13 @@ export default Vue.component('misskey-flavored-markdown', {
 
 				case 'quote': {
 					if (this.shouldBreak) {
-						return [createElement('div', {
+						return [h('div', {
 							attrs: {
 								class: 'quote'
 							}
 						}, genEl(token.children))];
 					} else {
-						return [createElement('span', {
+						return [h('span', {
 							attrs: {
 								class: 'quote'
 							}
@@ -225,7 +225,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'title': {
-					return [createElement('div', {
+					return [h('div', {
 						attrs: {
 							class: 'title'
 						}
@@ -233,7 +233,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'emoji': {
-					return [createElement('mk-emoji', {
+					return [h('mk-emoji', {
 						key: Math.random(),
 						attrs: {
 							emoji: token.node.props.emoji,
@@ -247,7 +247,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'mathInline': {
-					return [createElement(MkFormula, {
+					return [h(MkFormula, {
 						key: Math.random(),
 						props: {
 							formula: token.node.props.formula,
@@ -257,7 +257,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'mathBlock': {
-					return [createElement(MkFormula, {
+					return [h(MkFormula, {
 						key: Math.random(),
 						props: {
 							formula: token.node.props.formula,
@@ -267,7 +267,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'search': {
-					return [createElement(MkGoogle, {
+					return [h(MkGoogle, {
 						key: Math.random(),
 						props: {
 							q: token.node.props.query
@@ -284,6 +284,6 @@ export default Vue.component('misskey-flavored-markdown', {
 		}));
 
 		// Parse ast to DOM
-		return createElement('span', genEl(ast));
+		return h('span', genEl(ast));
 	}
 });
diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue
index cbdd08906c..252025f48a 100644
--- a/src/client/components/timeline.vue
+++ b/src/client/components/timeline.vue
@@ -46,11 +46,6 @@ export default defineComponent({
 	},
 
 	created() {
-		this.$once('hook:beforeDestroy', () => {
-			this.connection.dispose();
-			if (this.connection2) this.connection2.dispose();
-		});
-
 		const prepend = note => {
 			const _note = JSON.parse(JSON.stringify(note));	// deepcopy
 			(this.$refs.tl as any).prepend(_note);
@@ -130,6 +125,11 @@ export default defineComponent({
 		};
 	},
 
+	beforeUnmount() {
+		this.connection.dispose();
+		if (this.connection2) this.connection2.dispose();
+	},
+
 	methods: {
 		focus() {
 			this.$refs.tl.focus();
diff --git a/src/client/directives/autocomplete.ts b/src/client/directives/autocomplete.ts
index 44043017ef..a3e16135df 100644
--- a/src/client/directives/autocomplete.ts
+++ b/src/client/directives/autocomplete.ts
@@ -1,18 +1,19 @@
+import { Directive } from 'vue';
 import * as getCaretCoordinates from 'textarea-caret';
 import { toASCII } from 'punycode';
 
 export default {
-	bind(el, binding, vn) {
+	mounted(el, binding, vn) {
 		const self = el._autoCompleteDirective_ = {} as any;
 		self.x = new Autocomplete(el, vn.context, binding.value);
 		self.x.attach();
 	},
 
-	unbind(el, binding, vn) {
+	unmounted(el, binding, vn) {
 		const self = el._autoCompleteDirective_;
 		self.x.detach();
 	}
-};
+} as Directive;
 
 /**
  * オートコンプリートを管理するクラス。
diff --git a/src/client/directives/index.ts b/src/client/directives/index.ts
index 24db5cc152..8d6116f975 100644
--- a/src/client/directives/index.ts
+++ b/src/client/directives/index.ts
@@ -7,9 +7,9 @@ import particle from './particle';
 import tooltip from './tooltip';
 
 export default function(app: App) {
-	//app.directive('autocomplete', autocomplete);
-	//app.directive('userPreview', userPreview);
-	//app.directive('user-preview', userPreview);
+	app.directive('autocomplete', autocomplete);
+	app.directive('userPreview', userPreview);
+	app.directive('user-preview', userPreview);
 	app.directive('size', size);
 	//app.directive('particle', particle);
 	//app.directive('tooltip', tooltip);
diff --git a/src/client/directives/user-preview.ts b/src/client/directives/user-preview.ts
index 4db0d67c4a..a22ccb8eed 100644
--- a/src/client/directives/user-preview.ts
+++ b/src/client/directives/user-preview.ts
@@ -1,7 +1,10 @@
+import { Directive } from 'vue';
 import MkUserPreview from '../components/user-preview.vue';
 
 export default {
-	bind(el: HTMLElement, binding, vn) {
+	mounted(el: HTMLElement, binding, vn) {
+		// TODO: 新たにプロパティを作るのをやめMapを使う
+		// ただメモリ的には↓の方が省メモリかもしれないので検討中
 		const self = (el as any)._userPreviewDirective_ = {} as any;
 
 		self.user = binding.value;
@@ -68,8 +71,8 @@ export default {
 		});
 	},
 
-	unbind(el, binding, vn) {
+	unmounted(el, binding, vn) {
 		const self = el._userPreviewDirective_;
 		clearInterval(self.checkTimer);
 	}
-};
+} as Directive;
diff --git a/src/client/init.ts b/src/client/init.ts
index ea35493aac..e5d5b72467 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -16,6 +16,7 @@ import FontAwesomeIcon from './components/fa.vue';
 import Stream from './scripts/stream';
 import widgets from './widgets';
 import directives from './directives';
+import components from './components';
 import { version, langs, getLocale, apiUrl } from './config';
 import { store } from './store';
 import { router } from './router';
@@ -172,8 +173,7 @@ app.component('fa', FontAwesomeIcon);
 
 widgets(app);
 directives(app);
-
-//require('./components');
+components(app);
 
 document.body.innerHTML = '<div id="app"></div>';
 
diff --git a/src/client/root.vue b/src/client/root.vue
index 8d8bf3f7af..bd7391a0e4 100644
--- a/src/client/root.vue
+++ b/src/client/root.vue
@@ -40,6 +40,10 @@ export default defineComponent({
 	},
 
 	methods: {
+		api(endpoint: string, data: { [x: string]: any } = {}, token?) {
+			return this.$store.dispatch('api', { endpoint, data, token });
+		},
+
 		dialog(opts) {
 			this.$store.commit('showDialog', opts);
 		}
diff --git a/src/client/scripts/paging.ts b/src/client/scripts/paging.ts
index 832f0720e0..5755cfe611 100644
--- a/src/client/scripts/paging.ts
+++ b/src/client/scripts/paging.ts
@@ -1,4 +1,3 @@
-import Vue from 'vue';
 import { getScrollPosition, onScrollTop } from './scroll';
 
 const SECOND_FETCH_LIMIT = 30;
@@ -48,14 +47,14 @@ export default (opts) => ({
 	created() {
 		opts.displayLimit = opts.displayLimit || 30;
 		this.init();
+	},
 
-		this.$on('hook:activated', () => {
-			this.isBackTop = false;
-		});
+	activated() {
+		this.isBackTop = false;
+	},
 
-		this.$on('hook:deactivated', () => {
-			this.isBackTop = window.scrollY === 0;
-		});
+	deactivated() {
+		this.isBackTop = window.scrollY === 0;
 	},
 
 	mounted() {
@@ -75,7 +74,7 @@ export default (opts) => ({
 
 	methods: {
 		updateItem(i, item) {
-			Vue.set((this as any).items, i, item);
+			(this as any).items[i] = item;
 		},
 
 		reload() {