From 43930e6a84d11fb00b9ae9ac153c2538e179d538 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 19 Dec 2020 10:55:52 +0900
Subject: [PATCH] Storage improve (#6976)

* wip

* wip

* wip

* wip

* wip

* Update storage.ts

* wip

* wip

* wip

* wip

* Update storage.ts

* Update storage.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update storage.ts

* wip

* wip

* wip

* wip

* :pizza:

* wip

* wip

* wip

* wip

* wip

* wip

* Update deck-storage.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update store.ts

* wip

* wip

* wip

* wip

* Update init.ts

* wip

* wip

* Update pizzax.ts

* wip

* wip

* Update timeline.vue

* Update init.ts

* wip

* wip

* Update init.ts
---
 package.json                                  |   2 -
 src/client/@types/vuex-shim.d.ts              |  12 -
 src/client/account.ts                         |  86 ++
 src/client/cold-storage.ts                    |  34 -
 src/client/components/analog-clock.vue        |  10 -
 src/client/components/autocomplete.vue        |  12 +-
 src/client/components/captcha.vue             |   3 +-
 src/client/components/date-separated-list.vue |   4 +-
 .../components/drive-file-thumbnail.vue       |   5 +-
 src/client/components/drive.file.vue          |   4 +-
 src/client/components/drive.folder.vue        |  14 +-
 src/client/components/drive.vue               |   4 +-
 src/client/components/emoji-picker.vue        |  21 +-
 src/client/components/form/form.scss          |   2 +
 src/client/components/global/a.vue            |  14 +-
 src/client/components/global/acct.vue         |   2 +-
 src/client/components/global/avatar.vue       |   2 +-
 src/client/components/global/emoji.vue        |   6 +-
 src/client/components/global/error.vue        |   2 +-
 src/client/components/google.vue              |   2 +-
 src/client/components/instance-stats.vue      |   2 +-
 src/client/components/launch-pad.vue          |   2 +-
 src/client/components/media-banner.vue        |   5 +-
 src/client/components/media-image.vue         |  10 +-
 src/client/components/media-video.vue         |   2 +-
 src/client/components/mention.vue             |   6 +-
 src/client/components/mfm.ts                  |  14 +-
 src/client/components/note.sub.vue            |   2 +-
 src/client/components/note.vue                |  46 +-
 src/client/components/notes.vue               |   2 +-
 src/client/components/notifications.vue       |   8 +-
 src/client/components/page/page.post.vue      |   6 +-
 src/client/components/page/page.text.vue      |   2 +-
 src/client/components/page/page.vue           |   4 +-
 src/client/components/post-form.vue           |  28 +-
 .../components/reactions-viewer.reaction.vue  |   2 +-
 src/client/components/reactions-viewer.vue    |   2 +-
 src/client/components/sample.vue              |   2 +-
 src/client/components/sidebar.vue             |  42 +-
 src/client/components/signin.vue              |   6 +-
 src/client/components/signup.vue              |   6 +-
 src/client/components/sub-note-content.vue    |   2 +-
 src/client/components/timeline.vue            |  10 +-
 src/client/components/ui/context-menu.vue     |   2 +-
 src/client/components/ui/modal.vue            |   4 +-
 src/client/components/ui/pagination.vue       |   2 +-
 src/client/components/ui/window.vue           |   2 +-
 src/client/components/url-preview.vue         |   2 +-
 src/client/components/user-info.vue           |   4 +-
 src/client/components/user-list.vue           |   2 +-
 src/client/components/user-preview.vue        |   4 +-
 src/client/components/user-select-dialog.vue  |   6 +-
 src/client/components/users-dialog.vue        |   2 +-
 src/client/init.ts                            | 224 +++---
 src/client/instance.ts                        |  45 ++
 src/client/os.ts                              |   7 +-
 src/client/pages/_error_.vue                  |   2 +-
 src/client/pages/about-misskey.vue            |   4 +-
 src/client/pages/about.vue                    |   2 +-
 src/client/pages/announcements.vue            |   4 +-
 src/client/pages/auth.vue                     |  10 +-
 src/client/pages/channel.vue                  |   4 +-
 src/client/pages/channels.vue                 |   2 +-
 src/client/pages/clip.vue                     |   4 +-
 src/client/pages/explore.vue                  |   2 +-
 src/client/pages/follow-requests.vue          |   2 +-
 src/client/pages/instance/index.metrics.vue   |   2 +-
 src/client/pages/instance/index.vue           |   2 +-
 src/client/pages/instance/instance.vue        |   2 +-
 src/client/pages/instance/queue.chart.vue     |   2 +-
 src/client/pages/instance/settings.vue        |   3 +-
 src/client/pages/instance/user-dialog.vue     |   2 +-
 src/client/pages/messaging/index.vue          |   6 +-
 .../pages/messaging/messaging-room.form.vue   |   6 +-
 .../messaging/messaging-room.message.vue      |   4 +-
 src/client/pages/messaging/messaging-room.vue |   8 +-
 src/client/pages/mfm-cheat-sheet.vue          |   2 +-
 src/client/pages/miauth.vue                   |   6 +-
 src/client/pages/page-editor/page-editor.vue  |   2 +-
 src/client/pages/page.vue                     |   4 +-
 src/client/pages/pages.vue                    |   2 +-
 src/client/pages/reversi/game.board.vue       |  20 +-
 src/client/pages/reversi/game.setting.vue     |  16 +-
 src/client/pages/reversi/index.vue            |   2 +-
 src/client/pages/room/room.vue                |   7 +-
 src/client/pages/settings/2fa.vue             |  22 +-
 src/client/pages/settings/account-info.vue    |  18 +-
 src/client/pages/settings/deck.vue            |  26 +-
 src/client/pages/settings/drive.vue           |  10 +-
 src/client/pages/settings/email-address.vue   |   6 +-
 src/client/pages/settings/email.vue           |   6 +-
 src/client/pages/settings/general.vue         |  99 +--
 src/client/pages/settings/index.vue           |   8 +-
 src/client/pages/settings/integration.vue     |   6 +-
 src/client/pages/settings/notifications.vue   |   4 +-
 src/client/pages/settings/other.vue           |   2 +-
 src/client/pages/settings/plugins.vue         |  35 +-
 src/client/pages/settings/privacy.vue         |  26 +-
 src/client/pages/settings/profile.vue         |  46 +-
 src/client/pages/settings/reaction.vue        |  22 +-
 src/client/pages/settings/regedit.vue         |  75 --
 src/client/pages/settings/sidebar.vue         |  17 +-
 src/client/pages/settings/sounds.vue          |  30 +-
 src/client/pages/settings/theme.install.vue   |   9 +-
 src/client/pages/settings/theme.manage.vue    |  14 +-
 src/client/pages/settings/theme.vue           | 134 ++--
 src/client/pages/settings/word-mute.vue       |   6 +-
 src/client/pages/test.vue                     |   2 +-
 src/client/pages/theme-editor.vue             |   9 +-
 src/client/pages/timeline.tutorial.vue        |   4 +-
 src/client/pages/timeline.vue                 |  22 +-
 src/client/pages/user/index.photos.vue        |   4 +-
 src/client/pages/user/index.vue               |  18 +-
 src/client/pages/welcome.setup.vue            |   4 +-
 src/client/pizzax.ts                          | 124 +++
 src/client/router.ts                          |   4 +-
 src/client/scripts/aiscript/api.ts            |   8 +-
 src/client/scripts/get-user-menu.ts           |  11 +-
 src/client/scripts/please-login.ts            |   4 +-
 src/client/scripts/sound.ts                   |   6 +-
 src/client/sidebar.ts                         |  40 +-
 src/client/store.ts                           | 742 +++++++-----------
 src/client/tsconfig.json                      |   2 +-
 src/client/ui/_common_/common.vue             |   6 +-
 src/client/ui/_common_/header.vue             |   2 +-
 src/client/ui/_common_/stream-indicator.vue   |   2 +-
 src/client/ui/deck.vue                        |  25 +-
 src/client/ui/deck/antenna-column.vue         |   6 +-
 src/client/ui/deck/column.vue                 |  22 +-
 src/client/ui/deck/deck-store.ts              | 223 ++++++
 src/client/ui/deck/list-column.vue            |   6 +-
 src/client/ui/deck/notifications-column.vue   |   4 +-
 src/client/ui/deck/tl-column.vue              |  14 +-
 src/client/ui/deck/widgets-column.vue         |  30 +-
 src/client/ui/default.vue                     |  21 +-
 src/client/ui/default.widgets.vue             |  21 +-
 src/client/ui/desktop.vue                     |   7 +-
 src/client/ui/visitor/a.vue                   |  11 +-
 src/client/ui/visitor/b.vue                   |   9 +-
 src/client/ui/visitor/kanban.vue              |   2 +-
 src/client/ui/zen.vue                         |   7 +-
 src/client/widgets/activity.vue               |   2 +-
 src/client/widgets/define.ts                  |   9 +-
 src/client/widgets/memo.vue                   |   9 +-
 src/client/widgets/photos.vue                 |   2 +-
 yarn.lock                                     |  20 +-
 146 files changed, 1458 insertions(+), 1519 deletions(-)
 delete mode 100644 src/client/@types/vuex-shim.d.ts
 create mode 100644 src/client/account.ts
 delete mode 100644 src/client/cold-storage.ts
 create mode 100644 src/client/instance.ts
 delete mode 100644 src/client/pages/settings/regedit.vue
 create mode 100644 src/client/pizzax.ts
 create mode 100644 src/client/ui/deck/deck-store.ts

diff --git a/package.json b/package.json
index dbda3bc3d8..785400ec4c 100644
--- a/package.json
+++ b/package.json
@@ -251,8 +251,6 @@
 		"vue-router": "4.0.1",
 		"vue-style-loader": "4.1.2",
 		"vuedraggable": "4.0.1",
-		"vuex": "4.0.0-rc.2",
-		"vuex-persistedstate": "3.1.0",
 		"web-push": "3.4.4",
 		"webpack": "5.10.1",
 		"webpack-cli": "4.2.0",
diff --git a/src/client/@types/vuex-shim.d.ts b/src/client/@types/vuex-shim.d.ts
deleted file mode 100644
index 5bcc4c460e..0000000000
--- a/src/client/@types/vuex-shim.d.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { ComponentCustomProperties } from 'vue';
-import { Store } from 'vuex';
-
-declare module '@vue/runtime-core' {
-	// tslint:disable-next-line:no-empty-interface
-	interface State {
-	}
-
-	interface ComponentCustomProperties {
-		$store: Store<State>;
-	}
-}
diff --git a/src/client/account.ts b/src/client/account.ts
new file mode 100644
index 0000000000..fdf49ee213
--- /dev/null
+++ b/src/client/account.ts
@@ -0,0 +1,86 @@
+import { reactive } from 'vue';
+import { apiUrl } from '@/config';
+import { waiting } from '@/os';
+
+// TODO: 他のタブと永続化されたstateを同期
+
+type Account = {
+	id: string;
+	token: string;
+	clientData: Record<string, any>;
+};
+
+const data = localStorage.getItem('account');
+
+// TODO: 外部からはreadonlyに
+export const $i = data ? reactive(JSON.parse(data) as Account) : null;
+
+export function signout() {
+	localStorage.removeItem('account');
+	document.cookie = `igi=; path=/`;
+	location.href = '/';
+}
+
+export function getAccounts() {
+	const accountsData = localStorage.getItem('accounts');
+	const accounts: { id: Account['id'], token: Account['token'] }[] = accountsData ? JSON.parse(accountsData) : [];
+	return accounts;
+}
+
+export function addAccount(id: Account['id'], token: Account['token']) {
+	const accounts = getAccounts();
+	if (!accounts.some(x => x.id === id)) {
+		localStorage.setItem('accounts', JSON.stringify(accounts.concat([{ id, token }])));
+	}
+}
+
+function fetchAccount(token): Promise<Account> {
+	return new Promise((done, fail) => {
+		// Fetch user
+		fetch(`${apiUrl}/i`, {
+			method: 'POST',
+			body: JSON.stringify({
+				i: token
+			})
+		})
+		.then(res => {
+			// When failed to authenticate user
+			if (res.status !== 200 && res.status < 500) {
+				return signout();
+			}
+
+			// Parse response
+			res.json().then(i => {
+				i.token = token;
+				done(i);
+			});
+		})
+		.catch(fail);
+	});
+}
+
+export function updateAccount(data) {
+	for (const [key, value] of Object.entries(data)) {
+		$i[key] = value;
+	}
+}
+
+export function refreshAccount() {
+	fetchAccount($i.token).then(updateAccount);
+}
+
+export async function login(token: Account['token']) {
+	waiting();
+	if (_DEV_) console.log('logging as token ', token);
+	const me = await fetchAccount(token);
+	localStorage.setItem('account', JSON.stringify(me));
+	addAccount(me.id, token);
+	location.reload();
+}
+
+// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
+declare module '@vue/runtime-core' {
+	interface ComponentCustomProperties {
+		$i: typeof $i;
+	}
+}
diff --git a/src/client/cold-storage.ts b/src/client/cold-storage.ts
deleted file mode 100644
index 1bee2313fa..0000000000
--- a/src/client/cold-storage.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-// 常にメモリにロードしておく必要がないような設定情報を保管するストレージ
-
-const PREFIX = 'miux:';
-
-export const defaultDeviceSettings = {
-	sound_masterVolume: 0.3,
-	sound_note: { type: 'syuilo/down', volume: 1 },
-	sound_noteMy: { type: 'syuilo/up', volume: 1 },
-	sound_notification: { type: 'syuilo/pope2', volume: 1 },
-	sound_chat: { type: 'syuilo/pope1', volume: 1 },
-	sound_chatBg: { type: 'syuilo/waon', volume: 1 },
-	sound_antenna: { type: 'syuilo/triple', volume: 1 },
-	sound_channel: { type: 'syuilo/square-pico', volume: 1 },
-	sound_reversiPutBlack: { type: 'syuilo/kick', volume: 0.3 },
-	sound_reversiPutWhite: { type: 'syuilo/snare', volume: 0.3 },
-};
-
-export const device = {
-	get<T extends keyof typeof defaultDeviceSettings>(key: T): typeof defaultDeviceSettings[T] {
-		// TODO: indexedDBにする
-		//       ただしその際はnullチェックではなくキー存在チェックにしないとダメ
-		//       (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
-		const value = localStorage.getItem(PREFIX + key);
-		if (value == null) {
-			return defaultDeviceSettings[key];
-		} else {
-			return JSON.parse(value);
-		}
-	},
-
-	set(key: keyof typeof defaultDeviceSettings, value: any): any {
-		localStorage.setItem(PREFIX + key, JSON.stringify(value));
-	},
-};
diff --git a/src/client/components/analog-clock.vue b/src/client/components/analog-clock.vue
index b3fb7a515d..04d98508bb 100644
--- a/src/client/components/analog-clock.vue
+++ b/src/client/components/analog-clock.vue
@@ -116,16 +116,6 @@ export default defineComponent({
 			}
 		};
 		update();
-
-		this.$store.subscribe((mutation, state) => {
-			if (mutation.type !== 'device/set') return;
-
-			if (mutation?.payload?.key !== 'theme') return;
-
-			setTimeout(() => {
-				this.computedStyle = getComputedStyle(document.documentElement);
-			}, 250);
-		});
 	},
 
 	beforeUnmount() {
diff --git a/src/client/components/autocomplete.vue b/src/client/components/autocomplete.vue
index 0b81c58727..c3eaa36efb 100644
--- a/src/client/components/autocomplete.vue
+++ b/src/client/components/autocomplete.vue
@@ -17,8 +17,8 @@
 	</ol>
 	<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
 		<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
-			<span class="emoji" v-if="emoji.isCustomEmoji"><img :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
-			<span class="emoji" v-else-if="!useOsNativeEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span>
+			<span class="emoji" v-if="emoji.isCustomEmoji"><img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
+			<span class="emoji" v-else-if="!$store.state.useOsNativeEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span>
 			<span class="emoji" v-else>{{ emoji.emoji }}</span>
 			<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
 			<span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span>
@@ -128,12 +128,6 @@ export default defineComponent({
 		}
 	},
 
-	computed: {
-		useOsNativeEmojis(): boolean {
-			return this.$store.state.device.useOsNativeEmojis;
-		}
-	},
-
 	watch: {
 		showing() {
 			if (!this.showing) {
@@ -151,7 +145,7 @@ export default defineComponent({
 		this.setPosition();
 
 		//#region Construct Emoji DB
-		const customEmojis = this.$store.state.instance.meta.emojis;
+		const customEmojis = this.$instance.emojis;
 		const emojiDefinitions: EmojiDef[] = [];
 
 		for (const x of customEmojis) {
diff --git a/src/client/components/captcha.vue b/src/client/components/captcha.vue
index 94a13a8b5b..f37f395567 100644
--- a/src/client/components/captcha.vue
+++ b/src/client/components/captcha.vue
@@ -28,7 +28,6 @@ declare global {
 	interface Window extends CaptchaContainer {
 	}
 }
-import * as os from '@/os';
 
 export default defineComponent({
 	props: {
@@ -101,7 +100,7 @@ export default defineComponent({
 			if (this.captcha.render && this.$refs.captcha instanceof Element) {
 				this.captcha.render(this.$refs.captcha, {
 					sitekey: this.sitekey,
-					theme: this.$store.state.device.darkMode ? 'dark' : 'light',
+					theme: this.$store.state.darkMode ? 'dark' : 'light',
 					callback: this.callback,
 					'expired-callback': this.callback,
 					'error-callback': this.callback,
diff --git a/src/client/components/date-separated-list.vue b/src/client/components/date-separated-list.vue
index c79afb52f3..3336fab9bd 100644
--- a/src/client/components/date-separated-list.vue
+++ b/src/client/components/date-separated-list.vue
@@ -37,12 +37,14 @@ export default defineComponent({
 			});
 		}
 
-		return h(this.$store.state.device.animation ? TransitionGroup : 'div', {
+		return h(this.$store.state.animation ? TransitionGroup : 'div', this.$store.state.animation ? {
 			class: 'sqadhkmv _list_',
 			name: 'list',
 			tag: 'div',
 			'data-direction': this.direction,
 			'data-reversed': this.reversed ? 'true' : 'false',
+		} : {
+			class: 'sqadhkmv _list_',
 		}, this.items.map((item, i) => {
 			const el = this.$slots.default({
 				item: item
diff --git a/src/client/components/drive-file-thumbnail.vue b/src/client/components/drive-file-thumbnail.vue
index 7615014ba9..e669da6a90 100644
--- a/src/client/components/drive-file-thumbnail.vue
+++ b/src/client/components/drive-file-thumbnail.vue
@@ -27,6 +27,7 @@ import {
 	faFilm
 	} from '@fortawesome/free-solid-svg-icons';
 import ImgWithBlurhash from './img-with-blurhash.vue';
+import { ColdDeviceStorage } from '@/store';
 
 export default defineComponent({
 	components: {
@@ -89,12 +90,12 @@ export default defineComponent({
 	},
 	mounted() {
 		const audioTag = this.$refs.volumectrl as HTMLAudioElement;
-		if (audioTag) audioTag.volume = this.$store.state.device.mediaVolume;
+		if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume');
 	},
 	methods: {
 		volumechange() {
 			const audioTag = this.$refs.volumectrl as HTMLAudioElement;
-			this.$store.commit('device/set', { key: 'mediaVolume', value: audioTag.volume });
+			ColdDeviceStorage.set('mediaVolume', audioTag.volume);
 		}
 	}
 });
diff --git a/src/client/components/drive.file.vue b/src/client/components/drive.file.vue
index 261bbc83ec..81e314a725 100644
--- a/src/client/components/drive.file.vue
+++ b/src/client/components/drive.file.vue
@@ -8,11 +8,11 @@
 	@dragend="onDragend"
 	:title="title"
 >
-	<div class="label" v-if="$store.state.i.avatarId == file.id">
+	<div class="label" v-if="$i.avatarId == file.id">
 		<img src="/assets/label.svg"/>
 		<p>{{ $t('avatar') }}</p>
 	</div>
-	<div class="label" v-if="$store.state.i.bannerId == file.id">
+	<div class="label" v-if="$i.bannerId == file.id">
 		<img src="/assets/label.svg"/>
 		<p>{{ $t('banner') }}</p>
 	</div>
diff --git a/src/client/components/drive.folder.vue b/src/client/components/drive.folder.vue
index ab678217b6..79ae789f78 100644
--- a/src/client/components/drive.folder.vue
+++ b/src/client/components/drive.folder.vue
@@ -19,7 +19,7 @@
 		<template v-if="!hover"><Fa :icon="faFolder" fixed-width/></template>
 		{{ folder.name }}
 	</p>
-	<p class="upload" v-if="$store.state.settings.uploadFolder == folder.id">
+	<p class="upload" v-if="$store.state.uploadFolder == folder.id">
 		{{ $t('uploadFolder') }}
 	</p>
 	<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
@@ -213,11 +213,8 @@ export default defineComponent({
 			os.api('drive/folders/delete', {
 				folderId: this.folder.id
 			}).then(() => {
-				if (this.$store.state.settings.uploadFolder === this.folder.id) {
-					this.$store.dispatch('settings/set', {
-						key: 'uploadFolder',
-						value: null
-					});
+				if (this.$store.state.uploadFolder === this.folder.id) {
+					this.$store.set('uploadFolder', null);
 				}
 			}).catch(err => {
 				switch(err.id) {
@@ -238,10 +235,7 @@ export default defineComponent({
 		},
 
 		setAsUploadFolder() {
-			this.$store.dispatch('settings/set', {
-				key: 'uploadFolder',
-				value: this.folder.id
-			});
+			this.$store.set('uploadFolder', this.folder.id);
 		},
 
 		onContextmenu(e) {
diff --git a/src/client/components/drive.vue b/src/client/components/drive.vue
index f25d25da01..a743249b9d 100644
--- a/src/client/components/drive.vue
+++ b/src/client/components/drive.vue
@@ -136,7 +136,7 @@ export default defineComponent({
 	},
 
 	mounted() {
-		if (this.$store.state.device.enableInfiniteScroll && this.$refs.loadMoreFiles) {
+		if (this.$store.state.enableInfiniteScroll && this.$refs.loadMoreFiles) {
 			this.$nextTick(() => {
 				this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
 			});
@@ -159,7 +159,7 @@ export default defineComponent({
 	},
 
 	activated() {
-		if (this.$store.state.device.enableInfiniteScroll) {
+		if (this.$store.state.enableInfiniteScroll) {
 			this.$nextTick(() => {
 				this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
 			});
diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue
index 30d0e060ba..a9fa357086 100644
--- a/src/client/components/emoji-picker.vue
+++ b/src/client/components/emoji-picker.vue
@@ -13,7 +13,7 @@
 						tabindex="0"
 					>
 						<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
-						<img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+						<img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
 					</button>
 				</div>
 				<div v-if="searchResultUnicode.length > 0">
@@ -45,7 +45,7 @@
 				<section>
 					<header class="_acrylic"><Fa :icon="faClock" fixed-width/> {{ $t('recentUsed') }}</header>
 					<div>
-						<button v-for="emoji in $store.state.device.recentlyUsedEmojis"
+						<button v-for="emoji in $store.state.recentlyUsedEmojis"
 							class="_button"
 							@click="chosen(emoji, $event)"
 							:key="emoji"
@@ -67,7 +67,7 @@
 						@click="chosen(emoji, $event)"
 						:key="emoji.name"
 					>
-						<img :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+						<img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
 					</button>
 				</div>
 			</section>
@@ -100,6 +100,7 @@ import MkModal from '@/components/ui/modal.vue';
 import Particle from '@/components/particle.vue';
 import * as os from '@/os';
 import { isDeviceTouch } from '../scripts/is-device-touch';
+import { emojiCategories } from '@/instance';
 
 export default defineComponent({
 	components: {
@@ -125,12 +126,12 @@ export default defineComponent({
 		return {
 			emojilist: markRaw(emojilist),
 			getStaticImageUrl,
-			pinned: this.$store.state.settings.reactions,
-			width: this.asReactionPicker ? this.$store.state.device.reactionPickerWidth : 3,
-			height: this.asReactionPicker ? this.$store.state.device.reactionPickerHeight : 2,
+			pinned: this.$store.state.reactions,
+			width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3,
+			height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2,
 			big: this.asReactionPicker ? isDeviceTouch : false,
-			customEmojiCategories: this.$store.getters['instance/emojiCategories'],
-			customEmojis: this.$store.state.instance.meta.emojis,
+			customEmojiCategories: emojiCategories,
+			customEmojis: this.$instance.emojis,
 			visibleCategories: {},
 			q: null,
 			searchResultCustom: [],
@@ -346,10 +347,10 @@ export default defineComponent({
 
 			// 最近使った絵文字更新
 			if (!this.pinned.includes(key)) {
-				let recents = this.$store.state.device.recentlyUsedEmojis;
+				let recents = this.$store.state.recentlyUsedEmojis;
 				recents = recents.filter((e: any) => e !== key);
 				recents.unshift(key);
-				this.$store.commit('device/set', { key: 'recentlyUsedEmojis', value: recents.splice(0, 16) });
+				this.$store.set('recentlyUsedEmojis', recents.splice(0, 16));
 			}
 		},
 
diff --git a/src/client/components/form/form.scss b/src/client/components/form/form.scss
index b541bf826d..b6747bf738 100644
--- a/src/client/components/form/form.scss
+++ b/src/client/components/form/form.scss
@@ -12,6 +12,7 @@
 ._formLabel {
 	font-size: 80%;
 	padding: 0 16px 8px 16px;
+	opacity: 0.8;
 
 	&:empty {
 		display: none;
@@ -21,6 +22,7 @@
 ._formCaption {
 	font-size: 80%;
 	padding: 8px 16px 0 16px;
+	opacity: 0.8;
 
 	&:empty {
 		display: none;
diff --git a/src/client/components/global/a.vue b/src/client/components/global/a.vue
index f6c66257b3..e936003018 100644
--- a/src/client/components/global/a.vue
+++ b/src/client/components/global/a.vue
@@ -12,6 +12,7 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
 import { router } from '@/router';
 import { ui, url } from '@/config';
 import { popout } from '@/scripts/popout';
+import { ColdDeviceStorage } from '@/store';
 
 export default defineComponent({
 	inject: {
@@ -98,8 +99,8 @@ export default defineComponent({
 
 		nav() {
 			if (this.to.startsWith('/my/messaging')) {
-				if (this.$store.state.device.chatOpenBehavior === 'window') return this.window();
-				if (this.$store.state.device.chatOpenBehavior === 'popout') return this.popout();
+				if (ColdDeviceStorage.get('chatOpenBehavior') === 'window') return this.window();
+				if (ColdDeviceStorage.get('chatOpenBehavior') === 'popout') return this.popout();
 			}
 
 			if (this.behavior) {
@@ -111,12 +112,13 @@ export default defineComponent({
 			if (this.navHook) {
 				this.navHook(this.to);
 			} else {
-				if (this.$store.state.device.defaultSideView && this.sideViewHook && this.to !== '/') {
+				if (this.$store.state.defaultSideView && this.sideViewHook && this.to !== '/') {
 					return this.sideViewHook(this.to);
 				}
-				if (this.$store.state.device.deckNavWindow && (ui === 'deck') && this.to !== '/') {
-					return this.window();
-				}
+				// TODO: a.vueからdeck-sotreを参照したくないのでなんとかする
+				//if (deckStore.state.device.deckNavWindow && (ui === 'deck') && this.to !== '/') {
+				//	return this.window();
+				//}
 				if (ui === 'desktop') {
 					return this.window();
 				}
diff --git a/src/client/components/global/acct.vue b/src/client/components/global/acct.vue
index 9d434de6cd..cad906524e 100644
--- a/src/client/components/global/acct.vue
+++ b/src/client/components/global/acct.vue
@@ -1,7 +1,7 @@
 <template>
 <span class="mk-acct" v-once>
 	<span class="name">@{{ user.username }}</span>
-	<span class="host" v-if="user.host || detail || $store.state.settings.showFullAcct">@{{ user.host || host }}</span>
+	<span class="host" v-if="user.host || detail || $store.state.showFullAcct">@{{ user.host || host }}</span>
 </span>
 </template>
 
diff --git a/src/client/components/global/avatar.vue b/src/client/components/global/avatar.vue
index 1cfead919d..5723ea04b6 100644
--- a/src/client/components/global/avatar.vue
+++ b/src/client/components/global/avatar.vue
@@ -38,7 +38,7 @@ export default defineComponent({
 			return this.user.isCat;
 		},
 		url(): string {
-			return this.$store.state.device.disableShowingAnimatedImages
+			return this.$store.state.disableShowingAnimatedImages
 				? getStaticImageUrl(this.user.avatarUrl)
 				: this.user.avatarUrl;
 		},
diff --git a/src/client/components/global/emoji.vue b/src/client/components/global/emoji.vue
index e93cdb6f3b..5a715fe09a 100644
--- a/src/client/components/global/emoji.vue
+++ b/src/client/components/global/emoji.vue
@@ -54,13 +54,13 @@ export default defineComponent({
 		},
 
 		useOsNativeEmojis(): boolean {
-			return this.$store.state.device.useOsNativeEmojis && !this.isReaction;
+			return this.$store.state.useOsNativeEmojis && !this.isReaction;
 		},
 
 		ce() {
 			let ce = [];
 			if (this.customEmojis) ce = ce.concat(this.customEmojis);
-			if (this.$store.state.instance.meta && this.$store.state.instance.meta.emojis) ce = ce.concat(this.$store.state.instance.meta.emojis);
+			if (this.$instance && this.$instance.emojis) ce = ce.concat(this.$instance.emojis);
 			return ce;
 		}
 	},
@@ -72,7 +72,7 @@ export default defineComponent({
 					const customEmoji = this.ce.find(x => x.name === this.emoji.substr(1, this.emoji.length - 2));
 					if (customEmoji) {
 						this.customEmoji = customEmoji;
-						this.url = this.$store.state.device.disableShowingAnimatedImages
+						this.url = this.$store.state.disableShowingAnimatedImages
 							? getStaticImageUrl(customEmoji.url)
 							: customEmoji.url;
 					}
diff --git a/src/client/components/global/error.vue b/src/client/components/global/error.vue
index 311093dacf..e4c76faa31 100644
--- a/src/client/components/global/error.vue
+++ b/src/client/components/global/error.vue
@@ -1,5 +1,5 @@
 <template>
-<transition :name="$store.state.device.animation ? 'zoom' : ''" appear>
+<transition :name="$store.state.animation ? 'zoom' : ''" appear>
 	<div class="mjndxjcg">
 		<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
 		<p><Fa :icon="faExclamationTriangle"/> {{ $t('somethingHappened') }}</p>
diff --git a/src/client/components/google.vue b/src/client/components/google.vue
index bec670ac5f..a63a1476a9 100644
--- a/src/client/components/google.vue
+++ b/src/client/components/google.vue
@@ -23,7 +23,7 @@ export default defineComponent({
 	},
 	methods: {
 		search() {
-			const engine = this.$store.state.settings.webSearchEngine ||
+			const engine = this.$store.state.webSearchEngine ||
 				'https://www.google.com/search?q={{query}}';
 			const url = engine.replace('{{query}}', this.query)
 			window.open(url, '_blank');
diff --git a/src/client/components/instance-stats.vue b/src/client/components/instance-stats.vue
index dcb3c75fa0..652cedcebb 100644
--- a/src/client/components/instance-stats.vue
+++ b/src/client/components/instance-stats.vue
@@ -278,7 +278,7 @@ export default defineComponent({
 			}
 
 			// TODO: var(--panel)の色が暗いか明るいかで判定する
-			const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+			const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 
 			Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 			this.chartInstance = markRaw(new Chart(this.$refs.chart, {
diff --git a/src/client/components/launch-pad.vue b/src/client/components/launch-pad.vue
index d09a4002df..24af78247d 100644
--- a/src/client/components/launch-pad.vue
+++ b/src/client/components/launch-pad.vue
@@ -58,7 +58,7 @@ export default defineComponent({
 
 	computed: {
 		menu(): string[] {
-			return this.$store.state.deviceUser.menu;
+			return this.$store.state.menu;
 		},
 	},
 
diff --git a/src/client/components/media-banner.vue b/src/client/components/media-banner.vue
index dd95b66de2..d90d1186e7 100644
--- a/src/client/components/media-banner.vue
+++ b/src/client/components/media-banner.vue
@@ -29,6 +29,7 @@
 import { defineComponent } from 'vue';
 import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
 import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
 
 export default defineComponent({
 	props: {
@@ -45,12 +46,12 @@ export default defineComponent({
 	},
 	mounted() {
 		const audioTag = this.$refs.audio as HTMLAudioElement;
-		if (audioTag) audioTag.volume = this.$store.state.device.mediaVolume;
+		if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume');
 	},
 	methods: {
 		volumechange() {
 			const audioTag = this.$refs.audio as HTMLAudioElement;
-			this.$store.commit('device/set', { key: 'mediaVolume', value: audioTag.volume });
+			ColdDeviceStorage.set('mediaVolume', audioTag.volume);
 		},
 	},
 })
diff --git a/src/client/components/media-image.vue b/src/client/components/media-image.vue
index a9d0023cc2..55d334987f 100644
--- a/src/client/components/media-image.vue
+++ b/src/client/components/media-image.vue
@@ -52,13 +52,11 @@ export default defineComponent({
 	},
 	computed: {
 		url(): any {
-			let url = this.$store.state.device.disableShowingAnimatedImages
+			let url = this.$store.state.disableShowingAnimatedImages
 				? getStaticImageUrl(this.image.thumbnailUrl)
 				: this.image.thumbnailUrl;
 
-			if (this.$store.state.device.loadRemoteMedia) {
-				url = null;
-			} else if (this.raw || this.$store.state.device.loadRawImages) {
+			if (this.raw || this.$store.state.loadRawImages) {
 				url = this.image.url;
 			}
 
@@ -68,7 +66,7 @@ export default defineComponent({
 	created() {
 		// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
 		this.$watch('image', () => {
-			this.hide = (this.$store.state.device.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.device.nsfw !== 'ignore');
+			this.hide = (this.$store.state.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.nsfw !== 'ignore');
 			if (this.image.blurhash) {
 				this.color = extractAvgColorFromBlurhash(this.image.blurhash);
 			}
@@ -79,7 +77,7 @@ export default defineComponent({
 	},
 	methods: {
 		onClick() {
-			if (this.$store.state.device.imageNewTab) {
+			if (this.$store.state.imageNewTab) {
 				window.open(this.image.url, '_blank');
 			} else {
 				os.popup(ImageViewer, {
diff --git a/src/client/components/media-video.vue b/src/client/components/media-video.vue
index 3dfd60c87f..78b011157f 100644
--- a/src/client/components/media-video.vue
+++ b/src/client/components/media-video.vue
@@ -48,7 +48,7 @@ export default defineComponent({
 		}
 	},
 	created() {
-		this.hide = (this.$store.state.device.nsfw === 'force') ? true : this.video.isSensitive && (this.$store.state.device.nsfw !== 'ignore');
+		this.hide = (this.$store.state.nsfw === 'force') ? true : this.video.isSensitive && (this.$store.state.nsfw !== 'ignore');
 	},
 });
 </script>
diff --git a/src/client/components/mention.vue b/src/client/components/mention.vue
index 85f8436a42..69ea99b871 100644
--- a/src/client/components/mention.vue
+++ b/src/client/components/mention.vue
@@ -3,7 +3,7 @@
 	<span class="me" v-if="isMe">{{ $t('you') }}</span>
 	<span class="main">
 		<span class="username">@{{ username }}</span>
-		<span class="host" v-if="(host != localHost) || $store.state.settings.showFullAcct">@{{ toUnicode(host) }}</span>
+		<span class="host" v-if="(host != localHost) || $store.state.showFullAcct">@{{ toUnicode(host) }}</span>
 	</span>
 </MkA>
 <a class="ldlomzub" :href="url" target="_blank" rel="noopener" v-else>
@@ -50,8 +50,8 @@ export default defineComponent({
 			return this.host === localHost ? `@${this.username}` : `@${this.username}@${toUnicode(this.host)}`;
 		},
 		isMe(): boolean {
-			return this.$store.getters.isSignedIn && (
-				`@${this.username}@${toUnicode(this.host)}` === `@${this.$store.state.i.username}@${toUnicode(localHost)}`.toLowerCase()
+			return this.$i && (
+				`@${this.username}@${toUnicode(this.host)}` === `@${this.$i.username}@${toUnicode(localHost)}`.toLowerCase()
 			);
 		}
 	},
diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts
index a403e9a68c..c977c4f095 100644
--- a/src/client/components/mfm.ts
+++ b/src/client/components/mfm.ts
@@ -82,22 +82,22 @@ export default defineComponent({
 					let style;
 					switch (token.node.props.name) {
 						case 'tada': {
-							style = `font-size: 150%;` + (this.$store.state.device.animatedMfm ? 'animation: tada 1s linear infinite both;' : '');
+							style = `font-size: 150%;` + (this.$store.state.animatedMfm ? 'animation: tada 1s linear infinite both;' : '');
 							break;
 						}
 						case 'jelly': {
 							const speed = token.node.props.args.speed || '1s';
-							style = (this.$store.state.device.animatedMfm ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
+							style = (this.$store.state.animatedMfm ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
 							break;
 						}
 						case 'twitch': {
 							const speed = token.node.props.args.speed || '0.5s';
-							style = this.$store.state.device.animatedMfm ? `animation: mfm-twitch ${speed} ease infinite;` : '';
+							style = this.$store.state.animatedMfm ? `animation: mfm-twitch ${speed} ease infinite;` : '';
 							break;
 						}
 						case 'shake': {
 							const speed = token.node.props.args.speed || '0.5s';
-							style = this.$store.state.device.animatedMfm ? `animation: mfm-shake ${speed} ease infinite;` : '';
+							style = this.$store.state.animatedMfm ? `animation: mfm-shake ${speed} ease infinite;` : '';
 							break;
 						}
 						case 'spin': {
@@ -110,15 +110,15 @@ export default defineComponent({
 								token.node.props.args.y ? 'mfm-spinY' :
 								'mfm-spin';
 							const speed = token.node.props.args.speed || '1.5s';
-							style = this.$store.state.device.animatedMfm ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
+							style = this.$store.state.animatedMfm ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
 							break;
 						}
 						case 'jump': {
-							style = this.$store.state.device.animatedMfm ? 'animation: mfm-jump 0.75s linear infinite;' : '';
+							style = this.$store.state.animatedMfm ? 'animation: mfm-jump 0.75s linear infinite;' : '';
 							break;
 						}
 						case 'bounce': {
-							style = this.$store.state.device.animatedMfm ? 'animation: mfm-bounce 0.75s linear infinite; transform-origin: center bottom;' : '';
+							style = this.$store.state.animatedMfm ? 'animation: mfm-bounce 0.75s linear infinite; transform-origin: center bottom;' : '';
 							break;
 						}
 						case 'flip': {
diff --git a/src/client/components/note.sub.vue b/src/client/components/note.sub.vue
index 2ce045a34e..28553ea4a7 100644
--- a/src/client/components/note.sub.vue
+++ b/src/client/components/note.sub.vue
@@ -6,7 +6,7 @@
 			<XNoteHeader class="header" :note="note" :mini="true"/>
 			<div class="body">
 				<p v-if="note.cw != null" class="cw">
-					<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
+					<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis" />
 					<XCwButton v-model:value="showContent" :note="note"/>
 				</p>
 				<div class="content" v-show="note.cw == null || showContent">
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index a16fb6e162..04696b7cfa 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -43,14 +43,14 @@
 			<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
 			<div class="body">
 				<p v-if="appearNote.cw != null" class="cw">
-					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
+					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
 					<XCwButton v-model:value="showContent" :note="appearNote"/>
 				</p>
 				<div class="content" v-show="appearNote.cw == null || showContent">
 					<div class="text">
 						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
 						<MkA class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><Fa :icon="faReply"/></MkA>
-						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
+						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
 						<a class="rp" v-if="appearNote.renote != null">RN:</a>
 					</div>
 					<div class="files" v-if="appearNote.files.length > 0">
@@ -182,7 +182,7 @@ export default defineComponent({
 
 	computed: {
 		rs() {
-			return this.$store.state.settings.reactions;
+			return this.$store.state.reactions;
 		},
 		keymap(): any {
 			return {
@@ -222,11 +222,11 @@ export default defineComponent({
 		},
 
 		isMyNote(): boolean {
-			return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId);
+			return this.$i && (this.$i.id === this.appearNote.userId);
 		},
 
 		isMyRenote(): boolean {
-			return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.note.userId);
+			return this.$i && (this.$i.id === this.note.userId);
 		},
 
 		canRenote(): boolean {
@@ -262,14 +262,14 @@ export default defineComponent({
 		},
 
 		showTicker() {
-			if (this.$store.state.device.instanceTicker === 'always') return true;
-			if (this.$store.state.device.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
+			if (this.$store.state.instanceTicker === 'always') return true;
+			if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
 			return false;
 		}
 	},
 
 	async created() {
-		if (this.$store.getters.isSignedIn) {
+		if (this.$i) {
 			this.connection = os.stream;
 		}
 
@@ -282,7 +282,7 @@ export default defineComponent({
 			this.$emit('update:note', Object.freeze(result));
 		}
 
-		this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords);
+		this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
 
 		if (this.detail) {
 			os.api('notes/children', {
@@ -305,7 +305,7 @@ export default defineComponent({
 	mounted() {
 		this.capture(true);
 
-		if (this.$store.getters.isSignedIn) {
+		if (this.$i) {
 			this.connection.on('_connected_', this.onStreamConnected);
 		}
 	},
@@ -313,7 +313,7 @@ export default defineComponent({
 	beforeUnmount() {
 		this.decapture(true);
 
-		if (this.$store.getters.isSignedIn) {
+		if (this.$i) {
 			this.connection.off('_connected_', this.onStreamConnected);
 		}
 	},
@@ -340,14 +340,14 @@ export default defineComponent({
 		},
 
 		capture(withHandler = false) {
-			if (this.$store.getters.isSignedIn) {
+			if (this.$i) {
 				this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
 				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
 			}
 		},
 
 		decapture(withHandler = false) {
-			if (this.$store.getters.isSignedIn) {
+			if (this.$i) {
 				this.connection.send('un', {
 					id: this.appearNote.id
 				});
@@ -389,7 +389,7 @@ export default defineComponent({
 						[reaction]: currentCount + 1
 					};
 
-					if (body.userId === this.$store.state.i.id) {
+					if (body.userId === this.$i.id) {
 						n.myReaction = reaction;
 					}
 
@@ -414,7 +414,7 @@ export default defineComponent({
 						[reaction]: Math.max(0, currentCount - 1)
 					};
 
-					if (body.userId === this.$store.state.i.id) {
+					if (body.userId === this.$i.id) {
 						n.myReaction = null;
 					}
 
@@ -434,7 +434,7 @@ export default defineComponent({
 					choices[choice] = {
 						...choices[choice],
 						votes: choices[choice].votes + 1,
-						...(body.userId === this.$store.state.i.id ? {
+						...(body.userId === this.$i.id ? {
 							isVoted: true
 						} : {})
 					};
@@ -614,7 +614,7 @@ export default defineComponent({
 
 		getMenu() {
 			let menu;
-			if (this.$store.getters.isSignedIn) {
+			if (this.$i) {
 				const statePromise = os.api('notes/state', {
 					noteId: this.appearNote.id
 				});
@@ -649,7 +649,7 @@ export default defineComponent({
 					text: this.$t('clip'),
 					action: () => this.clip()
 				},
-				(this.appearNote.userId != this.$store.state.i.id) ? statePromise.then(state => state.isWatching ? {
+				(this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
 					icon: faEyeSlash,
 					text: this.$t('unwatch'),
 					action: () => this.toggleWatch(false)
@@ -658,7 +658,7 @@ export default defineComponent({
 					text: this.$t('watch'),
 					action: () => this.toggleWatch(true)
 				}) : undefined,
-				this.appearNote.userId == this.$store.state.i.id ? (this.$store.state.i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
+				this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
 					icon: faThumbtack,
 					text: this.$t('unpin'),
 					action: () => this.togglePin(false)
@@ -667,7 +667,7 @@ export default defineComponent({
 					text: this.$t('pin'),
 					action: () => this.togglePin(true)
 				} : undefined,
-				...(this.$store.state.i.isModerator || this.$store.state.i.isAdmin ? [
+				...(this.$i.isModerator || this.$i.isAdmin ? [
 					null,
 					{
 						icon: faBullhorn,
@@ -676,7 +676,7 @@ export default defineComponent({
 					}]
 					: []
 				),
-				...(this.appearNote.userId != this.$store.state.i.id ? [
+				...(this.appearNote.userId != this.$i.id ? [
 					null,
 					{
 						icon: faExclamationCircle,
@@ -691,9 +691,9 @@ export default defineComponent({
 					}]
 					: []
 				),
-				...(this.appearNote.userId == this.$store.state.i.id || this.$store.state.i.isModerator || this.$store.state.i.isAdmin ? [
+				...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
 					null,
-					this.appearNote.userId == this.$store.state.i.id ? {
+					this.appearNote.userId == this.$i.id ? {
 						icon: faEdit,
 						text: this.$t('deleteAndEdit'),
 						action: this.delEdit
diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue
index 649e7c4cf6..ee96687337 100644
--- a/src/client/components/notes.vue
+++ b/src/client/components/notes.vue
@@ -19,7 +19,7 @@
 	</XList>
 
 	<div v-show="more && !reversed" style="margin-top: var(--margin);">
-		<button class="_loadMore" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+		<button class="_loadMore" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
 			<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
 			<template v-if="moreFetching"><MkLoading inline/></template>
 		</button>
diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue
index 3eedf86558..03db8c755d 100644
--- a/src/client/components/notifications.vue
+++ b/src/client/components/notifications.vue
@@ -5,7 +5,7 @@
 		<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
 	</XList>
 
-	<button class="_loadMore" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+	<button class="_loadMore" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
 		<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
 		<template v-if="moreFetching"><MkLoading inline/></template>
 	</button>
@@ -59,7 +59,7 @@ export default defineComponent({
 
 	computed: {
 		allIncludeTypes() {
-			return this.includeTypes ?? notificationTypes.filter(x => !this.$store.state.i.mutingNotificationTypes.includes(x));
+			return this.includeTypes ?? notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x));
 		}
 	},
 
@@ -70,9 +70,9 @@ export default defineComponent({
 			},
 			deep: true
 		},
-		// TODO: vue/vuexのバグか仕様かは不明なものの、プロフィール更新するなどして $store.state.i が更新されると、
+		// TODO: vue/vuexのバグか仕様かは不明なものの、プロフィール更新するなどして $i が更新されると、
 		// mutingNotificationTypes に変化が無くてもこのハンドラーが呼び出され無駄なリロードが発生するのを直す
-		'$store.state.i.mutingNotificationTypes': {
+		'$i.mutingNotificationTypes': {
 			handler() {
 				if (this.includeTypes === null) {
 					this.reload();
diff --git a/src/client/components/page/page.post.vue b/src/client/components/page/page.post.vue
index ac8be4a397..46870ca0d7 100644
--- a/src/client/components/page/page.post.vue
+++ b/src/client/components/page/page.post.vue
@@ -49,9 +49,9 @@ export default defineComponent({
 				canvas.toBlob(blob => {
 					const data = new FormData();
 					data.append('file', blob);
-					data.append('i', this.$store.state.i.token);
-					if (this.$store.state.settings.uploadFolder) {
-						data.append('folderId', this.$store.state.settings.uploadFolder);
+					data.append('i', this.$i.token);
+					if (this.$store.state.uploadFolder) {
+						data.append('folderId', this.$store.state.uploadFolder);
 					}
 
 					fetch(apiUrl + '/drive/files/create', {
diff --git a/src/client/components/page/page.text.vue b/src/client/components/page/page.text.vue
index fff840f743..f109c9f041 100644
--- a/src/client/components/page/page.text.vue
+++ b/src/client/components/page/page.text.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mrdgzndn">
-	<Mfm :text="text" :is-note="false" :i="$store.state.i" :key="text"/>
+	<Mfm :text="text" :is-note="false" :i="$i" :key="text"/>
 	<MkUrlPreview v-for="url in urls" :url="url" :key="url" class="url"/>
 </div>
 </template>
diff --git a/src/client/components/page/page.vue b/src/client/components/page/page.vue
index b957779476..4aa86e075a 100644
--- a/src/client/components/page/page.vue
+++ b/src/client/components/page/page.vue
@@ -35,9 +35,9 @@ export default defineComponent({
 	created() {
 		this.hpml = new Hpml(this.page, {
 			randomSeed: Math.random(),
-			visitor: this.$store.state.i,
+			visitor: this.$i,
 			url: url,
-			enableAiScript: !this.$store.state.device.disablePagesScript
+			enableAiScript: !this.$store.state.disablePagesScript
 		});
 	},
 
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index 3f4561dad8..086e32a713 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -135,8 +135,8 @@ export default defineComponent({
 			poll: null,
 			useCw: false,
 			cw: null,
-			localOnly: this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.localOnly : this.$store.state.settings.defaultNoteLocalOnly,
-			visibility: this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.visibility : this.$store.state.settings.defaultNoteVisibility,
+			localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
+			visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility,
 			visibleUsers: [],
 			autocomplete: null,
 			draghover: false,
@@ -198,7 +198,7 @@ export default defineComponent({
 		},
 
 		max(): number {
-			return this.$store.state.instance.meta ? this.$store.state.instance.meta.maxNoteTextLength : 1000;
+			return this.$instance ? this.$instance.maxNoteTextLength : 1000;
 		}
 	},
 
@@ -223,8 +223,8 @@ export default defineComponent({
 				const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
 
 				// 自分は除外
-				if (this.$store.state.i.username == x.username && x.host == null) continue;
-				if (this.$store.state.i.username == x.username && x.host == host) continue;
+				if (this.$i.username == x.username && x.host == null) continue;
+				if (this.$i.username == x.username && x.host == host) continue;
 
 				// 重複は除外
 				if (this.text.indexOf(`${mention} `) != -1) continue;
@@ -243,12 +243,12 @@ export default defineComponent({
 			this.visibility = this.reply.visibility;
 			if (this.reply.visibility === 'specified') {
 				os.api('users/show', {
-					userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$store.state.i.id && uid !== this.reply.userId)
+					userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$i.id && uid !== this.reply.userId)
 				}).then(users => {
 					this.visibleUsers.push(...users);
 				});
 
-				if (this.reply.userId !== this.$store.state.i.id) {
+				if (this.reply.userId !== this.$i.id) {
 					os.api('users/show', { userId: this.reply.userId }).then(user => {
 						this.visibleUsers.push(user);
 					});
@@ -262,7 +262,7 @@ export default defineComponent({
 		}
 
 		// keep cw when reply
-		if (this.$store.state.settings.keepCw && this.reply && this.reply.cw) {
+		if (this.$store.keepCw && this.reply && this.reply.cw) {
 			this.useCw = true;
 			this.cw = this.reply.cw;
 		}
@@ -376,7 +376,7 @@ export default defineComponent({
 		},
 
 		upload(file: File, name?: string) {
-			os.upload(file, this.$store.state.settings.uploadFolder, name).then(res => {
+			os.upload(file, this.$store.state.uploadFolder, name).then(res => {
 				this.files.push(res);
 			});
 		},
@@ -399,14 +399,14 @@ export default defineComponent({
 			}, {
 				changeVisibility: visibility => {
 					this.visibility = visibility;
-					if (this.$store.state.settings.rememberNoteVisibility) {
-						this.$store.commit('deviceUser/setVisibility', visibility);
+					if (this.$store.state.rememberNoteVisibility) {
+						this.$store.set('visibility', visibility);
 					}
 				},
 				changeLocalOnly: localOnly => {
 					this.localOnly = localOnly;
-					if (this.$store.state.settings.rememberNoteVisibility) {
-						this.$store.commit('deviceUser/setLocalOnly', localOnly);
+					if (this.$store.state.rememberNoteVisibility) {
+						this.$store.set('localOnly', localOnly);
 					}
 				}
 			}, 'closed');
@@ -440,7 +440,7 @@ export default defineComponent({
 					const file = item.getAsFile();
 					const lio = file.name.lastIndexOf('.');
 					const ext = lio >= 0 ? file.name.slice(lio) : '';
-					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
+					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
 					this.upload(file, formatted);
 				}
 			}
diff --git a/src/client/components/reactions-viewer.reaction.vue b/src/client/components/reactions-viewer.reaction.vue
index 62128d7e66..a67b24601b 100644
--- a/src/client/components/reactions-viewer.reaction.vue
+++ b/src/client/components/reactions-viewer.reaction.vue
@@ -53,7 +53,7 @@ export default defineComponent({
 	},
 	computed: {
 		canToggle(): boolean {
-			return !this.reaction.match(/@\w/) && this.$store.getters.isSignedIn;
+			return !this.reaction.match(/@\w/) && this.$i;
 		},
 	},
 	watch: {
diff --git a/src/client/components/reactions-viewer.vue b/src/client/components/reactions-viewer.vue
index df10294d04..94a0318734 100644
--- a/src/client/components/reactions-viewer.vue
+++ b/src/client/components/reactions-viewer.vue
@@ -25,7 +25,7 @@ export default defineComponent({
 	},
 	computed: {
 		isMe(): boolean {
-			return this.$store.getters.isSignedIn && this.$store.state.i.id === this.note.userId;
+			return this.$i && this.$i.id === this.note.userId;
 		},
 	},
 });
diff --git a/src/client/components/sample.vue b/src/client/components/sample.vue
index 2e21b785d5..b6300ba446 100644
--- a/src/client/components/sample.vue
+++ b/src/client/components/sample.vue
@@ -51,7 +51,7 @@ export default defineComponent({
 			text: '',
 			flag: false,
 			radio: 'misskey',
-			mfm: `Hello world! This is an @example mention. BTW you are @${this.$store.state.i.username}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`
+			mfm: `Hello world! This is an @example mention. BTW you are @${this.$i.username}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`
 		}
 	},
 
diff --git a/src/client/components/sidebar.vue b/src/client/components/sidebar.vue
index 9a3a7c4823..584093f32a 100644
--- a/src/client/components/sidebar.vue
+++ b/src/client/components/sidebar.vue
@@ -12,7 +12,7 @@
 		<nav class="nav" :class="{ iconOnly, hidden }" v-show="showing">
 			<div>
 				<button class="item _button account" @click="openAccountMenu">
-					<MkAvatar :user="$store.state.i" class="avatar"/><MkAcct class="text" :user="$store.state.i"/>
+					<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
 				</button>
 				<MkA class="item index" active-class="active" to="/" exact>
 					<Fa :icon="faHome" fixed-width/><span class="text">{{ $t('timeline') }}</span>
@@ -25,7 +25,7 @@
 					</component>
 				</template>
 				<div class="divider"></div>
-				<button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$store.state.i.isAdmin || $store.state.i.isModerator" @click="oepnInstanceMenu">
+				<button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$i.isAdmin || $i.isModerator" @click="oepnInstanceMenu">
 					<Fa :icon="faServer" fixed-width/><span class="text">{{ $t('instance') }}</span>
 				</button>
 				<button class="item _button" @click="more">
@@ -49,6 +49,7 @@ import { host } from '@/config';
 import { search } from '@/scripts/search';
 import * as os from '@/os';
 import { sidebarDef } from '@/sidebar';
+import { getAccounts, addAccount, login } from '@/account';
 
 export default defineComponent({
 	data() {
@@ -67,7 +68,7 @@ export default defineComponent({
 
 	computed: {
 		menu(): string[] {
-			return this.$store.state.deviceUser.menu;
+			return this.$store.state.menu;
 		},
 
 		otherNavItemIndicated(): boolean {
@@ -84,7 +85,7 @@ export default defineComponent({
 			this.showing = false;
 		},
 
-		'$store.state.device.sidebarDisplay'() {
+		'$store.reactiveState.sidebarDisplay'() {
 			this.calcViewState();
 		},
 
@@ -108,8 +109,8 @@ export default defineComponent({
 
 	methods: {
 		calcViewState() {
-			this.iconOnly = (window.innerWidth <= 1279) || (this.$store.state.device.sidebarDisplay === 'icon');
-			this.hidden = (window.innerWidth <= 650) || (this.$store.state.device.sidebarDisplay === 'hide');
+			this.iconOnly = (window.innerWidth <= 1279) || (this.$store.state.sidebarDisplay === 'icon');
+			this.hidden = (window.innerWidth <= 650);
 		},
 
 		show() {
@@ -133,7 +134,8 @@ export default defineComponent({
 		},
 
 		async openAccountMenu(ev) {
-			const accounts = (await os.api('users/show', { userIds: this.$store.state.device.accounts.map(x => x.id) })).filter(x => x.id !== this.$store.state.i.id);
+			const storedAccounts = getAccounts();
+			const accounts = (await os.api('users/show', { userIds: storedAccounts.map(x => x.id) })).filter(x => x.id !== this.$i.id);
 
 			const accountItems = accounts.map(account => ({
 				type: 'user',
@@ -144,8 +146,8 @@ export default defineComponent({
 			os.modalMenu([...[{
 				type: 'link',
 				text: this.$t('profile'),
-				to: `/@${ this.$store.state.i.username }`,
-				avatar: this.$store.state.i,
+				to: `/@${ this.$i.username }`,
+				avatar: this.$i,
 			}, null, ...accountItems, {
 				icon: faPlus,
 				text: this.$t('addAcount'),
@@ -169,7 +171,7 @@ export default defineComponent({
 				text: this.$t('dashboard'),
 				to: '/instance',
 				icon: faTachometerAlt,
-			}, null, this.$store.state.i.isAdmin ? {
+			}, null, this.$i.isAdmin ? {
 				type: 'link',
 				text: this.$t('settings'),
 				to: '/instance/settings',
@@ -230,7 +232,7 @@ export default defineComponent({
 		addAcount() {
 			os.popup(import('./signin-dialog.vue'), {}, {
 				done: res => {
-					this.$store.dispatch('addAcount', res);
+					addAccount(res.id, res.i);
 					os.success();
 				},
 			}, 'closed');
@@ -239,30 +241,20 @@ export default defineComponent({
 		createAccount() {
 			os.popup(import('./signup-dialog.vue'), {}, {
 				done: res => {
-					this.$store.dispatch('addAcount', res);
+					addAccount(res.id, res.i);
 					this.switchAccountWithToken(res.i);
 				},
 			}, 'closed');
 		},
 
 		switchAccount(account: any) {
-			const token = this.$store.state.device.accounts.find((x: any) => x.id === account.id).token;
+			const storedAccounts = getAccounts();
+			const token = storedAccounts.find(x => x.id === account.id).token;
 			this.switchAccountWithToken(token);
 		},
 
 		switchAccountWithToken(token: string) {
-			os.waiting();
-
-			os.api('i', {}, token).then((i: any) => {
-				this.$store.dispatch('switchAccount', {
-					...i,
-					token: token
-				}).then(() => {
-					this.$nextTick(() => {
-						location.reload();
-					});
-				});
-			});
+			login(token);
 		},
 	}
 });
diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue
index 5052902aca..57c390191c 100755
--- a/src/client/components/signin.vue
+++ b/src/client/components/signin.vue
@@ -56,6 +56,7 @@ import MkInput from './ui/input.vue';
 import { apiUrl, host } from '@/config';
 import { byteify, hexify } from '@/scripts/2fa';
 import * as os from '@/os';
+import { login } from '@/account';
 
 export default defineComponent({
 	components: {
@@ -97,7 +98,7 @@ export default defineComponent({
 
 	computed: {
 		meta() {
-			return this.$store.state.instance.meta;
+			return this.$instance;
 		},
 	},
 
@@ -114,8 +115,7 @@ export default defineComponent({
 
 		onLogin(res) {
 			if (this.autoSet) {
-				localStorage.setItem('i', res.i);
-				location.reload();
+				login(res.i);
 			}
 		},
 
diff --git a/src/client/components/signup.vue b/src/client/components/signup.vue
index 6907b655ba..ec631c9429 100644
--- a/src/client/components/signup.vue
+++ b/src/client/components/signup.vue
@@ -59,6 +59,7 @@ import MkButton from './ui/button.vue';
 import MkInput from './ui/input.vue';
 import MkSwitch from './ui/switch.vue';
 import * as os from '@/os';
+import { login } from '@/account';
 
 export default defineComponent({
 	components: {
@@ -99,7 +100,7 @@ export default defineComponent({
 
 	computed: {
 		meta() {
-			return this.$store.state.instance.meta;
+			return this.$instance;
 		},
 
 		shouldDisableSubmitting(): boolean {
@@ -184,8 +185,7 @@ export default defineComponent({
 					this.$emit('signup', res);
 
 					if (this.autoSet) {
-						localStorage.setItem('i', res.i);
-						location.reload();
+						login(res.i);
 					}
 				});
 			}).catch(() => {
diff --git a/src/client/components/sub-note-content.vue b/src/client/components/sub-note-content.vue
index cb65a76495..04250dea5b 100644
--- a/src/client/components/sub-note-content.vue
+++ b/src/client/components/sub-note-content.vue
@@ -4,7 +4,7 @@
 		<span v-if="note.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
 		<span v-if="note.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span>
 		<MkA class="reply" v-if="note.replyId" :to="`/notes/${note.replyId}`"><Fa :icon="faReply"/></MkA>
-		<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/>
+		<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
 		<MkA class="rp" v-if="note.renoteId" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
 	</div>
 	<details v-if="note.files.length > 0">
diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue
index 011f22ee17..33e7256325 100644
--- a/src/client/components/timeline.vue
+++ b/src/client/components/timeline.vue
@@ -1,5 +1,5 @@
 <template>
-<XNotes :class="{ _noGap_: !$store.state.device.showGapBetweenNotesInTimeline }" ref="tl" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)" @queue="$emit('queue', $event)"/>
+<XNotes :class="{ _noGap_: !$store.state.showGapBetweenNotesInTimeline }" ref="tl" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)" @queue="$emit('queue', $event)"/>
 </template>
 
 <script lang="ts">
@@ -51,9 +51,9 @@ export default defineComponent({
 			connection2: null,
 			pagination: null,
 			baseQuery: {
-				includeMyRenotes: this.$store.state.settings.showMyRenotes,
-				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
-				includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+				includeMyRenotes: this.$store.state.showMyRenotes,
+				includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
+				includeLocalRenotes: this.$store.state.showLocalRenotes
 			},
 			query: {},
 		};
@@ -66,7 +66,7 @@ export default defineComponent({
 			this.$emit('note');
 
 			if (this.sound) {
-				sound.play(note.userId === this.$store.state.i.id ? 'noteMy' : 'note');
+				sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
 			}
 		};
 
diff --git a/src/client/components/ui/context-menu.vue b/src/client/components/ui/context-menu.vue
index e5cb4ddc75..561099cbe0 100644
--- a/src/client/components/ui/context-menu.vue
+++ b/src/client/components/ui/context-menu.vue
@@ -1,5 +1,5 @@
 <template>
-<transition :name="$store.state.device.animation ? 'fade' : ''" appear>
+<transition :name="$store.state.animation ? 'fade' : ''" appear>
 	<div class="nvlagfpb" @contextmenu.prevent.stop="() => {}">
 		<MkMenu :items="items" @close="$emit('closed')" class="_popup _shadow" :align="'left'"/>
 	</div>
diff --git a/src/client/components/ui/modal.vue b/src/client/components/ui/modal.vue
index 5a3e0f66a5..0d1038dce9 100644
--- a/src/client/components/ui/modal.vue
+++ b/src/client/components/ui/modal.vue
@@ -1,10 +1,10 @@
 <template>
 <div class="mk-modal" v-hotkey.global="keymap" :style="{ pointerEvents: showing ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
-	<transition :name="$store.state.device.animation ? 'modal-bg' : ''" appear>
+	<transition :name="$store.state.animation ? 'modal-bg' : ''" appear>
 		<div class="bg _modalBg" v-if="showing" @click="onBgClick"></div>
 	</transition>
 	<div class="content" :class="{ popup, fixed, top: position === 'top' }" @click.self="onBgClick" ref="content">
-		<transition :name="$store.state.device.animation ? popup ? 'modal-popup-content' : 'modal-content' : ''" appear @after-leave="$emit('closed')" @after-enter="childRendered">
+		<transition :name="$store.state.animation ? popup ? 'modal-popup-content' : 'modal-content' : ''" appear @after-leave="$emit('closed')" @after-enter="childRendered">
 			<slot v-if="showing"></slot>
 		</transition>
 	</div>
diff --git a/src/client/components/ui/pagination.vue b/src/client/components/ui/pagination.vue
index ca9f9f27db..89da66e6ae 100644
--- a/src/client/components/ui/pagination.vue
+++ b/src/client/components/ui/pagination.vue
@@ -5,7 +5,7 @@
 		<slot name="empty"></slot>
 	</div>
 	<div class="more" v-show="more" key="_more_">
-		<MkButton class="button" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
+		<MkButton class="button" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
 			<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
 			<template v-if="moreFetching"><MkLoading inline/></template>
 		</MkButton>
diff --git a/src/client/components/ui/window.vue b/src/client/components/ui/window.vue
index f1cf0d6836..eff1b62b55 100644
--- a/src/client/components/ui/window.vue
+++ b/src/client/components/ui/window.vue
@@ -1,5 +1,5 @@
 <template>
-<transition :name="$store.state.device.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
+<transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
 	<div class="ebkgocck" v-if="showing">
 		<div class="body _popup _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
 			<div class="header" @contextmenu.prevent.stop="onContextmenu">
diff --git a/src/client/components/url-preview.vue b/src/client/components/url-preview.vue
index 55872113be..90671a8f01 100644
--- a/src/client/components/url-preview.vue
+++ b/src/client/components/url-preview.vue
@@ -4,7 +4,7 @@
 	<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen />
 </div>
 <div v-else-if="tweetId && tweetExpanded" class="twitter" ref="twitter">
-	<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', left: `${tweetLeft}px`, width: `${tweetLeft < 0 ? 'auto' : '100%'}`, height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.device.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
+	<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', left: `${tweetLeft}px`, width: `${tweetLeft < 0 ? 'auto' : '100%'}`, height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
 </div>
 <div v-else class="mk-url-preview" v-size="{ max: [400, 350] }">
 	<transition name="zoom" mode="out-in">
diff --git a/src/client/components/user-info.vue b/src/client/components/user-info.vue
index 205a991e63..6f3b891fac 100644
--- a/src/client/components/user-info.vue
+++ b/src/client/components/user-info.vue
@@ -8,7 +8,7 @@
 	</div>
 	<div class="description">
 		<div class="mfm" v-if="user.description">
-			<Mfm :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+			<Mfm :text="user.description" :author="user" :i="$i" :custom-emojis="user.emojis"/>
 		</div>
 		<span v-else style="opacity: 0.7;">{{ $t('noAccountDescription') }}</span>
 	</div>
@@ -23,7 +23,7 @@
 			<p>{{ $t('followers') }}</p><span>{{ user.followersCount }}</span>
 		</div>
 	</div>
-	<MkFollowButton class="koudoku-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/>
+	<MkFollowButton class="koudoku-button" v-if="$i && user.id != $i.id" :user="user" mini/>
 </div>
 </template>
 
diff --git a/src/client/components/user-list.vue b/src/client/components/user-list.vue
index 0d58a2672a..97abf746a6 100644
--- a/src/client/components/user-list.vue
+++ b/src/client/components/user-list.vue
@@ -8,7 +8,7 @@
 	<div class="users">
 		<MkUserInfo class="user" v-for="user in users" :user="user" :key="user.id"/>
 	</div>
-	<button class="more" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :class="{ fetching: moreFetching }" v-show="more" :disabled="moreFetching">
+	<button class="more" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :class="{ fetching: moreFetching }" v-show="more" :disabled="moreFetching">
 		<template v-if="moreFetching"><Fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? $t('loading') : $t('loadMore') }}
 	</button>
 </div>
diff --git a/src/client/components/user-preview.vue b/src/client/components/user-preview.vue
index bc41cc822f..d7c99dc3fc 100644
--- a/src/client/components/user-preview.vue
+++ b/src/client/components/user-preview.vue
@@ -9,7 +9,7 @@
 				<p class="username"><MkAcct :user="user"/></p>
 			</div>
 			<div class="description">
-				<Mfm v-if="user.description" :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+				<Mfm v-if="user.description" :text="user.description" :author="user" :i="$i" :custom-emojis="user.emojis"/>
 			</div>
 			<div class="status">
 				<div>
@@ -22,7 +22,7 @@
 					<p>{{ $t('followers') }}</p><span>{{ user.followersCount }}</span>
 				</div>
 			</div>
-			<MkFollowButton class="koudoku-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/>
+			<MkFollowButton class="koudoku-button" v-if="$i && user.id != $i.id" :user="user" mini/>
 		</div>
 		<div v-else>
 			<MkLoading/>
diff --git a/src/client/components/user-select-dialog.vue b/src/client/components/user-select-dialog.vue
index cdb8990051..f977de2939 100644
--- a/src/client/components/user-select-dialog.vue
+++ b/src/client/components/user-select-dialog.vue
@@ -79,7 +79,7 @@ export default defineComponent({
 		});
 
 		this.recentUsers = await os.api('users/show', {
-			userIds: this.$store.state.device.recentlyUsedUsers
+			userIds: this.$store.state.recentlyUsedUsers
 		});
 	},
 
@@ -108,10 +108,10 @@ export default defineComponent({
 			this.$refs.dialog.close();
 
 			// 最近使ったユーザー更新
-			let recents = this.$store.state.device.recentlyUsedUsers;
+			let recents = this.$store.state.recentlyUsedUsers;
 			recents = recents.filter(x => x !== this.selected.id);
 			recents.unshift(this.selected.id);
-			this.$store.commit('device/set', { key: 'recentlyUsedUsers', value: recents.splice(0, 16) });
+			this.$store.set('recentlyUsedUsers', recents.splice(0, 16));
 		},
 
 		cancel() {
diff --git a/src/client/components/users-dialog.vue b/src/client/components/users-dialog.vue
index f2e8ec480e..46cbf69eb7 100644
--- a/src/client/components/users-dialog.vue
+++ b/src/client/components/users-dialog.vue
@@ -14,7 +14,7 @@
 			</div>
 		</MkA>
 	</div>
-	<button class="more _button" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching">
+	<button class="more _button" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching">
 		<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
 		<template v-if="moreFetching"><Fa :icon="faSpinner" pulse fixed-width/></template>
 	</button>
diff --git a/src/client/init.ts b/src/client/init.ts
index 9294733bbb..fa20622a1a 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -4,26 +4,54 @@
 
 import '@/style.scss';
 
-import { createApp } from 'vue';
+// TODO: そのうち消す
+if (localStorage.getItem('vuex') != null) {
+	const vuex = JSON.parse(localStorage.getItem('vuex'));
+
+	localStorage.setItem('accounts', JSON.stringify(vuex.device.accounts));
+	localStorage.setItem('miux:themes', JSON.stringify(vuex.device.themes));
+
+	for (const [k, v] of 	Object.entries(vuex.device.userData)) {
+		localStorage.setItem('pizzax::base::' + k, JSON.stringify({
+			widgets: v.widgets
+		}));
+
+		if (v.deck) {
+			localStorage.setItem('pizzax::deck::' + k, JSON.stringify({
+				columns: v.deck.columns,
+				layout: v.deck.layout,
+			}));
+		}
+	}
+
+	localStorage.removeItem('vuex');
+}
+
+import { createApp, watch } from 'vue';
 import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
 
-import widgets from './widgets';
-import directives from './directives';
+import widgets from '@/widgets';
+import directives from '@/directives';
 import components from '@/components';
-import { version, apiUrl, ui } from '@/config';
-import { store } from './store';
-import { router } from './router';
+import { version, ui } from '@/config';
+import { router } from '@/router';
 import { applyTheme } from '@/scripts/theme';
 import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
-import { i18n, lang } from './i18n';
+import { i18n, lang } from '@/i18n';
 import { stream, isMobile, dialog } from '@/os';
-import * as sound from './scripts/sound';
+import * as sound from '@/scripts/sound';
+import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
+import { defaultStore, ColdDeviceStorage } from '@/store';
+import { fetchInstance, instance } from '@/instance';
 
 console.info(`Misskey v${version}`);
 
 if (_DEV_) {
 	console.warn('Development mode!!!');
 
+	(window as any).$i = $i;
+	(window as any).$store = defaultStore;
+
 	window.addEventListener('error', event => {
 		console.error(event);
 		/*
@@ -81,80 +109,52 @@ html.setAttribute('lang', lang);
 //#endregion
 
 //#region Fetch user
-const signout = () => {
-	store.dispatch('logout');
-	location.href = '/';
-};
-
-// ユーザーをフェッチしてコールバックする
-const fetchme = (token) => new Promise((done, fail) => {
-	// Fetch user
-	fetch(`${apiUrl}/i`, {
-		method: 'POST',
-		body: JSON.stringify({
-			i: token
-		})
-	})
-	.then(res => {
-		// When failed to authenticate user
-		if (res.status !== 200 && res.status < 500) {
-			return signout();
-		}
-
-		// Parse response
-		res.json().then(i => {
-			i.token = token;
-			done(i);
-		});
-	})
-	.catch(fail);
-});
-
-// キャッシュがあったとき
-if (store.state.i != null) {
-	// TODO: i.token が null になるケースってどんな時だっけ?
-	if (store.state.i.token == null) {
-		signout();
+if ($i && $i.token) {
+	if (_DEV_) {
+		console.log('account cache found. refreshing...');
 	}
 
-	// 後から新鮮なデータをフェッチ
-	fetchme(store.state.i.token).then(freshData => {
-		store.dispatch('mergeMe', freshData);
-	});
+	refreshAccount();
 } else {
-	// Get token from localStorage
-	let i = localStorage.getItem('i');
+	if (_DEV_) {
+		console.log('no account cache found.');
+	}
 
 	// 連携ログインの場合用にCookieを参照する
-	if (i == null || i === 'null') {
-		i = (document.cookie.match(/igi=(\w+)/) || [null, null])[1];
-	}
+	const i = (document.cookie.match(/igi=(\w+)/) || [null, null])[1];
 
 	if (i != null && i !== 'null') {
+		if (_DEV_) {
+			console.log('signing...');
+		}
+
 		try {
 			document.body.innerHTML = '<div>Please wait...</div>';
-			const me = await fetchme(i);
-			await store.dispatch('login', me);
+			await login(i);
 			location.reload();
 		} catch (e) {
 			// Render the error screen
 			// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
 			document.body.innerHTML = '<div id="err">Oops!</div>';
 		}
+	} else {
+		if (_DEV_) {
+			console.log('not signed in');
+		}
 	}
 }
 //#endregion
 
-store.dispatch('instance/fetch').then(() => {
+fetchInstance().then(() => {
 	// Init service worker
 	//if (this.store.state.instance.meta.swPublickey) this.registerSw(this.store.state.instance.meta.swPublickey);
 });
 
-stream.init(store.state.i);
+stream.init($i);
 
 const app = createApp(await (
 	window.location.search === '?zen' ? import('@/ui/zen.vue') :
-	!store.getters.isSignedIn         ? import('@/ui/visitor.vue') :
+	!$i                               ? import('@/ui/visitor.vue') :
 	ui === 'deck'                     ? import('@/ui/deck.vue') :
 	ui === 'desktop'                  ? import('@/ui/desktop.vue') :
 	import('@/ui/default.vue')
@@ -164,7 +164,12 @@ if (_DEV_) {
 	app.config.performance = true;
 }
 
-app.use(store);
+app.config.globalProperties = {
+	$i,
+	$store: defaultStore,
+	$instance: instance,
+};
+
 app.use(router);
 app.use(i18n);
 // eslint-disable-next-line vue/component-definition-name-casing
@@ -180,46 +185,34 @@ await router.isReady();
 
 app.mount('body');
 
-// 他のタブと永続化されたstateを同期
-window.addEventListener('storage', e => {
-	if (e.key === 'vuex') {
-		store.replaceState({
-			...store.state,
-			...JSON.parse(e.newValue)
-		});
-	} else if (e.key === 'i') {
-		location.reload();
-	}
-}, false);
-
-store.watch(state => state.device.darkMode, darkMode => {
+watch(defaultStore.reactiveState.darkMode, (darkMode) => {
 	import('@/scripts/theme').then(({ builtinThemes }) => {
-		const themes = builtinThemes.concat(store.state.device.themes);
-		applyTheme(themes.find(x => x.id === (darkMode ? store.state.device.darkTheme : store.state.device.lightTheme)));
+		const themes = builtinThemes.concat(ColdDeviceStorage.get('themes'));
+		applyTheme(themes.find(x => x.id === (darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'))));
 	});
 });
 
 //#region Sync dark mode
-if (store.state.device.syncDeviceDarkMode) {
-	store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() });
+if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
+	defaultStore.set('darkMode', isDeviceDarkmode());
 }
 
 window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
-	if (store.state.device.syncDeviceDarkMode) {
-		store.commit('device/set', { key: 'darkMode', value: mql.matches });
+	if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
+		defaultStore.set('darkMode', mql.matches);
 	}
 });
 //#endregion
 
-store.watch(state => state.device.useBlurEffectForModal, v => {
+watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
 	document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
 }, { immediate: true });
 
 let reloadDialogShowing = false;
 stream.on('_disconnected_', async () => {
-	if (store.state.device.serverDisconnectedBehavior === 'reload') {
+	if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
 		location.reload();
-	} else if (store.state.device.serverDisconnectedBehavior === 'dialog') {
+	} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
 		if (reloadDialogShowing) return;
 		reloadDialogShowing = true;
 		const { canceled } = await dialog({
@@ -240,13 +233,13 @@ stream.on('emojiAdded', data => {
 	//store.commit('instance/set', );
 });
 
-for (const plugin of store.state.deviceUser.plugins.filter(p => p.active)) {
+for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
 	import('./plugin').then(({ install }) => {
 		install(plugin);
 	});
 }
 
-if (store.getters.isSignedIn) {
+if ($i) {
 	if ('Notification' in window) {
 		// 許可を得ていなかったらリクエスト
 		if (Notification.permission === 'default') {
@@ -258,103 +251,73 @@ if (store.getters.isSignedIn) {
 
 	// 自分の情報が更新されたとき
 	main.on('meUpdated', i => {
-		store.dispatch('mergeMe', i);
+		updateAccount(i);
 	});
 
 	main.on('readAllNotifications', () => {
-		store.dispatch('mergeMe', {
-			hasUnreadNotification: false
-		});
+		updateAccount({ hasUnreadNotification: false });
 	});
 
 	main.on('unreadNotification', () => {
-		store.dispatch('mergeMe', {
-			hasUnreadNotification: true
-		});
+		updateAccount({ hasUnreadNotification: true });
 	});
 
 	main.on('unreadMention', () => {
-		store.dispatch('mergeMe', {
-			hasUnreadMentions: true
-		});
+		updateAccount({ hasUnreadMentions: true });
 	});
 
 	main.on('readAllUnreadMentions', () => {
-		store.dispatch('mergeMe', {
-			hasUnreadMentions: false
-		});
+		updateAccount({ hasUnreadMentions: false });
 	});
 
 	main.on('unreadSpecifiedNote', () => {
-		store.dispatch('mergeMe', {
-			hasUnreadSpecifiedNotes: true
-		});
+		updateAccount({ hasUnreadSpecifiedNotes: true });
 	});
 
 	main.on('readAllUnreadSpecifiedNotes', () => {
-		store.dispatch('mergeMe', {
-			hasUnreadSpecifiedNotes: false
-		});
+		updateAccount({ hasUnreadSpecifiedNotes: false });
 	});
 
 	main.on('readAllMessagingMessages', () => {
-		store.dispatch('mergeMe', {
-			hasUnreadMessagingMessage: false
-		});
+		updateAccount({ hasUnreadMessagingMessage: false });
 	});
 
 	main.on('unreadMessagingMessage', () => {
-		store.dispatch('mergeMe', {
-			hasUnreadMessagingMessage: true
-		});
-
+		updateAccount({ hasUnreadMessagingMessage: true });
 		sound.play('chatBg');
 	});
 
 	main.on('readAllAntennas', () => {
-		store.dispatch('mergeMe', {
-			hasUnreadAntenna: false
-		});
+		updateAccount({ hasUnreadAntenna: false });
 	});
 
 	main.on('unreadAntenna', () => {
-		store.dispatch('mergeMe', {
-			hasUnreadAntenna: true
-		});
-
+		updateAccount({ hasUnreadAntenna: true });
 		sound.play('antenna');
 	});
 
 	main.on('readAllAnnouncements', () => {
-		store.dispatch('mergeMe', {
-			hasUnreadAnnouncement: false
-		});
+		updateAccount({ hasUnreadAnnouncement: false });
 	});
 
 	main.on('readAllChannels', () => {
-		store.dispatch('mergeMe', {
-			hasUnreadChannel: false
-		});
+		updateAccount({ hasUnreadChannel: false });
 	});
 
 	main.on('unreadChannel', () => {
-		store.dispatch('mergeMe', {
-			hasUnreadChannel: true
-		});
-
+		updateAccount({ hasUnreadChannel: true });
 		sound.play('channel');
 	});
 
 	main.on('readAllAnnouncements', () => {
-		store.dispatch('mergeMe', {
-			hasUnreadAnnouncement: false
-		});
+		updateAccount({ hasUnreadAnnouncement: false });
 	});
 
 	main.on('clientSettingUpdated', x => {
-		store.commit('settings/set', {
-			key: x.key,
-			value: x.value
+		updateAccount({
+			clientData: {
+				[x.key]: x.value
+			}
 		});
 	});
 
@@ -364,4 +327,3 @@ if (store.getters.isSignedIn) {
 		signout();
 	});
 }
-
diff --git a/src/client/instance.ts b/src/client/instance.ts
new file mode 100644
index 0000000000..89c0368599
--- /dev/null
+++ b/src/client/instance.ts
@@ -0,0 +1,45 @@
+import { computed, reactive } from 'vue';
+import { api } from './os';
+
+// TODO: 他のタブと永続化されたstateを同期
+
+type Instance = {
+	emojis: {
+		category: string;
+	}[];
+};
+
+const data = localStorage.getItem('instance');
+
+// TODO: instanceをリアクティブにするかは再考の余地あり
+
+export const instance: Instance = reactive(data ? JSON.parse(data) : {
+	// TODO: set default values
+});
+
+export async function fetchInstance() {
+	const meta = await api('meta', {
+		detail: false
+	});
+
+	for (const [k, v] of Object.entries(meta)) {
+		instance[k] = v;
+	}
+
+	localStorage.setItem('instance', JSON.stringify(instance));
+}
+
+export const emojiCategories = computed(() => {
+	const categories = new Set();
+	for (const emoji of instance.emojis) {
+		categories.add(emoji.category);
+	}
+	return Array.from(categories);
+});
+
+// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
+declare module '@vue/runtime-core' {
+	interface ComponentCustomProperties {
+		$instance: typeof instance;
+	}
+}
diff --git a/src/client/os.ts b/src/client/os.ts
index d43de4de44..efdb11eb59 100644
--- a/src/client/os.ts
+++ b/src/client/os.ts
@@ -1,12 +1,11 @@
 import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue';
 import { EventEmitter } from 'eventemitter3';
 import Stream from '@/scripts/stream';
-import { store } from '@/store';
 import { apiUrl, debug } from '@/config';
 import MkPostFormDialog from '@/components/post-form-dialog.vue';
 import MkWaitingDialog from '@/components/waiting-dialog.vue';
 import { resolve } from '@/router';
-import { device } from './cold-storage';
+import { $i } from './account';
 
 const ua = navigator.userAgent.toLowerCase();
 export const isMobile = /mobile|iphone|ipad|android/.test(ua);
@@ -40,7 +39,7 @@ export function api(endpoint: string, data: Record<string, any> = {}, token?: st
 
 	const promise = new Promise((resolve, reject) => {
 		// Append a credential
-		if (store.getters.isSignedIn) (data as any).i = store.state.i.token;
+		if ($i) (data as any).i = $i.token;
 		if (token !== undefined) (data as any).i = token;
 
 		// Send request
@@ -368,7 +367,7 @@ export function upload(file: File, folder?: any, name?: string) {
 			uploads.value.push(ctx);
 
 			const data = new FormData();
-			data.append('i', store.state.i.token);
+			data.append('i', $i.token);
 			data.append('force', 'true');
 			data.append('file', file);
 
diff --git a/src/client/pages/_error_.vue b/src/client/pages/_error_.vue
index c063b12b2e..96a1ece11b 100644
--- a/src/client/pages/_error_.vue
+++ b/src/client/pages/_error_.vue
@@ -1,5 +1,5 @@
 <template>
-<transition :name="$store.state.device.animation ? 'zoom' : ''" appear>
+<transition :name="$store.state.animation ? 'zoom' : ''" appear>
 	<div class="_section">
 		<div class="mjndxjch _content">
 			<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
diff --git a/src/client/pages/about-misskey.vue b/src/client/pages/about-misskey.vue
index 8da5a1f698..b0f45892d2 100644
--- a/src/client/pages/about-misskey.vue
+++ b/src/client/pages/about-misskey.vue
@@ -7,7 +7,7 @@
 				<img src="/assets/icons/512.png" alt="" class="icon" ref="icon" @load="iconLoaded" draggable="false"/>
 				<div class="misskey">Misskey</div>
 				<div class="version">v{{ version }}</div>
-				<span class="emoji" v-for="emoji in easterEggEmojis" :key="emoji.id" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$store.state.instance.meta.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
+				<span class="emoji" v-for="emoji in easterEggEmojis" :key="emoji.id" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
 			</div>
 		</section>
 		<section class="_formItem" style="text-align: center; padding: 0 16px;" @click="gravity">
@@ -144,7 +144,7 @@ export default defineComponent({
 
 	methods: {
 		iconLoaded() {
-			const emojis = this.$store.state.settings.reactions;
+			const emojis = this.$store.state.reactions;
 			const containerWidth = this.$refs.about.offsetWidth;
 			for (let i = 0; i < 32; i++) {
 				this.easterEggEmojis.push({
diff --git a/src/client/pages/about.vue b/src/client/pages/about.vue
index 88358b78f5..a7a277d86a 100644
--- a/src/client/pages/about.vue
+++ b/src/client/pages/about.vue
@@ -59,7 +59,7 @@ export default defineComponent({
 
 	computed: {
 		meta() {
-			return this.$store.state.instance.meta;
+			return this.$instance;
 		},
 	},
 });
diff --git a/src/client/pages/announcements.vue b/src/client/pages/announcements.vue
index 50dc994025..cb9cd7048d 100644
--- a/src/client/pages/announcements.vue
+++ b/src/client/pages/announcements.vue
@@ -2,12 +2,12 @@
 <div class="_section">
 	<MkPagination :pagination="pagination" #default="{items}" class="ruryvtyk _content">
 		<section class="_card announcement _vMargin" v-for="(announcement, i) in items" :key="announcement.id">
-			<div class="_title"><span v-if="$store.getters.isSignedIn && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
+			<div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
 			<div class="_content">
 				<Mfm :text="announcement.text"/>
 				<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
 			</div>
-			<div class="_footer" v-if="$store.getters.isSignedIn && !announcement.isRead">
+			<div class="_footer" v-if="$i && !announcement.isRead">
 				<MkButton @click="read(items, announcement, i)" primary><Fa :icon="faCheck"/> {{ $t('gotIt') }}</MkButton>
 			</div>
 		</section>
diff --git a/src/client/pages/auth.vue b/src/client/pages/auth.vue
index 4b67658b7d..b8a8c009e0 100755
--- a/src/client/pages/auth.vue
+++ b/src/client/pages/auth.vue
@@ -1,8 +1,8 @@
 <template>
-<div class="" v-if="$store.getters.isSignedIn && fetching">
+<div class="" v-if="$i && fetching">
 	<MkLoading/>
 </div>
-<div v-else-if="$store.getters.isSignedIn">
+<div v-else-if="$i">
 	<XForm
 		class="form"
 		ref="form"
@@ -33,6 +33,7 @@ import { defineComponent } from 'vue';
 import XForm from './auth.form.vue';
 import MkSignin from '@/components/signin.vue';
 import * as os from '@/os';
+import { login } from '@/account';
 
 export default defineComponent({
 	components: {
@@ -52,7 +53,7 @@ export default defineComponent({
 		}
 	},
 	mounted() {
-		if (!this.$store.getters.isSignedIn) return;
+		if (!this.$i) return;
 
 		// Fetch session
 		os.api('auth/session/show', {
@@ -83,8 +84,7 @@ export default defineComponent({
 				location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
 			}
 		}, onLogin(res) {
-			localStorage.setItem('i', res.i);
-			location.reload();
+			login(res.i);
 		}
 	}
 });
diff --git a/src/client/pages/channel.vue b/src/client/pages/channel.vue
index fe9092880a..790ad5230c 100644
--- a/src/client/pages/channel.vue
+++ b/src/client/pages/channel.vue
@@ -16,11 +16,11 @@
 			<div class="fade"></div>
 		</div>
 		<div class="description" v-if="channel.description">
-			<Mfm :text="channel.description" :is-note="false" :i="$store.state.i"/>
+			<Mfm :text="channel.description" :is-note="false" :i="$i"/>
 		</div>
 	</div>
 
-	<XPostForm :channel="channel" class="post-form _content _panel _vMargin" fixed v-if="$store.getters.isSignedIn"/>
+	<XPostForm :channel="channel" class="post-form _content _panel _vMargin" fixed v-if="$i"/>
 
 	<XTimeline class="_content _vMargin" src="channel" :channel="channelId" @before="before" @after="after"/>
 </div>
diff --git a/src/client/pages/channels.vue b/src/client/pages/channels.vue
index c355623bad..d1af46db53 100644
--- a/src/client/pages/channels.vue
+++ b/src/client/pages/channels.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	<div class="_section" style="padding: 0;" v-if="$store.getters.isSignedIn">
+	<div class="_section" style="padding: 0;" v-if="$i">
 		<MkTab class="_content" v-model:value="tab">
 			<option value="featured"><Fa :icon="faFireAlt"/> {{ $t('_channel.featured') }}</option>
 			<option value="following"><Fa :icon="faHeart"/> {{ $t('_channel.following') }}</option>
diff --git a/src/client/pages/clip.vue b/src/client/pages/clip.vue
index 7535e4de2e..ae6fa67700 100644
--- a/src/client/pages/clip.vue
+++ b/src/client/pages/clip.vue
@@ -2,7 +2,7 @@
 <div v-if="clip" class="_section">
 	<div class="okzinsic _content _panel _vMargin">
 		<div class="description" v-if="clip.description">
-			<Mfm :text="clip.description" :is-note="false" :i="$store.state.i"/>
+			<Mfm :text="clip.description" :is-note="false" :i="$i"/>
 		</div>
 		<div class="user">
 			<MkAvatar :user="clip.user" class="avatar"/> <MkUserName :user="clip.user" :nowrap="false"/>
@@ -58,7 +58,7 @@ export default defineComponent({
 
 	computed: {
 		isOwned(): boolean {
-			return this.$store.getters.isSignedIn && this.clip && (this.$store.state.i.id === this.clip.userId);
+			return this.$i && this.clip && (this.$i.id === this.clip.userId);
 		}
 	},
 
diff --git a/src/client/pages/explore.vue b/src/client/pages/explore.vue
index 3eecd8122d..2920fcb017 100644
--- a/src/client/pages/explore.vue
+++ b/src/client/pages/explore.vue
@@ -142,7 +142,7 @@ export default defineComponent({
 
 	computed: {
 		meta() {
-			return this.$store.state.instance.meta;
+			return this.$instance;
 		},
 		tagUsers(): any {
 			return {
diff --git a/src/client/pages/follow-requests.vue b/src/client/pages/follow-requests.vue
index d72efb5a3c..3c4b94fb1a 100644
--- a/src/client/pages/follow-requests.vue
+++ b/src/client/pages/follow-requests.vue
@@ -16,7 +16,7 @@
 						<p class="acct">@{{ acct(req.follower) }}</p>
 					</div>
 					<div class="description" v-if="req.follower.description" :title="req.follower.description">
-						<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$store.state.i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
+						<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
 					</div>
 					<div class="actions">
 						<button class="_button" @click="accept(req.follower)"><Fa :icon="faCheck"/></button>
diff --git a/src/client/pages/instance/index.metrics.vue b/src/client/pages/instance/index.metrics.vue
index f3060b29d5..4fd7e55ab1 100644
--- a/src/client/pages/instance/index.metrics.vue
+++ b/src/client/pages/instance/index.metrics.vue
@@ -160,7 +160,7 @@ export default defineComponent({
 	computed: {
 		gridColor() {
 			// TODO: var(--panel)の色が暗いか明るいかで判定する
-			return this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+			return this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 		},
 	},
 
diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue
index 79fe452589..e9614acf70 100644
--- a/src/client/pages/instance/index.vue
+++ b/src/client/pages/instance/index.vue
@@ -122,7 +122,7 @@ export default defineComponent({
 
 	computed: {
 		meta() {
-			return this.$store.state.instance.meta;
+			return this.$instance;
 		},
 	},
 
diff --git a/src/client/pages/instance/instance.vue b/src/client/pages/instance/instance.vue
index 97f85d3b1f..6e626cb178 100644
--- a/src/client/pages/instance/instance.vue
+++ b/src/client/pages/instance/instance.vue
@@ -206,7 +206,7 @@ export default defineComponent({
 		},
 
 		meta() {
-			return this.$store.state.instance.meta;
+			return this.$instance;
 		},
 
 		isBlocked() {
diff --git a/src/client/pages/instance/queue.chart.vue b/src/client/pages/instance/queue.chart.vue
index 742c2b7d3c..6a43d14a6b 100644
--- a/src/client/pages/instance/queue.chart.vue
+++ b/src/client/pages/instance/queue.chart.vue
@@ -63,7 +63,7 @@ export default defineComponent({
 		this.fetchJobs();
 
 		// TODO: var(--panel)の色が暗いか明るいかで判定する
-		const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+		const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 
 		Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 
diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue
index d9d87ddbe3..62271b3ebe 100644
--- a/src/client/pages/instance/settings.vue
+++ b/src/client/pages/instance/settings.vue
@@ -259,6 +259,7 @@ import MkInfo from '@/components/ui/info.vue';
 import { url } from '@/config';
 import getAcct from '../../../misc/acct/render';
 import * as os from '@/os';
+import { fetchInstance } from '@/instance';
 
 export default defineComponent({
 	components: {
@@ -565,7 +566,7 @@ export default defineComponent({
 				summalyProxy: this.summalyProxy,
 				useStarForReactionFallback: this.useStarForReactionFallback,
 			}).then(() => {
-				this.$store.dispatch('instance/fetch');
+				fetchInstance();
 				if (withDialog) {
 					os.success();
 				}
diff --git a/src/client/pages/instance/user-dialog.vue b/src/client/pages/instance/user-dialog.vue
index 059a5c194c..810db8c508 100644
--- a/src/client/pages/instance/user-dialog.vue
+++ b/src/client/pages/instance/user-dialog.vue
@@ -24,7 +24,7 @@
 		</div>
 		<div class="_section">
 			<div class="_content">
-				<MkSwitch v-if="user.host == null && $store.state.i.isAdmin && (moderator || !user.isAdmin)" @update:value="toggleModerator" v-model:value="moderator">{{ $t('moderator') }}</MkSwitch>
+				<MkSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" @update:value="toggleModerator" v-model:value="moderator">{{ $t('moderator') }}</MkSwitch>
 				<MkSwitch @update:value="toggleSilence" v-model:value="silenced">{{ $t('silence') }}</MkSwitch>
 				<MkSwitch @update:value="toggleSuspend" v-model:value="suspended">{{ $t('suspend') }}</MkSwitch>
 			</div>
diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue
index c56542c689..3df15cbd5b 100644
--- a/src/client/pages/messaging/index.vue
+++ b/src/client/pages/messaging/index.vue
@@ -6,7 +6,7 @@
 		<div class="history" v-if="messages.length > 0">
 			<MkA v-for="(message, i) in messages"
 				class="message _panel"
-				:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($store.state.i.id) : message.isRead }"
+				:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
 				:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
 				:data-index="i"
 				:key="message.id"
@@ -88,7 +88,7 @@ export default defineComponent({
 		getAcct,
 
 		isMe(message) {
-			return message.userId == this.$store.state.i.id;
+			return message.userId == this.$i.id;
 		},
 
 		onMessage(message) {
@@ -111,7 +111,7 @@ export default defineComponent({
 					if (found.recipientId) {
 						found.isRead = true;
 					} else if (found.groupId) {
-						found.reads.push(this.$store.state.i.id);
+						found.reads.push(this.$i.id);
 					}
 				}
 			}
diff --git a/src/client/pages/messaging/messaging-room.form.vue b/src/client/pages/messaging/messaging-room.form.vue
index 3b5b9aa966..ca7fa4905f 100644
--- a/src/client/pages/messaging/messaging-room.form.vue
+++ b/src/client/pages/messaging/messaging-room.form.vue
@@ -91,8 +91,8 @@ export default defineComponent({
 					const file = items[0].getAsFile();
 					const lio = file.name.lastIndexOf('.');
 					const ext = lio >= 0 ? file.name.slice(lio) : '';
-					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, '1')}${ext}`;
-					const name = this.$store.state.settings.pasteDialog
+					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, '1')}${ext}`;
+					const name = this.$store.state.pasteDialog
 						? await os.dialog({
 							title: this.$t('enterFileName'),
 							input: {
@@ -163,7 +163,7 @@ export default defineComponent({
 		},
 
 		upload(file: File, name?: string) {
-			os.upload(file, this.$store.state.settings.uploadFolder, name).then(res => {
+			os.upload(file, this.$store.state.uploadFolder, name).then(res => {
 				this.file = res;
 			});
 		},
diff --git a/src/client/pages/messaging/messaging-room.message.vue b/src/client/pages/messaging/messaging-room.message.vue
index 33ad371ebb..0b6d9affaf 100644
--- a/src/client/pages/messaging/messaging-room.message.vue
+++ b/src/client/pages/messaging/messaging-room.message.vue
@@ -7,7 +7,7 @@
 				<img src="/assets/remove.png" alt="Delete"/>
 			</button>
 			<div class="content" v-if="!message.isDeleted">
-				<Mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
+				<Mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$i"/>
 				<div class="file" v-if="message.file">
 					<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
 						<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
@@ -56,7 +56,7 @@ export default defineComponent({
 	},
 	computed: {
 		isMe(): boolean {
-			return this.message.userId === this.$store.state.i.id;
+			return this.message.userId === this.$i.id;
 		},
 		urls(): string[] {
 			if (this.message.text) {
diff --git a/src/client/pages/messaging/messaging-room.vue b/src/client/pages/messaging/messaging-room.vue
index d4331c1390..0a26b4926d 100644
--- a/src/client/pages/messaging/messaging-room.vue
+++ b/src/client/pages/messaging/messaging-room.vue
@@ -110,7 +110,7 @@ const Component = defineComponent({
 
 	mounted() {
 		this.fetch();
-		if (this.$store.state.device.enableInfiniteScroll) {
+		if (this.$store.state.enableInfiniteScroll) {
 			this.$nextTick(() => this.ilObserver.observe(this.$refs.loadMore as Element));
 		}
 	},
@@ -224,7 +224,7 @@ const Component = defineComponent({
 			const _isBottom = isBottom(this.$el, 64);
 
 			this.messages.push(message);
-			if (message.userId != this.$store.state.i.id && !document.hidden) {
+			if (message.userId != this.$i.id && !document.hidden) {
 				this.connection.send('read', {
 					id: message.id
 				});
@@ -235,7 +235,7 @@ const Component = defineComponent({
 				this.$nextTick(() => {
 					this.scrollToBottom();
 				});
-			} else if (message.userId != this.$store.state.i.id) {
+			} else if (message.userId != this.$i.id) {
 				// Notify
 				this.notifyNewMessage();
 			}
@@ -299,7 +299,7 @@ const Component = defineComponent({
 		onVisibilitychange() {
 			if (document.hidden) return;
 			for (const message of this.messages) {
-				if (message.userId !== this.$store.state.i.id && !message.isRead) {
+				if (message.userId !== this.$i.id && !message.isRead) {
 					this.connection.send('read', {
 						id: message.id
 					});
diff --git a/src/client/pages/mfm-cheat-sheet.vue b/src/client/pages/mfm-cheat-sheet.vue
index c9346d3fc9..f1e9dc1950 100644
--- a/src/client/pages/mfm-cheat-sheet.vue
+++ b/src/client/pages/mfm-cheat-sheet.vue
@@ -238,7 +238,7 @@ export default defineComponent({
 			preview_hashtag: '#test',
 			preview_url: `https://example.com`,
 			preview_link: `[${this.$t('_mfm.dummy')}](https://example.com)`,
-			preview_emoji: `:${this.$store.state.instance.meta.emojis[0].name}:`,
+			preview_emoji: `:${this.$instance.emojis[0].name}:`,
 			preview_bold: `**${this.$t('_mfm.dummy')}**`,
 			preview_small: `<small>${this.$t('_mfm.dummy')}</small>`,
 			preview_center: `<center>${this.$t('_mfm.dummy')}</center>`,
diff --git a/src/client/pages/miauth.vue b/src/client/pages/miauth.vue
index 2de058d0dd..6cf5f39c55 100644
--- a/src/client/pages/miauth.vue
+++ b/src/client/pages/miauth.vue
@@ -1,5 +1,5 @@
 <template>
-<div v-if="$store.getters.isSignedIn">
+<div v-if="$i">
 	<div class="waiting _section" v-if="state == 'waiting'">
 		<div class="_content">
 			<MkLoading/>
@@ -41,6 +41,7 @@ import { defineComponent } from 'vue';
 import MkSignin from '@/components/signin.vue';
 import MkButton from '@/components/ui/button.vue';
 import * as os from '@/os';
+import { login } from '@/account';
 
 export default defineComponent({
 	components: {
@@ -88,8 +89,7 @@ export default defineComponent({
 			this.state = 'denied';
 		},
 		onLogin(res) {
-			localStorage.setItem('i', res.i);
-			location.reload();
+			login(res.i);
 		}
 	}
 });
diff --git a/src/client/pages/page-editor/page-editor.vue b/src/client/pages/page-editor/page-editor.vue
index a9fc0cb49e..d33dd1b7fe 100644
--- a/src/client/pages/page-editor/page-editor.vue
+++ b/src/client/pages/page-editor/page-editor.vue
@@ -141,7 +141,7 @@ export default defineComponent({
 				title: this.$t('_pages.newPage'),
 				icon: faPencilAlt,
 			}),
-			author: this.$store.state.i,
+			author: this.$i,
 			readonly: false,
 			page: null,
 			pageId: null,
diff --git a/src/client/pages/page.vue b/src/client/pages/page.vue
index 43c1688824..f2ccce67bf 100644
--- a/src/client/pages/page.vue
+++ b/src/client/pages/page.vue
@@ -21,9 +21,9 @@
 	<div class="_section links">
 		<div class="_content">
 			<MkA :to="`./${page.name}/view-source`" class="link">{{ $t('_pages.viewSource') }}</MkA>
-			<template v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId">
+			<template v-if="$i && $i.id === page.userId">
 				<MkA :to="`/pages/edit/${page.id}`" class="link">{{ $t('_pages.editThisPage') }}</MkA>
-				<button v-if="$store.state.i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $t('unpin') }}</button>
+				<button v-if="$i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $t('unpin') }}</button>
 				<button v-else @click="pin(true)" class="link _textButton">{{ $t('pin') }}</button>
 			</template>
 		</div>
diff --git a/src/client/pages/pages.vue b/src/client/pages/pages.vue
index 5121e36419..26dcddddf9 100644
--- a/src/client/pages/pages.vue
+++ b/src/client/pages/pages.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	<MkTab v-model:value="tab" v-if="$store.getters.isSignedIn">
+	<MkTab v-model:value="tab" v-if="$i">
 		<option value="featured"><Fa :icon="faFireAlt"/> {{ $t('_pages.featured') }}</option>
 		<option value="my"><Fa :icon="faEdit"/> {{ $t('_pages.my') }}</option>
 		<option value="liked"><Fa :icon="faHeart"/> {{ $t('_pages.liked') }}</option>
diff --git a/src/client/pages/reversi/game.board.vue b/src/client/pages/reversi/game.board.vue
index 302d7bc79c..0c4b1505b4 100644
--- a/src/client/pages/reversi/game.board.vue
+++ b/src/client/pages/reversi/game.board.vue
@@ -22,11 +22,11 @@
 	</div>
 
 	<div class="board">
-		<div class="labels-x" v-if="$store.state.settings.gamesReversiShowBoardLabels">
+		<div class="labels-x" v-if="$store.state.gamesReversiShowBoardLabels">
 			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
 		</div>
 		<div class="flex">
-			<div class="labels-y" v-if="$store.state.settings.gamesReversiShowBoardLabels">
+			<div class="labels-y" v-if="$store.state.gamesReversiShowBoardLabels">
 				<div v-for="i in game.map.length">{{ i }}</div>
 			</div>
 			<div class="cells" :style="cellsStyle">
@@ -35,7 +35,7 @@
 					@click="set(i)"
 					:title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`"
 				>
-					<template v-if="$store.state.settings.gamesReversiUseAvatarStones || true">
+					<template v-if="$store.state.gamesReversiUseAvatarStones || true">
 						<img v-if="stone === true" :src="blackUser.avatarUrl" alt="black">
 						<img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white">
 					</template>
@@ -45,11 +45,11 @@
 					</template>
 				</div>
 			</div>
-			<div class="labels-y" v-if="$store.state.settings.gamesReversiShowBoardLabels">
+			<div class="labels-y" v-if="$store.state.gamesReversiShowBoardLabels">
 				<div v-for="i in game.map.length">{{ i }}</div>
 			</div>
 		</div>
-		<div class="labels-x" v-if="$store.state.settings.gamesReversiShowBoardLabels">
+		<div class="labels-x" v-if="$store.state.gamesReversiShowBoardLabels">
 			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
 		</div>
 	</div>
@@ -126,14 +126,14 @@ export default defineComponent({
 
 	computed: {
 		iAmPlayer(): boolean {
-			if (!this.$store.getters.isSignedIn) return false;
-			return this.game.user1Id == this.$store.state.i.id || this.game.user2Id == this.$store.state.i.id;
+			if (!this.$i) return false;
+			return this.game.user1Id == this.$i.id || this.game.user2Id == this.$i.id;
 		},
 
 		myColor(): Color {
 			if (!this.iAmPlayer) return null;
-			if (this.game.user1Id == this.$store.state.i.id && this.game.black == 1) return true;
-			if (this.game.user2Id == this.$store.state.i.id && this.game.black == 2) return true;
+			if (this.game.user1Id == this.$i.id && this.game.black == 1) return true;
+			if (this.game.user2Id == this.$i.id && this.game.black == 2) return true;
 			return false;
 		},
 
@@ -234,7 +234,7 @@ export default defineComponent({
 		isMyTurn(): boolean {
 			if (!this.iAmPlayer) return false;
 			if (this.turnUser() == null) return false;
-			return this.turnUser().id == this.$store.state.i.id;
+			return this.turnUser().id == this.$i.id;
 		},
 
 		set(pos) {
diff --git a/src/client/pages/reversi/game.setting.vue b/src/client/pages/reversi/game.setting.vue
index d679d0f6d8..0a98d880e7 100644
--- a/src/client/pages/reversi/game.setting.vue
+++ b/src/client/pages/reversi/game.setting.vue
@@ -169,13 +169,13 @@ export default defineComponent({
 			return categories.filter((item, pos) => categories.indexOf(item) == pos);
 		},
 		isAccepted(): boolean {
-			if (this.game.user1Id == this.$store.state.i.id && this.game.user1Accepted) return true;
-			if (this.game.user2Id == this.$store.state.i.id && this.game.user2Accepted) return true;
+			if (this.game.user1Id == this.$i.id && this.game.user1Accepted) return true;
+			if (this.game.user2Id == this.$i.id && this.game.user2Accepted) return true;
 			return false;
 		},
 		isOpAccepted(): boolean {
-			if (this.game.user1Id != this.$store.state.i.id && this.game.user1Accepted) return true;
-			if (this.game.user2Id != this.$store.state.i.id && this.game.user2Accepted) return true;
+			if (this.game.user1Id != this.$i.id && this.game.user1Accepted) return true;
+			if (this.game.user2Id != this.$i.id && this.game.user2Accepted) return true;
 			return false;
 		}
 	},
@@ -186,8 +186,8 @@ export default defineComponent({
 		this.connection.on('initForm', this.onInitForm);
 		this.connection.on('message', this.onMessage);
 
-		if (this.game.user1Id != this.$store.state.i.id && this.game.form1) this.form = this.game.form1;
-		if (this.game.user2Id != this.$store.state.i.id && this.game.form2) this.form = this.game.form2;
+		if (this.game.user1Id != this.$i.id && this.game.form1) this.form = this.game.form1;
+		if (this.game.user2Id != this.$i.id && this.game.form2) this.form = this.game.form2;
 	},
 
 	beforeUnmount() {
@@ -233,12 +233,12 @@ export default defineComponent({
 		},
 
 		onInitForm(x) {
-			if (x.userId == this.$store.state.i.id) return;
+			if (x.userId == this.$i.id) return;
 			this.form = x.form;
 		},
 
 		onMessage(x) {
-			if (x.userId == this.$store.state.i.id) return;
+			if (x.userId == this.$i.id) return;
 			this.messages.unshift(x.message);
 		},
 
diff --git a/src/client/pages/reversi/index.vue b/src/client/pages/reversi/index.vue
index 95806c4088..0ae0ad5a04 100644
--- a/src/client/pages/reversi/index.vue
+++ b/src/client/pages/reversi/index.vue
@@ -91,7 +91,7 @@ export default defineComponent({
 	},
 
 	mounted() {
-		if (this.$store.getters.isSignedIn) {
+		if (this.$i) {
 			this.connection = os.stream.useSharedConnection('gamesReversi');
 
 			this.connection.on('invited', this.onInvited);
diff --git a/src/client/pages/room/room.vue b/src/client/pages/room/room.vue
index 91d8077c64..b60372ccb1 100644
--- a/src/client/pages/room/room.vue
+++ b/src/client/pages/room/room.vue
@@ -62,6 +62,7 @@ import MkButton from '@/components/ui/button.vue';
 import MkSelect from '@/components/ui/select.vue';
 import { selectFile } from '@/scripts/select-file';
 import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
 
 let room: Room;
 
@@ -107,7 +108,7 @@ export default defineComponent({
 			...parseAcct(this.acct)
 		});
 
-		this.isMyRoom = this.$store.getters.isSignedIn && (this.$store.state.i.id === this.user.id);
+		this.isMyRoom = this.$i && (this.$i.id === this.user.id);
 
 		const roomInfo = await os.api('room/show', {
 			userId: this.user.id
@@ -117,7 +118,7 @@ export default defineComponent({
 		this.carpetColor = roomInfo.carpetColor;
 
 		room = new Room(this.user, this.isMyRoom, roomInfo, this.$el, {
-			graphicsQuality: this.$store.state.device.roomGraphicsQuality,
+			graphicsQuality: ColdDeviceStorage.get('roomGraphicsQuality'),
 			onChangeSelect: obj => {
 				this.objectSelected = obj != null;
 				if (obj) {
@@ -132,7 +133,7 @@ export default defineComponent({
 					});
 				}
 			},
-			useOrthographicCamera: this.$store.state.device.roomUseOrthographicCamera
+			useOrthographicCamera: ColdDeviceStorage.get('roomUseOrthographicCamera'),
 		});
 	},
 
diff --git a/src/client/pages/settings/2fa.vue b/src/client/pages/settings/2fa.vue
index dc6d12a40f..0e75382ee8 100644
--- a/src/client/pages/settings/2fa.vue
+++ b/src/client/pages/settings/2fa.vue
@@ -2,8 +2,8 @@
 <section class="_card">
 	<div class="_title"><Fa :icon="faLock"/> {{ $t('twoStepAuthentication') }}</div>
 	<div class="_content">
-		<MkButton v-if="!data && !$store.state.i.twoFactorEnabled" @click="register">{{ $t('_2fa.registerDevice') }}</MkButton>
-		<template v-if="$store.state.i.twoFactorEnabled">
+		<MkButton v-if="!data && !$i.twoFactorEnabled" @click="register">{{ $t('_2fa.registerDevice') }}</MkButton>
+		<template v-if="$i.twoFactorEnabled">
 			<p>{{ $t('_2fa.alreadyRegistered') }}</p>
 			<MkButton @click="unregister">{{ $t('unregister') }}</MkButton>
 
@@ -13,14 +13,14 @@
 				<h2 class="heading">{{ $t('securityKey') }}</h2>
 				<p>{{ $t('_2fa.securityKeyInfo') }}</p>
 				<div class="key-list">
-					<div class="key" v-for="key in $store.state.i.securityKeysList">
+					<div class="key" v-for="key in $i.securityKeysList">
 						<h3>{{ key.name }}</h3>
 						<div class="last-used">{{ $t('lastUsed') }}<MkTime :time="key.lastUsed"/></div>
 						<MkButton @click="unregisterKey(key)">{{ $t('unregister') }}</MkButton>
 					</div>
 				</div>
 
-				<MkSwitch v-model:value="usePasswordLessLogin" @update:value="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0">{{ $t('passwordLessLogin') }}</MkSwitch>
+				<MkSwitch v-model:value="usePasswordLessLogin" @update:value="updatePasswordLessLogin" v-if="$i.securityKeysList.length > 0">{{ $t('passwordLessLogin') }}</MkSwitch>
 
 				<MkInfo warn v-if="registration && registration.error">{{ $t('error') }} {{ registration.error }}</MkInfo>
 				<MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('_2fa.registerKey') }}</MkButton>
@@ -42,7 +42,7 @@
 				</ol>
 			</template>
 		</template>
-		<div v-if="data && !$store.state.i.twoFactorEnabled">
+		<div v-if="data && !$i.twoFactorEnabled">
 			<ol style="margin: 0; padding: 0 0 0 1em;">
 				<li>
 					<i18n-t keypath="_2fa.step1" tag="span">
@@ -96,7 +96,7 @@ export default defineComponent({
 			},
 			data: null,
 			supportsCredentials: !!navigator.credentials,
-			usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin,
+			usePasswordLessLogin: this.$i.usePasswordLessLogin,
 			registration: null,
 			keyName: '',
 			token: null,
@@ -136,7 +136,7 @@ export default defineComponent({
 					this.updatePasswordLessLogin();
 				}).then(() => {
 					os.success();
-					this.$store.state.i.twoFactorEnabled = false;
+					this.$i.twoFactorEnabled = false;
 				});
 			});
 		},
@@ -146,7 +146,7 @@ export default defineComponent({
 				token: this.token
 			}).then(() => {
 				os.success();
-				this.$store.state.i.twoFactorEnabled = true;
+				this.$i.twoFactorEnabled = true;
 			}).catch(e => {
 				os.dialog({
 					type: 'error',
@@ -213,9 +213,9 @@ export default defineComponent({
 								name: 'Misskey'
 							},
 							user: {
-								id: byteify(this.$store.state.i.id, 'ascii'),
-								name: this.$store.state.i.username,
-								displayName: this.$store.state.i.name,
+								id: byteify(this.$i.id, 'ascii'),
+								name: this.$i.username,
+								displayName: this.$i.name,
 							},
 							pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
 							timeout: 60000,
diff --git a/src/client/pages/settings/account-info.vue b/src/client/pages/settings/account-info.vue
index c881b91535..2907b4b27e 100644
--- a/src/client/pages/settings/account-info.vue
+++ b/src/client/pages/settings/account-info.vue
@@ -2,13 +2,13 @@
 <FormBase>
 	<FormKeyValueView>
 		<template #key>ID</template>
-		<template #value><span class="_monospace">{{ $store.state.i.id }}</span></template>
+		<template #value><span class="_monospace">{{ $i.id }}</span></template>
 	</FormKeyValueView>
 
 	<FormGroup>
 		<FormKeyValueView>
 			<template #key>{{ $t('registeredDate') }}</template>
-			<template #value><MkTime :time="$store.state.i.createdAt" mode="detail"/></template>
+			<template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
 		</FormKeyValueView>
 	</FormGroup>
 
@@ -104,27 +104,27 @@
 		<template #label>{{ $t('other') }}</template>
 		<FormKeyValueView>
 			<template #key>emailVerified</template>
-			<template #value>{{ $store.state.i.emailVerified ? $t('yes') : $t('no') }}</template>
+			<template #value>{{ $i.emailVerified ? $t('yes') : $t('no') }}</template>
 		</FormKeyValueView>
 		<FormKeyValueView>
 			<template #key>twoFactorEnabled</template>
-			<template #value>{{ $store.state.i.twoFactorEnabled ? $t('yes') : $t('no') }}</template>
+			<template #value>{{ $i.twoFactorEnabled ? $t('yes') : $t('no') }}</template>
 		</FormKeyValueView>
 		<FormKeyValueView>
 			<template #key>securityKeys</template>
-			<template #value>{{ $store.state.i.securityKeys ? $t('yes') : $t('no') }}</template>
+			<template #value>{{ $i.securityKeys ? $t('yes') : $t('no') }}</template>
 		</FormKeyValueView>
 		<FormKeyValueView>
 			<template #key>usePasswordLessLogin</template>
-			<template #value>{{ $store.state.i.usePasswordLessLogin ? $t('yes') : $t('no') }}</template>
+			<template #value>{{ $i.usePasswordLessLogin ? $t('yes') : $t('no') }}</template>
 		</FormKeyValueView>
 		<FormKeyValueView>
 			<template #key>isModerator</template>
-			<template #value>{{ $store.state.i.isModerator ? $t('yes') : $t('no') }}</template>
+			<template #value>{{ $i.isModerator ? $t('yes') : $t('no') }}</template>
 		</FormKeyValueView>
 		<FormKeyValueView>
 			<template #key>isAdmin</template>
-			<template #value>{{ $store.state.i.isAdmin ? $t('yes') : $t('no') }}</template>
+			<template #value>{{ $i.isAdmin ? $t('yes') : $t('no') }}</template>
 		</FormKeyValueView>
 	</FormGroup>
 </FormBase>
@@ -171,7 +171,7 @@ export default defineComponent({
 		this.$emit('info', this.INFO);
 
 		os.api('users/stats', {
-			userId: this.$store.state.i.id
+			userId: this.$i.id
 		}).then(stats => {
 			this.stats = stats;
 		});
diff --git a/src/client/pages/settings/deck.vue b/src/client/pages/settings/deck.vue
index 2eb2f60cba..c5833f146d 100644
--- a/src/client/pages/settings/deck.vue
+++ b/src/client/pages/settings/deck.vue
@@ -5,17 +5,17 @@
 		<div class="_title"><Fa :icon="faColumns"/> </div>
 		<div class="_content">
 			<div>{{ $t('defaultNavigationBehaviour') }}</div>
-			<MkSwitch v-model:value="deckNavWindow">{{ $t('openInWindow') }}</MkSwitch>
+			<MkSwitch v-model:value="navWindow">{{ $t('openInWindow') }}</MkSwitch>
 		</div>
 		<div class="_content">
-			<MkSwitch v-model:value="deckAlwaysShowMainColumn">
+			<MkSwitch v-model:value="alwaysShowMainColumn">
 				{{ $t('_deck.alwaysShowMainColumn') }}
 			</MkSwitch>
 		</div>
 		<div class="_content">
 			<div>{{ $t('_deck.columnAlign') }}</div>
-			<MkRadio v-model="deckColumnAlign" value="left">{{ $t('left') }}</MkRadio>
-			<MkRadio v-model="deckColumnAlign" value="center">{{ $t('center') }}</MkRadio>
+			<MkRadio v-model="columnAlign" value="left">{{ $t('left') }}</MkRadio>
+			<MkRadio v-model="columnAlign" value="center">{{ $t('center') }}</MkRadio>
 		</div>
 	</section>
 
@@ -38,6 +38,7 @@ import FormBase from '@/components/form/base.vue';
 import FormGroup from '@/components/form/group.vue';
 import { clientDb, set } from '@/db';
 import * as os from '@/os';
+import { deckStore } from '@/ui/deck/deck-store';
 
 export default defineComponent({
 	components: {
@@ -67,20 +68,9 @@ export default defineComponent({
 	},
 
 	computed: {
-		deckNavWindow: {
-			get() { return this.$store.state.device.deckNavWindow; },
-			set(value) { this.$store.commit('device/set', { key: 'deckNavWindow', value }); }
-		},
-
-		deckAlwaysShowMainColumn: {
-			get() { return this.$store.state.device.deckAlwaysShowMainColumn; },
-			set(value) { this.$store.commit('device/set', { key: 'deckAlwaysShowMainColumn', value }); }
-		},
-
-		deckColumnAlign: {
-			get() { return this.$store.state.device.deckColumnAlign; },
-			set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); }
-		},
+		navWindow: deckStore.makeGetterSetter('navWindow'),
+		alwaysShowMainColumn: deckStore.makeGetterSetter('alwaysShowMainColumn'),
+		columnAlign: deckStore.makeGetterSetter('columnAlign'),
 	},
 
 	mounted() {
diff --git a/src/client/pages/settings/drive.vue b/src/client/pages/settings/drive.vue
index a7d623be37..1b7cf600c7 100644
--- a/src/client/pages/settings/drive.vue
+++ b/src/client/pages/settings/drive.vue
@@ -28,9 +28,9 @@ export default defineComponent({
 	},
 
 	async created() {
-		if (this.$store.state.settings.uploadFolder) {
+		if (this.$store.state.uploadFolder) {
 			this.uploadFolder = await os.api('drive/folders/show', {
-				folderId: this.$store.state.settings.uploadFolder
+				folderId: this.$store.state.uploadFolder
 			});
 		}
 	},
@@ -38,11 +38,11 @@ export default defineComponent({
 	methods: {
 		chooseUploadFolder() {
 			os.selectDriveFolder(false).then(async folder => {
-				await this.$store.dispatch('settings/set', { key: 'uploadFolder', value: folder ? folder.id : null });
+				this.$store.set('uploadFolder', folder ? folder.id : null);
 				os.success();
-				if (this.$store.state.settings.uploadFolder) {
+				if (this.$store.state.uploadFolder) {
 					this.uploadFolder = await os.api('drive/folders/show', {
-						folderId: this.$store.state.settings.uploadFolder
+						folderId: this.$store.state.uploadFolder
 					});
 				} else {
 					this.uploadFolder = null;
diff --git a/src/client/pages/settings/email-address.vue b/src/client/pages/settings/email-address.vue
index 7ff89d7910..e6e23144d6 100644
--- a/src/client/pages/settings/email-address.vue
+++ b/src/client/pages/settings/email-address.vue
@@ -3,8 +3,8 @@
 	<FormGroup>
 		<FormInput v-model:value="emailAddress" type="email">
 			{{ $t('emailAddress') }}
-			<template #desc v-if="$store.state.i.email && !$store.state.i.emailVerified">{{ $t('verificationEmailSent') }}</template>
-			<template #desc v-else-if="emailAddress === $store.state.i.email && $store.state.i.emailVerified">{{ $t('emailVerified') }}</template>
+			<template #desc v-if="$i.email && !$i.emailVerified">{{ $t('verificationEmailSent') }}</template>
+			<template #desc v-else-if="emailAddress === $i.email && $i.emailVerified">{{ $t('emailVerified') }}</template>
 		</FormInput>
 	</FormGroup>
 	<FormButton @click="save" primary>{{ $t('save') }}</FormButton>
@@ -44,7 +44,7 @@ export default defineComponent({
 	},
 
 	created() {
-		this.emailAddress = this.$store.state.i.email;
+		this.emailAddress = this.$i.email;
 	},
 
 	mounted() {
diff --git a/src/client/pages/settings/email.vue b/src/client/pages/settings/email.vue
index f72ee29a97..49a0996737 100644
--- a/src/client/pages/settings/email.vue
+++ b/src/client/pages/settings/email.vue
@@ -3,9 +3,9 @@
 	<FormGroup>
 		<template #label>{{ $t('emailAddress') }}</template>
 		<FormLink to="/settings/email/address">
-			<template v-if="$store.state.i.email && !$store.state.i.emailVerified" #icon><Fa :icon="faExclamationTriangle" style="color: var(--warn);"/></template>
-			<template v-else-if="$store.state.i.email && $store.state.i.emailVerified" #icon><Fa :icon="faCheck" style="color: var(--success);"/></template>
-			{{ $store.state.i.email || $t('notSet') }}
+			<template v-if="$i.email && !$i.emailVerified" #icon><Fa :icon="faExclamationTriangle" style="color: var(--warn);"/></template>
+			<template v-else-if="$i.email && $i.emailVerified" #icon><Fa :icon="faCheck" style="color: var(--success);"/></template>
+			{{ $i.email || $t('notSet') }}
 		</FormLink>
 	</FormGroup>
 </FormBase>
diff --git a/src/client/pages/settings/general.vue b/src/client/pages/settings/general.vue
index c8d1022ad1..0151a29b5a 100644
--- a/src/client/pages/settings/general.vue
+++ b/src/client/pages/settings/general.vue
@@ -38,7 +38,7 @@
 		<FormSwitch v-model:value="disableShowingAnimatedImages">{{ $t('disableShowingAnimatedImages') }}</FormSwitch>
 		<FormSwitch v-model:value="useSystemFont">{{ $t('useSystemFont') }}</FormSwitch>
 		<FormSwitch v-model:value="useOsNativeEmojis">{{ $t('useOsNativeEmojis') }}
-			<div><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
+			<div><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪" :key="useOsNativeEmojis"/></div>
 		</FormSwitch>
 	</FormGroup>
 
@@ -96,6 +96,8 @@ import MkLink from '@/components/link.vue';
 import { langs } from '@/config';
 import { clientDb, set } from '@/db';
 import * as os from '@/os';
+import { defaultStore } from '@/store';
+import { ColdDeviceStorage } from '@/store';
 
 export default defineComponent({
 	components: {
@@ -126,85 +128,22 @@ export default defineComponent({
 	},
 
 	computed: {
-		serverDisconnectedBehavior: {
-			get() { return this.$store.state.device.serverDisconnectedBehavior; },
-			set(value) { this.$store.commit('device/set', { key: 'serverDisconnectedBehavior', value }); }
-		},
-
-		reduceAnimation: {
-			get() { return !this.$store.state.device.animation; },
-			set(value) { this.$store.commit('device/set', { key: 'animation', value: !value }); }
-		},
-
-		useBlurEffectForModal: {
-			get() { return this.$store.state.device.useBlurEffectForModal; },
-			set(value) { this.$store.commit('device/set', { key: 'useBlurEffectForModal', value: value }); }
-		},
-
-		showGapBetweenNotesInTimeline: {
-			get() { return this.$store.state.device.showGapBetweenNotesInTimeline; },
-			set(value) { this.$store.commit('device/set', { key: 'showGapBetweenNotesInTimeline', value: value }); }
-		},
-
-		disableAnimatedMfm: {
-			get() { return !this.$store.state.device.animatedMfm; },
-			set(value) { this.$store.commit('device/set', { key: 'animatedMfm', value: !value }); }
-		},
-
-		useOsNativeEmojis: {
-			get() { return this.$store.state.device.useOsNativeEmojis; },
-			set(value) { this.$store.commit('device/set', { key: 'useOsNativeEmojis', value }); }
-		},
-
-		imageNewTab: {
-			get() { return this.$store.state.device.imageNewTab; },
-			set(value) { this.$store.commit('device/set', { key: 'imageNewTab', value }); }
-		},
-
-		disablePagesScript: {
-			get() { return this.$store.state.device.disablePagesScript; },
-			set(value) { this.$store.commit('device/set', { key: 'disablePagesScript', value }); }
-		},
-
-		showFixedPostForm: {
-			get() { return this.$store.state.device.showFixedPostForm; },
-			set(value) { this.$store.commit('device/set', { key: 'showFixedPostForm', value }); }
-		},
-
-		defaultSideView: {
-			get() { return this.$store.state.device.defaultSideView; },
-			set(value) { this.$store.commit('device/set', { key: 'defaultSideView', value }); }
-		},
-
-		chatOpenBehavior: {
-			get() { return this.$store.state.device.chatOpenBehavior; },
-			set(value) { this.$store.commit('device/set', { key: 'chatOpenBehavior', value }); }
-		},
-
-		instanceTicker: {
-			get() { return this.$store.state.device.instanceTicker; },
-			set(value) { this.$store.commit('device/set', { key: 'instanceTicker', value }); }
-		},
-
-		loadRawImages: {
-			get() { return this.$store.state.device.loadRawImages; },
-			set(value) { this.$store.commit('device/set', { key: 'loadRawImages', value }); }
-		},
-
-		disableShowingAnimatedImages: {
-			get() { return this.$store.state.device.disableShowingAnimatedImages; },
-			set(value) { this.$store.commit('device/set', { key: 'disableShowingAnimatedImages', value }); }
-		},
-
-		nsfw: {
-			get() { return this.$store.state.device.nsfw; },
-			set(value) { this.$store.commit('device/set', { key: 'nsfw', value }); }
-		},
-
-		enableInfiniteScroll: {
-			get() { return this.$store.state.device.enableInfiniteScroll; },
-			set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); }
-		},
+		serverDisconnectedBehavior: defaultStore.makeGetterSetter('serverDisconnectedBehavior'),
+		reduceAnimation: defaultStore.makeGetterSetter('animation', v => !v, v => !v),
+		useBlurEffectForModal: defaultStore.makeGetterSetter('useBlurEffectForModal'),
+		showGapBetweenNotesInTimeline: defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'),
+		disableAnimatedMfm: defaultStore.makeGetterSetter('animatedMfm', v => !v, v => !v),
+		useOsNativeEmojis: defaultStore.makeGetterSetter('useOsNativeEmojis'),
+		disableShowingAnimatedImages: defaultStore.makeGetterSetter('disableShowingAnimatedImages'),
+		loadRawImages: defaultStore.makeGetterSetter('loadRawImages'),
+		imageNewTab: defaultStore.makeGetterSetter('imageNewTab'),
+		nsfw: defaultStore.makeGetterSetter('nsfw'),
+		disablePagesScript: defaultStore.makeGetterSetter('disablePagesScript'),
+		showFixedPostForm: defaultStore.makeGetterSetter('showFixedPostForm'),
+		defaultSideView: defaultStore.makeGetterSetter('defaultSideView'),
+		chatOpenBehavior: ColdDeviceStorage.makeGetterSetter('chatOpenBehavior'),
+		instanceTicker: defaultStore.makeGetterSetter('instanceTicker'),
+		enableInfiniteScroll: defaultStore.makeGetterSetter('enableInfiniteScroll'),
 	},
 
 	watch: {
diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue
index 4bdc71aada..209d70aae3 100644
--- a/src/client/pages/settings/index.vue
+++ b/src/client/pages/settings/index.vue
@@ -41,13 +41,13 @@
 import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, ref, watch } from 'vue';
 import { faCog, faPalette, faPlug, faUser, faListUl, faLock, faCommentSlash, faMusic, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes } from '@fortawesome/free-solid-svg-icons';
 import { faLaugh, faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons';
-import { store } from '@/store';
 import { i18n } from '@/i18n';
 import FormLink from '@/components/form/link.vue';
 import FormGroup from '@/components/form/group.vue';
 import FormBase from '@/components/form/base.vue';
 import FormButton from '@/components/form/button.vue';
-import { scroll } from '../../scripts/scroll';
+import { scroll } from '@/scripts/scroll';
+import { signout } from '@/account';
 
 export default defineComponent({
 	components: {
@@ -101,7 +101,6 @@ export default defineComponent({
 				case 'plugins': return defineAsyncComponent(() => import('./plugins.vue'));
 				case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
 				case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
-				case 'regedit': return defineAsyncComponent(() => import('./regedit.vue'));
 				case 'experimental-features': return defineAsyncComponent(() => import('./experimental-features.vue'));
 				default: return null;
 			}
@@ -125,8 +124,7 @@ export default defineComponent({
 			onInfo,
 			component,
 			logout: () => {
-				store.dispatch('logout');
-				location.href = '/';
+				signout();
 			},
 			faPalette, faPlug, faUser, faListUl, faLock, faLaugh, faCommentSlash, faMusic, faBell, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes, faEnvelope,
 		};
diff --git a/src/client/pages/settings/integration.vue b/src/client/pages/settings/integration.vue
index e980e18dbc..c10a901516 100644
--- a/src/client/pages/settings/integration.vue
+++ b/src/client/pages/settings/integration.vue
@@ -57,11 +57,11 @@ export default defineComponent({
 
 	computed: {
 		integrations() {
-			return this.$store.state.i.integrations;
+			return this.$i.integrations;
 		},
 		
 		meta() {
-			return this.$store.state.instance.meta;
+			return this.$instance;
 		},
 	},
 
@@ -74,7 +74,7 @@ export default defineComponent({
 	mounted() {
 		this.$emit('info', this.INFO);
 
-		document.cookie = `igi=${this.$store.state.i.token}; path=/;` +
+		document.cookie = `igi=${this.$i.token}; path=/;` +
 			` max-age=31536000;` +
 			(document.location.protocol.startsWith('https') ? ' secure' : '');
 
diff --git a/src/client/pages/settings/notifications.vue b/src/client/pages/settings/notifications.vue
index d26a11ef78..db89e7cfe7 100644
--- a/src/client/pages/settings/notifications.vue
+++ b/src/client/pages/settings/notifications.vue
@@ -58,7 +58,7 @@ export default defineComponent({
 		},
 
 		configure() {
-			const includingTypes = notificationTypes.filter(x => !this.$store.state.i.mutingNotificationTypes.includes(x));
+			const includingTypes = notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x));
 			os.popup(import('@/components/notification-setting-window.vue'), {
 				includingTypes,
 				showGlobalToggle: false,
@@ -68,7 +68,7 @@ export default defineComponent({
 					await os.apiWithDialog('i/update', {
 						mutingNotificationTypes: notificationTypes.filter(x => !value.includes(x)),
 					}).then(i => {
-						this.$store.state.i.mutingNotificationTypes = i.mutingNotificationTypes;
+						this.$i.mutingNotificationTypes = i.mutingNotificationTypes;
 					});
 				}
 			}, 'closed');
diff --git a/src/client/pages/settings/other.vue b/src/client/pages/settings/other.vue
index 4570c060ce..599a1d8539 100644
--- a/src/client/pages/settings/other.vue
+++ b/src/client/pages/settings/other.vue
@@ -1,6 +1,6 @@
 <template>
 <FormBase>
-	<FormSwitch :value="$store.state.i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote">
+	<FormSwitch :value="$i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote">
 		{{ $t('showFeaturedNotesInTimeline') }}
 	</FormSwitch>
 
diff --git a/src/client/pages/settings/plugins.vue b/src/client/pages/settings/plugins.vue
index 6cedf02548..1fda97fc57 100644
--- a/src/client/pages/settings/plugins.vue
+++ b/src/client/pages/settings/plugins.vue
@@ -15,7 +15,7 @@
 		<details>
 			<summary><Fa :icon="faFolderOpen"/> {{ $t('manage') }}</summary>
 			<MkSelect v-model:value="selectedPluginId">
-				<option v-for="x in $store.state.deviceUser.plugins" :value="x.id" :key="x.id">{{ x.name }}</option>
+				<option v-for="x in plugins" :value="x.id" :key="x.id">{{ x.name }}</option>
 			</MkSelect>
 			<template v-if="selectedPlugin">
 				<div style="margin: -8px 0 8px 0;">
@@ -55,6 +55,7 @@ import MkSelect from '@/components/ui/select.vue';
 import MkInfo from '@/components/ui/info.vue';
 import MkSwitch from '@/components/ui/switch.vue';
 import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
 
 export default defineComponent({
 	components: {
@@ -68,6 +69,7 @@ export default defineComponent({
 	data() {
 		return {
 			script: '',
+			plugins: ColdDeviceStorage.get('plugins'),
 			selectedPluginId: null,
 			faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog
 		}
@@ -76,11 +78,22 @@ export default defineComponent({
 	computed: {
 		selectedPlugin() {
 			if (this.selectedPluginId == null) return null;
-			return this.$store.state.deviceUser.plugins.find(x => x.id === this.selectedPluginId);
+			return this.plugins.find(x => x.id === this.selectedPluginId);
 		},
 	},
 
 	methods: {
+		installPlugin({ id, meta, ast, token }) {
+			ColdDeviceStorage.set('plugins', this.plugins.concat({
+				...meta,
+				id,
+				active: true,
+				configData: {},
+				token: token,
+				ast: ast
+			}));
+		},
+
 		async install() {
 			let ast;
 			try {
@@ -137,7 +150,7 @@ export default defineComponent({
 				}, 'closed');
 			});
 
-			this.$store.commit('deviceUser/installPlugin', {
+			this.installPlugin({
 				id: uuid(),
 				meta: {
 					name, version, author, description, permissions, config
@@ -154,7 +167,7 @@ export default defineComponent({
 		},
 
 		uninstall() {
-			this.$store.commit('deviceUser/uninstallPlugin', this.selectedPluginId);
+			ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== this.selectedPluginId));
 			os.success();
 			this.$nextTick(() => {
 				location.reload();
@@ -171,10 +184,9 @@ export default defineComponent({
 			const { canceled, result } = await os.form(this.selectedPlugin.name, config);
 			if (canceled) return;
 
-			this.$store.commit('deviceUser/configPlugin', {
-				id: this.selectedPluginId,
-				config: result
-			});
+			const plugins = ColdDeviceStorage.get('plugins');
+			plugins.find(p => p.id === this.selectedPluginId).configData = result;
+			ColdDeviceStorage.set('plugins', plugins);
 
 			this.$nextTick(() => {
 				location.reload();
@@ -182,10 +194,9 @@ export default defineComponent({
 		},
 
 		changeActive(plugin, active) {
-			this.$store.commit('deviceUser/changePluginActive', {
-				id: plugin.id,
-				active: active
-			});
+			const plugins = ColdDeviceStorage.get('plugins');
+			plugins.find(p => p.id === plugin.id).active = active;
+			ColdDeviceStorage.set('plugins', plugins);
 
 			this.$nextTick(() => {
 				location.reload();
diff --git a/src/client/pages/settings/privacy.vue b/src/client/pages/settings/privacy.vue
index d5242a5f51..6ac87ee643 100644
--- a/src/client/pages/settings/privacy.vue
+++ b/src/client/pages/settings/privacy.vue
@@ -35,6 +35,7 @@ import FormSelect from '@/components/form/select.vue';
 import FormBase from '@/components/form/base.vue';
 import FormGroup from '@/components/form/group.vue';
 import * as os from '@/os';
+import { defaultStore } from '@/store';
 
 export default defineComponent({
 	components: {
@@ -60,27 +61,16 @@ export default defineComponent({
 	},
 
 	computed: {
-		defaultNoteVisibility: {
-			get() { return this.$store.state.settings.defaultNoteVisibility; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
-		},
-
-		defaultNoteLocalOnly: {
-			get() { return this.$store.state.settings.defaultNoteLocalOnly; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteLocalOnly', value }); }
-		},
-
-		rememberNoteVisibility: {
-			get() { return this.$store.state.settings.rememberNoteVisibility; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
-		},
+		defaultNoteVisibility: defaultStore.makeGetterSetter('defaultNoteVisibility'),
+		defaultNoteLocalOnly: defaultStore.makeGetterSetter('defaultNoteLocalOnly'),
+		rememberNoteVisibility: defaultStore.makeGetterSetter('rememberNoteVisibility'),
 	},
 
 	created() {
-		this.isLocked = this.$store.state.i.isLocked;
-		this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed;
-		this.noCrawle = this.$store.state.i.noCrawle;
-		this.isExplorable = this.$store.state.i.isExplorable;
+		this.isLocked = this.$i.isLocked;
+		this.autoAcceptFollowed = this.$i.autoAcceptFollowed;
+		this.noCrawle = this.$i.noCrawle;
+		this.isExplorable = this.$i.isExplorable;
 	},
 
 	mounted() {
diff --git a/src/client/pages/settings/profile.vue b/src/client/pages/settings/profile.vue
index b981cdcc1d..a471af9c0a 100644
--- a/src/client/pages/settings/profile.vue
+++ b/src/client/pages/settings/profile.vue
@@ -1,8 +1,8 @@
 <template>
 <FormBase>
 	<FormGroup>
-		<div class="_formItem _formPanel llvierxe" :style="{ backgroundImage: $store.state.i.bannerUrl ? `url(${ $store.state.i.bannerUrl })` : null }">
-			<MkAvatar class="avatar" :user="$store.state.i"/>
+		<div class="_formItem _formPanel llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
+			<MkAvatar class="avatar" :user="$i"/>
 		</div>
 		<FormButton @click="changeAvatar" primary>{{ $t('_profile.changeAvatar') }}</FormButton>
 		<FormButton @click="changeBanner" primary>{{ $t('_profile.changeBanner') }}</FormButton>
@@ -100,24 +100,24 @@ export default defineComponent({
 	},
 
 	created() {
-		this.name = this.$store.state.i.name;
-		this.description = this.$store.state.i.description;
-		this.location = this.$store.state.i.location;
-		this.birthday = this.$store.state.i.birthday;
-		this.avatarId = this.$store.state.i.avatarId;
-		this.bannerId = this.$store.state.i.bannerId;
-		this.isBot = this.$store.state.i.isBot;
-		this.isCat = this.$store.state.i.isCat;
-		this.alwaysMarkNsfw = this.$store.state.i.alwaysMarkNsfw;
+		this.name = this.$i.name;
+		this.description = this.$i.description;
+		this.location = this.$i.location;
+		this.birthday = this.$i.birthday;
+		this.avatarId = this.$i.avatarId;
+		this.bannerId = this.$i.bannerId;
+		this.isBot = this.$i.isBot;
+		this.isCat = this.$i.isCat;
+		this.alwaysMarkNsfw = this.$i.alwaysMarkNsfw;
 
-		this.fieldName0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].name : null;
-		this.fieldValue0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].value : null;
-		this.fieldName1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].name : null;
-		this.fieldValue1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].value : null;
-		this.fieldName2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].name : null;
-		this.fieldValue2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].value : null;
-		this.fieldName3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].name : null;
-		this.fieldValue3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].value : null;
+		this.fieldName0 = this.$i.fields[0] ? this.$i.fields[0].name : null;
+		this.fieldValue0 = this.$i.fields[0] ? this.$i.fields[0].value : null;
+		this.fieldName1 = this.$i.fields[1] ? this.$i.fields[1].name : null;
+		this.fieldValue1 = this.$i.fields[1] ? this.$i.fields[1].value : null;
+		this.fieldName2 = this.$i.fields[2] ? this.$i.fields[2].name : null;
+		this.fieldValue2 = this.$i.fields[2] ? this.$i.fields[2].value : null;
+		this.fieldName3 = this.$i.fields[3] ? this.$i.fields[3].name : null;
+		this.fieldValue3 = this.$i.fields[3] ? this.$i.fields[3].value : null;
 	},
 
 	mounted() {
@@ -227,10 +227,10 @@ export default defineComponent({
 				alwaysMarkNsfw: !!this.alwaysMarkNsfw,
 			}).then(i => {
 				this.saving = false;
-				this.$store.state.i.avatarId = i.avatarId;
-				this.$store.state.i.avatarUrl = i.avatarUrl;
-				this.$store.state.i.bannerId = i.bannerId;
-				this.$store.state.i.bannerUrl = i.bannerUrl;
+				this.$i.avatarId = i.avatarId;
+				this.$i.avatarUrl = i.avatarUrl;
+				this.$i.bannerId = i.bannerId;
+				this.$i.bannerUrl = i.bannerUrl;
 
 				if (notify) {
 					os.success();
diff --git a/src/client/pages/settings/reaction.vue b/src/client/pages/settings/reaction.vue
index ec1d4e5cf2..6f1a5c9541 100644
--- a/src/client/pages/settings/reaction.vue
+++ b/src/client/pages/settings/reaction.vue
@@ -43,8 +43,8 @@ import FormInput from '@/components/form/input.vue';
 import FormRadios from '@/components/form/radios.vue';
 import FormBase from '@/components/form/base.vue';
 import FormButton from '@/components/form/button.vue';
-import { defaultSettings } from '@/store';
 import * as os from '@/os';
+import { defaultStore } from '@/store';
 
 export default defineComponent({
 	components: {
@@ -67,24 +67,14 @@ export default defineComponent({
 					handler: this.preview
 				}
 			},
-			reactions: JSON.parse(JSON.stringify(this.$store.state.settings.reactions)),
+			reactions: JSON.parse(JSON.stringify(this.$store.state.reactions)),
 			faLaugh, faSave, faEye, faUndo, faPlus
 		}
 	},
 
 	computed: {
-		useFullReactionPicker: {
-			get() { return this.$store.state.device.useFullReactionPicker; },
-			set(value) { this.$store.commit('device/set', { key: 'useFullReactionPicker', value: value }); }
-		},
-		reactionPickerWidth: {
-			get() { return this.$store.state.device.reactionPickerWidth; },
-			set(value) { this.$store.commit('device/set', { key: 'reactionPickerWidth', value: value }); }
-		},
-		reactionPickerHeight: {
-			get() { return this.$store.state.device.reactionPickerHeight; },
-			set(value) { this.$store.commit('device/set', { key: 'reactionPickerHeight', value: value }); }
-		},
+		reactionPickerWidth: defaultStore.makeGetterSetter('reactionPickerWidth'),
+		reactionPickerHeight: defaultStore.makeGetterSetter('reactionPickerHeight'),
 	},
 
 	watch: {
@@ -102,7 +92,7 @@ export default defineComponent({
 
 	methods: {
 		save() {
-			this.$store.dispatch('settings/set', { key: 'reactions', value: this.reactions });
+			this.$store.set('reactions', this.reactions);
 		},
 
 		remove(reaction, ev) {
@@ -129,7 +119,7 @@ export default defineComponent({
 			});
 			if (canceled) return;
 
-			this.reactions = JSON.parse(JSON.stringify(defaultSettings.reactions));
+			this.reactions = JSON.parse(JSON.stringify(this.$store.def.reactions.default));
 		},
 
 		chooseEmoji(ev) {
diff --git a/src/client/pages/settings/regedit.vue b/src/client/pages/settings/regedit.vue
deleted file mode 100644
index 3db60c5905..0000000000
--- a/src/client/pages/settings/regedit.vue
+++ /dev/null
@@ -1,75 +0,0 @@
-<template>
-<div>
-	<div class="_section">
-		<MkInfo warn>{{ $t('editTheseSettingsMayBreakAccount') }}</MkInfo>
-	</div>
-	<div class="_section">
-		<div class="_title">Account</div>
-		<div class="_content">
-			<MkTextarea v-model:value="settings" code tall></MkTextarea>
-			<!--<MkButton @click="saveSettings">Save</MkButton>-->
-		</div>
-	</div>
-	<div class="_section">
-		<div class="_title">Device</div>
-		<div class="_content">
-			<MkTextarea v-model:value="deviceSettings" code tall></MkTextarea>
-			<MkButton @click="saveDeviceSettings">Save</MkButton>
-		</div>
-	</div>
-	<div class="_section">
-		<div class="_title">Device (per account)</div>
-		<div class="_content">
-			<MkTextarea v-model:value="deviceUserSettings" code tall></MkTextarea>
-			<MkButton @click="saveDeviceUserSettings">Save</MkButton>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { faCode } from '@fortawesome/free-solid-svg-icons';
-import * as JSON5 from 'json5';
-import MkInfo from '@/components/ui/info.vue';
-import MkButton from '@/components/ui/button.vue';
-import MkTextarea from '@/components/ui/textarea.vue';
-import * as os from '@/os';
-
-export default defineComponent({
-	components: {
-		MkInfo, MkButton, MkTextarea
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			INFO: {
-				title: 'RegEdit',
-				icon: faCode
-			},
-
-			settings: JSON5.stringify(this.$store.state.settings, null, '\t'),
-			deviceSettings: JSON5.stringify(this.$store.state.device, null, '\t'),
-			deviceUserSettings: JSON5.stringify(this.$store.state.deviceUser, null, '\t'),
-		};
-	},
-
-	mounted() {
-		this.$emit('info', this.INFO);
-	},
-
-	methods: {
-		saveDeviceSettings() {
-			const obj = JSON5.parse(this.deviceSettings);
-			this.$store.commit('device/overwrite', obj);
-		},
-
-		saveDeviceUserSettings() {
-			const obj = JSON5.parse(this.deviceUserSettings);
-			this.$store.commit('deviceUser/overwrite', obj);
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/settings/sidebar.vue b/src/client/pages/settings/sidebar.vue
index 4138aaf733..d835f57b55 100644
--- a/src/client/pages/settings/sidebar.vue
+++ b/src/client/pages/settings/sidebar.vue
@@ -26,9 +26,9 @@ import FormRadios from '@/components/form/radios.vue';
 import FormBase from '@/components/form/base.vue';
 import FormGroup from '@/components/form/group.vue';
 import FormButton from '@/components/form/button.vue';
-import { defaultDeviceUserSettings } from '@/store';
 import * as os from '@/os';
 import { sidebarDef } from '@/sidebar';
+import { defaultStore } from '@/store';
 
 export default defineComponent({
 	components: {
@@ -57,14 +57,11 @@ export default defineComponent({
 			return this.items.trim().split('\n').filter(x => x.trim() !== '');
 		},
 
-		sidebarDisplay: {
-			get() { return this.$store.state.device.sidebarDisplay; },
-			set(value) { this.$store.commit('device/set', { key: 'sidebarDisplay', value }); }
-		},
+		sidebarDisplay: defaultStore.makeGetterSetter('sidebarDisplay')
 	},
 
 	created() {
-		this.items = this.$store.state.deviceUser.menu.join('\n');
+		this.items = this.$store.state.menu.join('\n');
 	},
 
 	mounted() {
@@ -73,7 +70,7 @@ export default defineComponent({
 
 	methods: {
 		async addItem() {
-			const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.deviceUser.menu.includes(k));
+			const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.menu.includes(k));
 			const { canceled, result: item } = await os.dialog({
 				type: null,
 				title: this.$t('addItem'),
@@ -92,12 +89,12 @@ export default defineComponent({
 		},
 
 		save() {
-			this.$store.commit('deviceUser/setMenu', this.splited);
+			this.$store.set('menu', this.splited);
 		},
 
 		reset() {
-			this.$store.commit('deviceUser/setMenu', defaultDeviceUserSettings.menu);
-			this.items = this.$store.state.deviceUser.menu.join('\n');
+			this.$store.reset('menu');
+			this.items = this.$store.state.menu.join('\n');
 		},
 	},
 });
diff --git a/src/client/pages/settings/sounds.vue b/src/client/pages/settings/sounds.vue
index f19be54e82..d254dc4cfb 100644
--- a/src/client/pages/settings/sounds.vue
+++ b/src/client/pages/settings/sounds.vue
@@ -26,7 +26,7 @@ import FormBase from '@/components/form/base.vue';
 import FormButton from '@/components/form/button.vue';
 import FormGroup from '@/components/form/group.vue';
 import * as os from '@/os';
-import { device, defaultDeviceSettings } from '@/cold-storage';
+import { ColdDeviceStorage } from '@/store';
 import { playFile } from '@/scripts/sound';
 
 const soundsTypes = [
@@ -79,8 +79,8 @@ export default defineComponent({
 
 	computed: {
 		masterVolume: { // TODO: (外部)関数にcomputedを使うのはアレなので直す
-			get() { return device.get('sound_masterVolume'); },
-			set(value) { device.set('sound_masterVolume', value); }
+			get() { return ColdDeviceStorage.get('sound_masterVolume'); },
+			set(value) { ColdDeviceStorage.set('sound_masterVolume', value); }
 		},
 		volumeIcon() {
 			return this.masterVolume === 0 ? faVolumeMute : faVolumeUp;
@@ -88,15 +88,15 @@ export default defineComponent({
 	},
 
 	created() {
-		this.sounds.note = device.get('sound_note');
-		this.sounds.noteMy = device.get('sound_noteMy');
-		this.sounds.notification = device.get('sound_notification');
-		this.sounds.chat = device.get('sound_chat');
-		this.sounds.chatBg = device.get('sound_chatBg');
-		this.sounds.antenna = device.get('sound_antenna');
-		this.sounds.channel = device.get('sound_channel');
-		this.sounds.reversiPutBlack = device.get('sound_reversiPutBlack');
-		this.sounds.reversiPutWhite = device.get('sound_reversiPutWhite');
+		this.sounds.note = ColdDeviceStorage.get('sound_note');
+		this.sounds.noteMy = ColdDeviceStorage.get('sound_noteMy');
+		this.sounds.notification = ColdDeviceStorage.get('sound_notification');
+		this.sounds.chat = ColdDeviceStorage.get('sound_chat');
+		this.sounds.chatBg = ColdDeviceStorage.get('sound_chatBg');
+		this.sounds.antenna = ColdDeviceStorage.get('sound_antenna');
+		this.sounds.channel = ColdDeviceStorage.get('sound_channel');
+		this.sounds.reversiPutBlack = ColdDeviceStorage.get('sound_reversiPutBlack');
+		this.sounds.reversiPutWhite = ColdDeviceStorage.get('sound_reversiPutWhite');
 	},
 
 	mounted() {
@@ -138,14 +138,14 @@ export default defineComponent({
 				volume: result.volume,
 			};
 
-			device.set('sound_' + type, v);
+			ColdDeviceStorage.set('sound_' + type, v);
 			this.sounds[type] = v;
 		},
 
 		reset() {
 			for (const sound of Object.keys(this.sounds)) {
-				const v = defaultDeviceSettings['sound_' + sound];
-				device.set('sound_' + sound, v);
+				const v = ColdDeviceStorage.default['sound_' + sound];
+				ColdDeviceStorage.set('sound_' + sound, v);
 				this.sounds[sound] = v;
 			}
 		}
diff --git a/src/client/pages/settings/theme.install.vue b/src/client/pages/settings/theme.install.vue
index c3f2565cca..4ec9764229 100644
--- a/src/client/pages/settings/theme.install.vue
+++ b/src/client/pages/settings/theme.install.vue
@@ -24,6 +24,7 @@ import FormLink from '@/components/form/link.vue';
 import FormButton from '@/components/form/button.vue';
 import { applyTheme, validateTheme } from '@/scripts/theme';
 import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
 
 export default defineComponent({
 	components: {
@@ -73,7 +74,7 @@ export default defineComponent({
 				});
 				return false;
 			}
-			if (this.$store.state.device.themes.some(t => t.id === theme.id)) {
+			if (ColdDeviceStorage.get('themes').some(t => t.id === theme.id)) {
 				os.dialog({
 					type: 'info',
 					text: this.$t('_theme.alreadyInstalled')
@@ -92,10 +93,8 @@ export default defineComponent({
 		install(code) {
 			const theme = this.parseThemeCode(code);
 			if (!theme) return;
-			const themes = this.$store.state.device.themes.concat(theme);
-			this.$store.commit('device/set', {
-				key: 'themes', value: themes
-			});
+			const themes = ColdDeviceStorage.get('themes').concat(theme);
+			ColdDeviceStorage.set('themes', themes);
 			os.dialog({
 				type: 'success',
 				text: this.$t('_theme.installed', { name: theme.name })
diff --git a/src/client/pages/settings/theme.manage.vue b/src/client/pages/settings/theme.manage.vue
index a7bd97a4d5..9fa93aac5f 100644
--- a/src/client/pages/settings/theme.manage.vue
+++ b/src/client/pages/settings/theme.manage.vue
@@ -34,6 +34,7 @@ import FormButton from '@/components/form/button.vue';
 import { Theme, builtinThemes } from '@/scripts/theme';
 import copyToClipboard from '@/scripts/copy-to-clipboard';
 import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
 
 export default defineComponent({
 	components: {
@@ -54,6 +55,7 @@ export default defineComponent({
 				title: this.$t('_theme.manage'),
 				icon: faFolderOpen
 			},
+			installedThemes: ColdDeviceStorage.ref('themes'),
 			builtinThemes,
 			selectedThemeId: null,
 			faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
@@ -62,11 +64,7 @@ export default defineComponent({
 
 	computed: {
 		themes(): Theme[] {
-			return builtinThemes.concat(this.$store.state.device.themes);
-		},
-
-		installedThemes(): Theme[] {
-			return this.$store.state.device.themes;
+			return this.builtinThemes.concat(this.installedThemes.value);
 		},
 	
 		selectedTheme() {
@@ -92,10 +90,8 @@ export default defineComponent({
 
 		uninstall() {
 			const theme = this.selectedTheme;
-			const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
-			this.$store.commit('device/set', {
-				key: 'themes', value: themes
-			});
+			const themes = ColdDeviceStorage.get('themes').filter(t => t.id != theme.id);
+			ColdDeviceStorage.set('themes', themes);
 			os.success();
 		},
 	}
diff --git a/src/client/pages/settings/theme.vue b/src/client/pages/settings/theme.vue
index 50dcf4952c..1536d0cc07 100644
--- a/src/client/pages/settings/theme.vue
+++ b/src/client/pages/settings/theme.vue
@@ -60,25 +60,25 @@
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent, onMounted, ref, watch } from 'vue';
 import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
 import FormSwitch from '@/components/form/switch.vue';
 import FormSelect from '@/components/form/select.vue';
-import FormRadios from '@/components/form/radios.vue';
 import FormBase from '@/components/form/base.vue';
 import FormGroup from '@/components/form/group.vue';
 import FormLink from '@/components/form/link.vue';
 import FormButton from '@/components/form/button.vue';
-import { Theme, builtinThemes, applyTheme } from '@/scripts/theme';
+import { builtinThemes, applyTheme } from '@/scripts/theme';
 import { selectFile } from '@/scripts/select-file';
 import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
-import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
 
 export default defineComponent({
 	components: {
 		FormSwitch,
 		FormSelect,
-		FormRadios,
 		FormBase,
 		FormGroup,
 		FormLink,
@@ -86,96 +86,70 @@ export default defineComponent({
 	},
 
 	emits: ['info'],
-	
-	data() {
-		return {
-			INFO: {
-				title: this.$t('theme'),
-				icon: faPalette
-			},
-			builtinThemes,
-			wallpaper: localStorage.getItem('wallpaper'),
-			faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
-		}
-	},
 
-	computed: {
-		themes(): Theme[] {
-			return builtinThemes.concat(this.$store.state.device.themes);
-		},
+	setup(props, { emit }) {
+		const INFO = {
+			title: i18n.global.t('theme'),
+			icon: faPalette
+		};
 
-		installedThemes(): Theme[] {
-			return this.$store.state.device.themes;
-		},
-	
-		darkThemes(): Theme[] {
-			return this.themes.filter(t => t.base == 'dark' || t.kind == 'dark');
-		},
+		const installedThemes = ColdDeviceStorage.ref('themes');
+		const themes = computed(() => builtinThemes.concat(installedThemes.value));
+		const darkThemes = computed(() => themes.value.filter(t => t.base == 'dark' || t.kind == 'dark'));
+		const lightThemes = computed(() => themes.value.filter(t => t.base == 'light' || t.kind == 'light'));
+		const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
+		const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
+		const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
+		const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
+		const wallpaper = ref(localStorage.getItem('wallpaper'));
 
-		lightThemes(): Theme[] {
-			return this.themes.filter(t => t.base == 'light' || t.kind == 'light');
-		},
-		
-		darkTheme: {
-			get() { return this.$store.state.device.darkTheme; },
-			set(value) { this.$store.commit('device/set', { key: 'darkTheme', value }); }
-		},
-
-		lightTheme: {
-			get() { return this.$store.state.device.lightTheme; },
-			set(value) { this.$store.commit('device/set', { key: 'lightTheme', value }); }
-		},
-
-		darkMode: {
-			get() { return this.$store.state.device.darkMode; },
-			set(value) { this.$store.commit('device/set', { key: 'darkMode', value }); }
-		},
-
-		syncDeviceDarkMode: {
-			get() { return this.$store.state.device.syncDeviceDarkMode; },
-			set(value) { this.$store.commit('device/set', { key: 'syncDeviceDarkMode', value }); }
-		},
-	},
-
-	watch: {
-		darkTheme() {
-			if (this.$store.state.device.darkMode) {
-				applyTheme(this.themes.find(x => x.id === this.darkTheme));
+		watch(darkTheme, () => {
+			if (defaultStore.state.darkMode) {
+				applyTheme(themes.value.find(x => x.id === darkTheme.value));
 			}
-		},
+		});
 
-		lightTheme() {
-			if (!this.$store.state.device.darkMode) {
-				applyTheme(this.themes.find(x => x.id === this.lightTheme));
+		watch(lightTheme, () => {
+			if (!defaultStore.state.darkMode) {
+				applyTheme(themes.value.find(x => x.id === lightTheme.value));
 			}
-		},
+		});
 
-		syncDeviceDarkMode() {
-			if (this.$store.state.device.syncDeviceDarkMode) {
-				this.$store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() });
+		watch(syncDeviceDarkMode, () => {
+			if (syncDeviceDarkMode) {
+				defaultStore.set('darkMode', isDeviceDarkmode());
 			}
-		},
+		});
 
-		wallpaper() {
-			if (this.wallpaper == null) {
+		watch(wallpaper, () => {
+			if (wallpaper.value == null) {
 				localStorage.removeItem('wallpaper');
 			} else {
-				localStorage.setItem('wallpaper', this.wallpaper);
+				localStorage.setItem('wallpaper', wallpaper.value);
 			}
 			location.reload();
-		}
-	},
+		});
 
-	mounted() {
-		this.$emit('info', this.INFO);
-	},
+		onMounted(() => {
+			emit('info', INFO);
+		});
 
-	methods: {
-		setWallpaper(e) {
-			selectFile(e.currentTarget || e.target, null, false).then(file => {
-				this.wallpaper = file.url;
-			});
-		},
+		return {
+			INFO,
+			darkThemes,
+			lightThemes,
+			darkTheme,
+			lightTheme,
+			darkMode,
+			syncDeviceDarkMode,
+			wallpaper,
+			setWallpaper(e) {
+				selectFile(e.currentTarget || e.target, null, false).then(file => {
+					wallpaper.value = file.url;
+				});
+			},
+			faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
+		};
 	}
 });
 </script>
diff --git a/src/client/pages/settings/word-mute.vue b/src/client/pages/settings/word-mute.vue
index 3148c635bc..f02f9bf2f3 100644
--- a/src/client/pages/settings/word-mute.vue
+++ b/src/client/pages/settings/word-mute.vue
@@ -85,8 +85,8 @@ export default defineComponent({
 	},
 
 	async created() {
-		this.softMutedWords = this.$store.state.settings.mutedWords.map(x => x.join(' ')).join('\n');
-		this.hardMutedWords = this.$store.state.i.mutedWords.map(x => x.join(' ')).join('\n');
+		this.softMutedWords = this.$store.state.mutedWords.map(x => x.join(' ')).join('\n');
+		this.hardMutedWords = this.$i.mutedWords.map(x => x.join(' ')).join('\n');
 
 		this.hardWordMutedNotesCount = (await os.api('i/get-word-muted-notes-count', {})).count;
 	},
@@ -97,7 +97,7 @@ export default defineComponent({
 
 	methods: {
 		async save() {
-			this.$store.dispatch('settings/set', { key: 'mutedWords', value: this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')) });
+			this.$store.set('mutedWords', this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')));
 			await os.api('i/update', {
 				mutedWords: this.hardMutedWords.trim().split('\n').map(x => x.trim().split(' ')),
 			});
diff --git a/src/client/pages/test.vue b/src/client/pages/test.vue
index 77aa264c31..2f890e78a6 100644
--- a/src/client/pages/test.vue
+++ b/src/client/pages/test.vue
@@ -252,7 +252,7 @@ export default defineComponent({
 		},
 
 		resetTutorial() {
-			this.$store.dispatch('settings/set', { key: 'tutorial', value: 0 });
+			this.$store.set('tutorial', 0);
 		},
 	}
 });
diff --git a/src/client/pages/theme-editor.vue b/src/client/pages/theme-editor.vue
index 4b1ec59cfe..b2ce1b080e 100644
--- a/src/client/pages/theme-editor.vue
+++ b/src/client/pages/theme-editor.vue
@@ -107,6 +107,7 @@ import { convertToMisskeyTheme, ThemeValue, convertToViewModel, ThemeViewModel }
 import { Theme, applyTheme, lightTheme, darkTheme, themeProps, validateTheme } from '@/scripts/theme';
 import { host } from '@/config';
 import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
 
 export default defineComponent({
 	components: {
@@ -128,7 +129,7 @@ export default defineComponent({
 			name: '',
 			description: '',
 			baseTheme: 'light' as 'dark' | 'light',
-			author: `@${this.$store.state.i.username}@${toUnicode(host)}`,
+			author: `@${this.$i.username}@${toUnicode(host)}`,
 			themeToImport: '',
 			changed: false,
 			lightTheme, darkTheme, themeProps,
@@ -211,10 +212,8 @@ export default defineComponent({
 	
 		save() {
 			const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
-			const themes = this.$store.state.device.themes.concat(theme);
-			this.$store.commit('device/set', {
-				key: 'themes', value: themes
-			});
+			const themes = ColdDeviceStorage.get('themes').concat(theme);
+			ColdDeviceStorage.set('themes', themes);
 			os.dialog({
 				type: 'success',
 				text: this.$t('_theme.installed', { name: theme.name })
diff --git a/src/client/pages/timeline.tutorial.vue b/src/client/pages/timeline.tutorial.vue
index 837915229e..f3347c1463 100644
--- a/src/client/pages/timeline.tutorial.vue
+++ b/src/client/pages/timeline.tutorial.vue
@@ -83,8 +83,8 @@ export default defineComponent({
 
 	computed: {
 		tutorial: {
-			get() { return this.$store.state.settings.tutorial || 0; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'tutorial', value }); }
+			get() { return this.$store.reactiveState.tutorial.value || 0; },
+			set(value) { this.$store.set('tutorial', value); }
 		},
 	},
 });
diff --git a/src/client/pages/timeline.vue b/src/client/pages/timeline.vue
index af4de09af0..e0309d46f8 100644
--- a/src/client/pages/timeline.vue
+++ b/src/client/pages/timeline.vue
@@ -3,8 +3,8 @@
 	<div class="new" v-if="queue > 0" :style="{ width: width + 'px' }"><button class="_buttonPrimary" @click="top()">{{ $t('newNoteRecived') }}</button></div>
 
 	<div class="_section">
-		<XTutorial v-if="$store.state.settings.tutorial != -1" class="tutorial _content _vMargin"/>
-		<XPostForm v-if="$store.state.device.showFixedPostForm" class="post-form _panel _content _vMargin" fixed/>
+		<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _content _vMargin"/>
+		<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _panel _content _vMargin" fixed/>
 		<XTimeline ref="tl"
 			class="_content _vMargin"
 			:key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src"
@@ -59,7 +59,7 @@ export default defineComponent({
 					selected: computed(() => this.src === 'home')
 				}];
 
-				if (!this.$store.state.instance.meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin) {
+				if (!this.$instance.disableLocalTimeline || this.$i.isModerator || this.$i.isAdmin) {
 					tabs.push({
 						id: 'local',
 						title: null,
@@ -79,7 +79,7 @@ export default defineComponent({
 					});
 				}
 
-				if (!this.$store.state.instance.meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin) {
+				if (!this.$instance.disableGlobalTimeline || this.$i.isModerator || this.$i.isAdmin) {
 					tabs.push({
 						id: 'global',
 						title: null,
@@ -95,7 +95,7 @@ export default defineComponent({
 					title: null,
 					icon: faEllipsisH,
 					onClick: this.choose,
-					indicate: computed(() => this.$store.state.i.hasUnreadAntenna || this.$store.state.i.hasUnreadChannel)
+					indicate: computed(() => this.$i.hasUnreadAntenna || this.$i.hasUnreadChannel)
 				});
 
 				return {
@@ -118,7 +118,7 @@ export default defineComponent({
 		},
 
 		meta() {
-			return this.$store.state.instance.meta;
+			return this.$instance;
 		},
 	},
 
@@ -144,13 +144,13 @@ export default defineComponent({
 	},
 
 	created() {
-		this.src = this.$store.state.deviceUser.tl.src;
+		this.src = this.$store.state.tl.src;
 		if (this.src === 'list') {
-			this.list = this.$store.state.deviceUser.tl.arg;
+			this.list = this.$store.state.tl.arg;
 		} else if (this.src === 'antenna') {
-			this.antenna = this.$store.state.deviceUser.tl.arg;
+			this.antenna = this.$store.state.tl.arg;
 		} else if (this.src === 'channel') {
-			this.channel = this.$store.state.deviceUser.tl.arg;
+			this.channel = this.$store.state.tl.arg;
 		}
 	},
 
@@ -218,7 +218,7 @@ export default defineComponent({
 		},
 
 		saveSrc() {
-			this.$store.commit('deviceUser/setTl', {
+			this.$store.set('tl', {
 				src: this.src,
 				arg:
 					this.src === 'list' ? this.list :
diff --git a/src/client/pages/user/index.photos.vue b/src/client/pages/user/index.photos.vue
index 7d498cfb30..8cb72518b2 100644
--- a/src/client/pages/user/index.photos.vue
+++ b/src/client/pages/user/index.photos.vue
@@ -51,7 +51,7 @@ export default defineComponent({
 		os.api('users/notes', {
 			userId: this.user.id,
 			fileType: image,
-			excludeNsfw: this.$store.state.device.nsfw !== 'ignore',
+			excludeNsfw: this.$store.state.nsfw !== 'ignore',
 			limit: 9,
 		}).then(notes => {
 			for (const note of notes) {
@@ -69,7 +69,7 @@ export default defineComponent({
 	},
 	methods: {
 		thumbnail(image: any): string {
-			return this.$store.state.device.disableShowingAnimatedImages
+			return this.$store.state.disableShowingAnimatedImages
 				? getStaticImageUrl(image.thumbnailUrl)
 				: image.thumbnailUrl;
 		},
diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue
index ea21f54c64..2efc9b993d 100644
--- a/src/client/pages/user/index.vue
+++ b/src/client/pages/user/index.vue
@@ -13,7 +13,7 @@
 					<MkUserName :user="user" :nowrap="false" class="name"/>
 					<MkAcct :user="user" :detail="true" class="acct"/>
 				</div>
-				<div class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed"><span>{{ $t('followsYou') }}</span></div>
+				<div class="followed" v-if="$i && $i.id != user.id && user.isFollowed"><span>{{ $t('followsYou') }}</span></div>
 				<div class="status">
 					<MkA :to="userPage(user)" :class="{ active: page === 'index' }">
 						<b>{{ number(user.notesCount) }}</b>
@@ -29,7 +29,7 @@
 					</MkA>
 				</div>
 				<div class="description">
-					<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+					<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
 					<p v-else class="empty">{{ $t('noAccountDescription') }}</p>
 				</div>
 				<div class="fields system">
@@ -52,7 +52,7 @@
 							<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
 						</dt>
 						<dd class="value">
-							<Mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/>
+							<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/>
 						</dd>
 					</dl>
 				</div>
@@ -75,7 +75,7 @@
 					</MkA>
 					<div class="actions">
 						<button @click="menu" class="menu _button"><Fa :icon="faEllipsisH"/></button>
-						<MkFollowButton v-if="!$store.getters.isSignedIn || $store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
+						<MkFollowButton v-if="!$i || $i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
 					</div>
 				</div>
 				<template v-if="page === 'index'">
@@ -115,10 +115,10 @@
 							<span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span>
 						</div>
 					</div>
-					<span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span>
-					<div class="actions" v-if="$store.getters.isSignedIn">
+					<span class="followed" v-if="$i && $i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span>
+					<div class="actions" v-if="$i">
 						<button @click="menu" class="menu _button"><Fa :icon="faEllipsisH"/></button>
-						<MkFollowButton v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+						<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
 					</div>
 				</div>
 				<MkAvatar class="avatar" :user="user" :disable-preview="true"/>
@@ -133,7 +133,7 @@
 					</div>
 				</div>
 				<div class="description">
-					<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+					<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
 					<p v-else class="empty">{{ $t('noAccountDescription') }}</p>
 				</div>
 				<div class="fields system">
@@ -156,7 +156,7 @@
 							<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
 						</dt>
 						<dd class="value">
-							<Mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/>
+							<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/>
 						</dd>
 					</dl>
 				</div>
diff --git a/src/client/pages/welcome.setup.vue b/src/client/pages/welcome.setup.vue
index ef39a4ca06..d205d7976b 100644
--- a/src/client/pages/welcome.setup.vue
+++ b/src/client/pages/welcome.setup.vue
@@ -26,6 +26,7 @@ import MkButton from '@/components/ui/button.vue';
 import MkInput from '@/components/ui/input.vue';
 import { host } from '@/config';
 import * as os from '@/os';
+import { login } from '@/account';
 
 export default defineComponent({
 	components: {
@@ -52,8 +53,7 @@ export default defineComponent({
 				username: this.username,
 				password: this.password,
 			}).then(res => {
-				localStorage.setItem('i', res.token);
-				location.href = '/';
+				login(res.i);
 			}).catch(() => {
 				this.submitting = false;
 
diff --git a/src/client/pizzax.ts b/src/client/pizzax.ts
new file mode 100644
index 0000000000..02a1bf8bdc
--- /dev/null
+++ b/src/client/pizzax.ts
@@ -0,0 +1,124 @@
+import { Ref, ref, watch } from 'vue';
+import { $i } from './account';
+import { api } from './os';
+
+type StateDef = Record<string, {
+	where: 'account' | 'device' | 'deviceAccount';
+	default: any;
+}>;
+
+type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
+
+export class Storage<T extends StateDef> {
+	public readonly key: string;
+
+	public readonly def: T;
+
+	// TODO: これが実装されたらreadonlyにしたい: https://github.com/microsoft/TypeScript/issues/37487
+	public readonly state: { [K in keyof T]: T[K]['default'] };
+	public readonly reactiveState: { [K in keyof T]: Ref<T[K]['default']> };
+
+	constructor(key: string, def: T) {
+		this.key = 'pizzax::' + key;
+		this.def = def;
+
+		// TODO: indexedDBにする
+		const deviceState = JSON.parse(localStorage.getItem(this.key) || '{}');
+		const deviceAccountState = $i ? JSON.parse(localStorage.getItem(this.key + '::' + $i.id) || '{}') : {};
+
+		const state = {};
+		const reactiveState = {};
+		for (const [k, v] of Object.entries(def)) {
+			if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
+				state[k] = deviceState[k];
+			} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call($i.clientData, k)) {
+				state[k] = $i.clientData[k];
+			} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
+				state[k] = deviceAccountState[k];
+			} else {
+				state[k] = v.default;
+				if (_DEV_) console.log('Use default value', k, v.default);
+			}
+		}
+		for (const [k, v] of Object.entries(state)) {
+			reactiveState[k] = ref(v);
+		}
+		this.state = state as any;
+		this.reactiveState = reactiveState as any;
+
+		if ($i) {
+			watch($i, () => {
+				if (_DEV_) console.log('$i updated');
+
+				for (const [k, v] of Object.entries(def)) {
+					if (v.where === 'account' && Object.prototype.hasOwnProperty.call($i!.clientData, k)) {
+						state[k] = $i!.clientData[k];
+						reactiveState[k].value = $i!.clientData[k];
+					}
+				}
+			});
+		}
+	}
+
+	public set<K extends keyof T>(key: K, value: T[K]['default']): void {
+		if (_DEV_) console.log('set', key, value);
+
+		this.state[key] = value;
+		this.reactiveState[key].value = value;
+
+		switch (this.def[key].where) {
+			case 'device': {
+				const deviceState = JSON.parse(localStorage.getItem(this.key) || '{}');
+				deviceState[key] = value;
+				localStorage.setItem(this.key, JSON.stringify(deviceState));
+				break;
+			}
+			case 'deviceAccount': {
+				if ($i == null) break;
+				const deviceAccountState = JSON.parse(localStorage.getItem(this.key + '::' + $i.id) || '{}');
+				deviceAccountState[key] = value;
+				localStorage.setItem(this.key + '::' + $i.id, JSON.stringify(deviceAccountState));
+				break;
+			}
+			case 'account': {
+				api('i/update-client-setting', {
+					name: key,
+					value: value
+				});
+				break;
+			}
+		}
+	}
+
+	public push<K extends keyof T>(key: K, value: ArrayElement<T[K]['default']>): void {
+		const currentState = this.state[key];
+		this.set(key, [...currentState, value]);
+	}
+
+	public reset(key: keyof T) {
+		this.set(key, this.def[key].default);
+	}
+
+	/**
+	 * 特定のキーの、簡易的なgetter/setterを作ります
+	 * 主にvue場で設定コントロールのmodelとして使う用
+	 */
+	public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]) {
+		// TODO: VueのcustomRef使うと良い感じになるかも
+		const valueRef = ref(this.state[key]);
+		return {
+			get: () => {
+				if (getter) {
+					return getter(valueRef.value);
+				} else {
+					return valueRef.value;
+				}
+			},
+			set: (value: unknown) => {
+				const val = setter ? setter(value) : value;
+				this.set(key, val);
+				valueRef.value = val;
+			}
+		};
+	}
+}
diff --git a/src/client/router.ts b/src/client/router.ts
index a21c6494b9..ad157d2a24 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -3,7 +3,7 @@ import { createRouter, createWebHistory } from 'vue-router';
 import MkLoading from '@/pages/_loading_.vue';
 import MkError from '@/pages/_error_.vue';
 import MkTimeline from '@/pages/timeline.vue';
-import { store } from './store';
+import { $i } from './account';
 
 const page = (path: string) => defineAsyncComponent({
 	loader: () => import(`./pages/${path}.vue`),
@@ -17,7 +17,7 @@ export const router = createRouter({
 	history: createWebHistory(),
 	routes: [
 		// NOTE: MkTimelineをdynamic importするとAsyncComponentWrapperが間に入るせいでkeep-aliveのコンポーネント指定が効かなくなる
-		{ path: '/', name: 'index', component: store.getters.isSignedIn ? MkTimeline : page('welcome') },
+		{ path: '/', name: 'index', component: $i ? MkTimeline : page('welcome') },
 		{ path: '/@:acct/:page?', name: 'user', component: page('user/index'), props: route => ({ acct: route.params.acct, page: route.params.page || 'index' }) },
 		{ path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) },
 		{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
diff --git a/src/client/scripts/aiscript/api.ts b/src/client/scripts/aiscript/api.ts
index f5618bd14c..20c15d809e 100644
--- a/src/client/scripts/aiscript/api.ts
+++ b/src/client/scripts/aiscript/api.ts
@@ -1,13 +1,13 @@
 import { utils, values } from '@syuilo/aiscript';
-import { store } from '@/store';
 import * as os from '@/os';
+import { $i } from '@/account';
 
 export function createAiScriptEnv(opts) {
 	let apiRequests = 0;
 	return {
-		USER_ID: store.getters.isSignedIn ? values.STR(store.state.i.id) : values.NULL,
-		USER_NAME: store.getters.isSignedIn ? values.STR(store.state.i.name) : values.NULL,
-		USER_USERNAME: store.getters.isSignedIn ? values.STR(store.state.i.username) : values.NULL,
+		USER_ID: $i ? values.STR($i.id) : values.NULL,
+		USER_NAME: $i ? values.STR($i.name) : values.NULL,
+		USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
 		'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
 			await os.dialog({
 				type: type ? type.value : 'info',
diff --git a/src/client/scripts/get-user-menu.ts b/src/client/scripts/get-user-menu.ts
index a97fed9919..bac944aa0b 100644
--- a/src/client/scripts/get-user-menu.ts
+++ b/src/client/scripts/get-user-menu.ts
@@ -5,11 +5,12 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
 import { host } from '@/config';
 import getAcct from '../../misc/acct/render';
 import * as os from '@/os';
-import { store, userActions } from '@/store';
+import { userActions } from '@/store';
 import { router } from '@/router';
+import { $i } from '@/account';
 
 export function getUserMenu(user) {
-	const meId = store.getters.isSignedIn ? store.state.i.id : null;
+	const meId = $i ? $i.id : null;
 
 	async function pushList() {
 		const t = i18n.global.t('selectList'); // なぜか後で参照すると null になるので最初にメモリに確保しておく
@@ -146,7 +147,7 @@ export function getUserMenu(user) {
 		action: inviteGroup
 	} : undefined] as any;
 
-	if (store.getters.isSignedIn && meId != user.id) {
+	if ($i && meId != user.id) {
 		menu = menu.concat([null, {
 			icon: user.isMuted ? faEye : faEyeSlash,
 			text: user.isMuted ? i18n.global.t('unmute') : i18n.global.t('mute'),
@@ -163,7 +164,7 @@ export function getUserMenu(user) {
 			action: reportAbuse
 		}]);
 
-		if (store.getters.isSignedIn && (store.state.i.isAdmin || store.state.i.isModerator)) {
+		if ($i && ($i.isAdmin || $i.isModerator)) {
 			menu = menu.concat([null, {
 				icon: faMicrophoneSlash,
 				text: user.isSilenced ? i18n.global.t('unsilence') : i18n.global.t('silence'),
@@ -176,7 +177,7 @@ export function getUserMenu(user) {
 		}
 	}
 
-	if (store.getters.isSignedIn && meId === user.id) {
+	if ($i && meId === user.id) {
 		menu = menu.concat([null, {
 			icon: faPencilAlt,
 			text: i18n.global.t('editProfile'),
diff --git a/src/client/scripts/please-login.ts b/src/client/scripts/please-login.ts
index a221665295..1d27de2c7f 100644
--- a/src/client/scripts/please-login.ts
+++ b/src/client/scripts/please-login.ts
@@ -1,9 +1,9 @@
+import { $i } from '@/account';
 import { i18n } from '@/i18n';
 import { dialog } from '@/os';
-import { store } from '@/store';
 
 export function pleaseLogin() {
-	if (store.getters.isSignedIn) return;
+	if ($i) return;
 
 	dialog({
 		title: i18n.global.t('signinRequired'),
diff --git a/src/client/scripts/sound.ts b/src/client/scripts/sound.ts
index 13fd9a80f5..176d2b68bf 100644
--- a/src/client/scripts/sound.ts
+++ b/src/client/scripts/sound.ts
@@ -1,15 +1,15 @@
-import { device } from '@/cold-storage';
+import { ColdDeviceStorage } from '@/store';
 
 const cache = new Map<string, HTMLAudioElement>();
 
 export function play(type: string) {
-	const sound = device.get('sound_' + type as any);
+	const sound = ColdDeviceStorage.get('sound_' + type as any);
 	if (sound.type == null) return;
 	playFile(sound.type, sound.volume);
 }
 
 export function playFile(file: string, volume: number) {
-	const masterVolume = device.get('sound_masterVolume');
+	const masterVolume = ColdDeviceStorage.get('sound_masterVolume');
 	if (masterVolume === 0) return;
 
 	let audio: HTMLAudioElement;
diff --git a/src/client/sidebar.ts b/src/client/sidebar.ts
index a3a32d7875..318c0c5331 100644
--- a/src/client/sidebar.ts
+++ b/src/client/sidebar.ts
@@ -1,37 +1,37 @@
 import { faBell, faComments, faEnvelope } from '@fortawesome/free-regular-svg-icons';
 import { faAt, faBroadcastTower, faCloud, faColumns, faDoorClosed, faFileAlt, faFireAlt, faGamepad, faHashtag, faListUl, faPaperclip, faSatellite, faSatelliteDish, faSearch, faStar, faTerminal, faUserClock, faUsers } from '@fortawesome/free-solid-svg-icons';
 import { computed } from 'vue';
-import { store } from '@/store';
 import { search } from '@/scripts/search';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
+import { $i } from './account';
 
 export const sidebarDef = {
 	notifications: {
 		title: 'notifications',
 		icon: faBell,
-		show: computed(() => store.getters.isSignedIn),
-		indicated: computed(() => store.getters.isSignedIn && store.state.i.hasUnreadNotification),
+		show: computed(() => $i != null),
+		indicated: computed(() => $i != null && $i.hasUnreadNotification),
 		to: '/my/notifications',
 	},
 	messaging: {
 		title: 'messaging',
 		icon: faComments,
-		show: computed(() => store.getters.isSignedIn),
-		indicated: computed(() => store.getters.isSignedIn && store.state.i.hasUnreadMessagingMessage),
+		show: computed(() => $i != null),
+		indicated: computed(() => $i != null && $i.hasUnreadMessagingMessage),
 		to: '/my/messaging',
 	},
 	drive: {
 		title: 'drive',
 		icon: faCloud,
-		show: computed(() => store.getters.isSignedIn),
+		show: computed(() => $i != null),
 		to: '/my/drive',
 	},
 	followRequests: {
 		title: 'followRequests',
 		icon: faUserClock,
-		show: computed(() => store.getters.isSignedIn && store.state.i.isLocked),
-		indicated: computed(() => store.getters.isSignedIn && store.state.i.hasPendingReceivedFollowRequest),
+		show: computed(() => $i != null && $i.isLocked),
+		indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
 		to: '/my/follow-requests',
 	},
 	featured: {
@@ -47,7 +47,7 @@ export const sidebarDef = {
 	announcements: {
 		title: 'announcements',
 		icon: faBroadcastTower,
-		indicated: computed(() => store.getters.isSignedIn && store.state.i.hasUnreadAnnouncement),
+		indicated: computed(() => $i != null && $i.hasUnreadAnnouncement),
 		to: '/announcements',
 	},
 	search: {
@@ -58,39 +58,39 @@ export const sidebarDef = {
 	lists: {
 		title: 'lists',
 		icon: faListUl,
-		show: computed(() => store.getters.isSignedIn),
+		show: computed(() => $i != null),
 		to: '/my/lists',
 	},
 	groups: {
 		title: 'groups',
 		icon: faUsers,
-		show: computed(() => store.getters.isSignedIn),
+		show: computed(() => $i != null),
 		to: '/my/groups',
 	},
 	antennas: {
 		title: 'antennas',
 		icon: faSatellite,
-		show: computed(() => store.getters.isSignedIn),
+		show: computed(() => $i != null),
 		to: '/my/antennas',
 	},
 	mentions: {
 		title: 'mentions',
 		icon: faAt,
-		show: computed(() => store.getters.isSignedIn),
-		indicated: computed(() => store.getters.isSignedIn && store.state.i.hasUnreadMentions),
+		show: computed(() => $i != null),
+		indicated: computed(() => $i != null && $i.hasUnreadMentions),
 		to: '/my/mentions',
 	},
 	messages: {
 		title: 'directNotes',
 		icon: faEnvelope,
-		show: computed(() => store.getters.isSignedIn),
-		indicated: computed(() => store.getters.isSignedIn && store.state.i.hasUnreadSpecifiedNotes),
+		show: computed(() => $i != null),
+		indicated: computed(() => $i != null && $i.hasUnreadSpecifiedNotes),
 		to: '/my/messages',
 	},
 	favorites: {
 		title: 'favorites',
 		icon: faStar,
-		show: computed(() => store.getters.isSignedIn),
+		show: computed(() => $i != null),
 		to: '/my/favorites',
 	},
 	pages: {
@@ -101,7 +101,7 @@ export const sidebarDef = {
 	clips: {
 		title: 'clip',
 		icon: faPaperclip,
-		show: computed(() => store.getters.isSignedIn),
+		show: computed(() => $i != null),
 		to: '/my/clips',
 	},
 	channels: {
@@ -122,8 +122,8 @@ export const sidebarDef = {
 	rooms: {
 		title: 'rooms',
 		icon: faDoorClosed,
-		show: computed(() => store.getters.isSignedIn),
-		to: computed(() => `/@${store.state.i.username}/room`),
+		show: computed(() => $i != null),
+		to: computed(() => `/@${$i.username}/room`),
 	},
 	ui: {
 		title: 'switchUi',
diff --git a/src/client/store.ts b/src/client/store.ts
index 0776c94796..363abe4b7f 100644
--- a/src/client/store.ts
+++ b/src/client/store.ts
@@ -1,99 +1,6 @@
-import { createStore } from 'vuex';
-import createPersistedState from 'vuex-persistedstate';
-import * as nestedProperty from 'nested-property';
-import { api } from '@/os';
-import { erase } from '../prelude/array';
-
-export const defaultSettings = {
-	tutorial: 0,
-	keepCw: false,
-	showFullAcct: false,
-	rememberNoteVisibility: false,
-	defaultNoteVisibility: 'public',
-	defaultNoteLocalOnly: false,
-	uploadFolder: null,
-	pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
-	memo: null,
-	reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
-	mutedWords: [],
-};
-
-export const defaultDeviceUserSettings = {
-	visibility: 'public',
-	localOnly: false,
-	widgets: [],
-	tl: {
-		src: 'home'
-	},
-	menu: [
-		'notifications',
-		'messaging',
-		'drive',
-		'-',
-		'followRequests',
-		'featured',
-		'explore',
-		'announcements',
-		'search',
-		'-',
-		'ui',
-	],
-	deck: {
-		columns: [],
-		layout: [],
-	},
-	plugins: [] as {
-		id: string;
-		name: string;
-		active: boolean;
-		configData: Record<string, any>;
-		token: string;
-		ast: any[];
-	}[],
-};
-
-export const defaultDeviceSettings = {
-	lang: null,
-	loadRawImages: false,
-	nsfw: 'respect', // respect, force, ignore
-	useOsNativeEmojis: false,
-	serverDisconnectedBehavior: 'quiet',
-	accounts: [],
-	recentlyUsedEmojis: [],
-	recentlyUsedUsers: [],
-	themes: [],
-	darkTheme: '8050783a-7f63-445a-b270-36d0f6ba1677',
-	lightTheme: '4eea646f-7afa-4645-83e9-83af0333cd37',
-	darkMode: false,
-	deckMode: false,
-	syncDeviceDarkMode: true,
-	animation: true,
-	animatedMfm: true,
-	imageNewTab: false,
-	chatOpenBehavior: 'page',
-	defaultSideView: false,
-	deckNavWindow: true,
-	showFixedPostForm: false,
-	disablePagesScript: false,
-	enableInfiniteScroll: true,
-	useBlurEffectForModal: true,
-	useFullReactionPicker: false,
-	reactionPickerWidth: 1,
-	reactionPickerHeight: 1,
-	showGapBetweenNotesInTimeline: true,
-	sidebarDisplay: 'full', // full, icon, hide
-	instanceTicker: 'remote', // none, remote, always
-	roomGraphicsQuality: 'medium',
-	roomUseOrthographicCamera: true,
-	deckColumnAlign: 'left',
-	deckAlwaysShowMainColumn: true,
-	deckMainColumnPlace: 'left',
-	userData: {},
-};
-
-function copy<T>(data: T): T {
-	return JSON.parse(JSON.stringify(data));
-}
+import { markRaw, ref } from 'vue';
+import { Storage } from './pizzax';
+import { Theme } from './scripts/theme';
 
 export const postFormActions = [];
 export const userActions = [];
@@ -101,399 +8,278 @@ export const noteActions = [];
 export const noteViewInterruptors = [];
 export const notePostInterruptors = [];
 
-export const store = createStore({
-	strict: _DEV_,
-
-	plugins: [createPersistedState({
-		paths: ['i', 'device', 'deviceUser', 'settings', 'instance']
-	})],
-
-	state: {
-		i: null,
+// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう)
+//       あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない
+export const defaultStore = markRaw(new Storage('base', {
+	tutorial: {
+		where: 'account',
+		default: 0
+	},
+	keepCw: {
+		where: 'account',
+		default: false
+	},
+	showFullAcct: {
+		where: 'account',
+		default: false
+	},
+	rememberNoteVisibility: {
+		where: 'account',
+		default: false
+	},
+	defaultNoteVisibility: {
+		where: 'account',
+		default: 'public'
+	},
+	defaultNoteLocalOnly: {
+		where: 'account',
+		default: false
+	},
+	uploadFolder: {
+		where: 'account',
+		default: null
+	},
+	pastedFileName: {
+		where: 'account',
+		default: 'yyyy-MM-dd HH-mm-ss [{{number}}]'
+	},
+	memo: {
+		where: 'account',
+		default: null
+	},
+	reactions: {
+		where: 'account',
+		default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮']
+	},
+	mutedWords: {
+		where: 'account',
+		default: []
 	},
 
-	getters: {
-		isSignedIn: state => state.i != null,
+	menu: {
+		where: 'deviceAccount',
+		default: [
+			'notifications',
+			'messaging',
+			'drive',
+			'-',
+			'followRequests',
+			'featured',
+			'explore',
+			'announcements',
+			'search',
+			'-',
+			'ui',
+		]
+	},
+	visibility: {
+		where: 'deviceAccount',
+		default: 'public' as 'public' | 'home' | 'followers' | 'specified'
+	},
+	localOnly: {
+		where: 'deviceAccount',
+		default: false
+	},
+	widgets: {
+		where: 'deviceAccount',
+		default: [] as {
+			name: string;
+			id: string;
+			data: Record<string, any>;
+		}[]
+	},
+	tl: {
+		where: 'deviceAccount',
+		default: {
+			src: 'home',
+			arg: null
+		}
 	},
 
-	mutations: {
-		updateI(state, x) {
-			state.i = x;
-		},
-
-		updateIKeyValue(state, { key, value }) {
-			state.i[key] = value;
-		},
+	serverDisconnectedBehavior: {
+		where: 'device',
+		default: 'quiet' as 'quiet' | 'reload' | 'dialog'
 	},
-
-	actions: {
-		async login(ctx, i) {
-			ctx.commit('updateI', i);
-			ctx.commit('settings/init', i.clientData);
-			ctx.commit('deviceUser/init', ctx.state.device.userData[i.id] || {});
-			// TODO: ローカルストレージを消してページリロードしたときは i が無いのでその場合のハンドリングをよしなにやる
-			await ctx.dispatch('addAcount', { id: i.id, i: localStorage.getItem('i') });
-		},
-
-		addAcount(ctx, info) {
-			if (!ctx.state.device.accounts.some(x => x.id === info.id)) {
-				ctx.commit('device/set', {
-					key: 'accounts',
-					value: ctx.state.device.accounts.concat([{ id: info.id, token: info.i }])
-				});
-			}
-		},
-
-		logout(ctx) {
-			ctx.commit('device/setUserData', { userId: ctx.state.i.id, data: ctx.state.deviceUser });
-			ctx.commit('updateI', null);
-			ctx.commit('settings/init', {});
-			ctx.commit('deviceUser/init', {});
-			localStorage.removeItem('i');
-			document.cookie = `igi=; path=/`;
-		},
-
-		async switchAccount(ctx, i) {
-			ctx.commit('device/setUserData', { userId: ctx.state.i.id, data: ctx.state.deviceUser });
-			localStorage.setItem('i', i.token);
-			await ctx.dispatch('login', i);
-		},
-
-		mergeMe(ctx, me) {
-			// TODO: プロパティ一つ一つに対してコミットが発生するのはアレなので良い感じにする
-			for (const [key, value] of Object.entries(me)) {
-				ctx.commit('updateIKeyValue', { key, value });
-			}
-
-			if (me.clientData) {
-				ctx.commit('settings/init', me.clientData);
-			}
-		},
+	nsfw: {
+		where: 'device',
+		default: 'respect' as 'respect' | 'force' | 'ignore'
 	},
+	animation: {
+		where: 'device',
+		default: true
+	},
+	animatedMfm: {
+		where: 'device',
+		default: true
+	},
+	loadRawImages: {
+		where: 'device',
+		default: false
+	},
+	imageNewTab: {
+		where: 'device',
+		default: false
+	},
+	disableShowingAnimatedImages: {
+		where: 'device',
+		default: false
+	},
+	disablePagesScript: {
+		where: 'device',
+		default: false
+	},
+	useOsNativeEmojis: {
+		where: 'device',
+		default: false
+	},
+	useBlurEffectForModal: {
+		where: 'device',
+		default: true
+	},
+	showFixedPostForm: {
+		where: 'device',
+		default: false
+	},
+	enableInfiniteScroll: {
+		where: 'device',
+		default: true
+	},
+	showGapBetweenNotesInTimeline: {
+		where: 'device',
+		default: true
+	},
+	darkMode: {
+		where: 'device',
+		default: false
+	},
+	instanceTicker: {
+		where: 'device',
+		default: 'remote' as 'none' | 'remote' | 'always'
+	},
+	reactionPickerWidth: {
+		where: 'device',
+		default: 1
+	},
+	reactionPickerHeight: {
+		where: 'device',
+		default: 1
+	},
+	recentlyUsedEmojis: {
+		where: 'device',
+		default: [] as string[]
+	},
+	recentlyUsedUsers: {
+		where: 'device',
+		default: [] as string[]
+	},
+	defaultSideView: {
+		where: 'device',
+		default: false
+	},
+	sidebarDisplay: {
+		where: 'device',
+		default: 'full' as 'full' | 'icon'
+	},
+}));
 
-	modules: {
-		instance: {
-			namespaced: true,
+// TODO: 他のタブと永続化されたstateを同期
 
-			state: {
-				meta: null
-			},
+const PREFIX = 'miux:';
 
-			getters: {
-				emojiCategories: state => {
-					const categories = new Set();
-					for (const emoji of state.meta.emojis) {
-						categories.add(emoji.category);
-					}
-					return Array.from(categories);
-				},
-			},
+type Plugin = {
+	id: string;
+	name: string;
+	active: boolean;
+	configData: Record<string, any>;
+	token: string;
+	ast: any[];
+};
 
-			mutations: {
-				set(state, meta) {
-					state.meta = meta;
-				},
-			},
+/**
+ * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
+ */
+export class ColdDeviceStorage {
+	public static default = {
+		themes: [] as Theme[],
+		darkTheme: '8050783a-7f63-445a-b270-36d0f6ba1677',
+		lightTheme: '4eea646f-7afa-4645-83e9-83af0333cd37',
+		syncDeviceDarkMode: true,
+		chatOpenBehavior: 'page' as 'page' | 'window' | 'popout',
+		plugins: [] as Plugin[],
+		mediaVolume: 0.5,
+		sound_masterVolume: 0.3,
+		sound_note: { type: 'syuilo/down', volume: 1 },
+		sound_noteMy: { type: 'syuilo/up', volume: 1 },
+		sound_notification: { type: 'syuilo/pope2', volume: 1 },
+		sound_chat: { type: 'syuilo/pope1', volume: 1 },
+		sound_chatBg: { type: 'syuilo/waon', volume: 1 },
+		sound_antenna: { type: 'syuilo/triple', volume: 1 },
+		sound_channel: { type: 'syuilo/square-pico', volume: 1 },
+		sound_reversiPutBlack: { type: 'syuilo/kick', volume: 0.3 },
+		sound_reversiPutWhite: { type: 'syuilo/snare', volume: 0.3 },
+		roomGraphicsQuality: 'medium' as 'cheep' | 'low' | 'medium' | 'high' | 'ultra',
+		roomUseOrthographicCamera: true,
+	};
 
-			actions: {
-				async fetch(ctx) {
-					const meta = await api('meta', {
-						detail: false
-					});
+	public static watchers = [];
 
-					ctx.commit('set', meta);
-				}
-			}
-		},
-
-		device: {
-			namespaced: true,
-
-			state: defaultDeviceSettings,
-
-			mutations: {
-				overwrite(state, x) {
-					for (const k of Object.keys(state)) {
-						if (x[k] === undefined) delete state[k];
-					}
-					for (const k of Object.keys(x)) {
-						state[k] = x[k];
-					}
-				},
-
-				set(state, x: { key: string; value: any }) {
-					state[x.key] = x.value;
-				},
-
-				setUserData(state, x: { userId: string; data: any }) {
-					state.userData[x.userId] = copy(x.data);
-				},
-			}
-		},
-
-		deviceUser: {
-			namespaced: true,
-
-			state: defaultDeviceUserSettings,
-
-			mutations: {
-				overwrite(state, x) {
-					for (const k of Object.keys(state)) {
-						if (x[k] === undefined) delete state[k];
-					}
-					for (const k of Object.keys(x)) {
-						state[k] = x[k];
-					}
-				},
-
-				init(state, x) {
-					for (const [key, value] of Object.entries(defaultDeviceUserSettings)) {
-						if (Object.prototype.hasOwnProperty.call(x, key)) {
-							state[key] = x[key];
-						} else {
-							state[key] = value;
-						}
-					}
-				},
-
-				set(state, x: { key: string; value: any }) {
-					state[x.key] = x.value;
-				},
-
-				setTl(state, x) {
-					state.tl = {
-						src: x.src,
-						arg: x.arg
-					};
-				},
-
-				setMenu(state, menu) {
-					state.menu = menu;
-				},
-
-				setVisibility(state, visibility) {
-					state.visibility = visibility;
-				},
-
-				setLocalOnly(state, localOnly) {
-					state.localOnly = localOnly;
-				},
-
-				setWidgets(state, widgets) {
-					state.widgets = widgets;
-				},
-
-				addWidget(state, widget) {
-					state.widgets.unshift(widget);
-				},
-
-				removeWidget(state, widget) {
-					state.widgets = state.widgets.filter(w => w.id != widget.id);
-				},
-
-				updateWidget(state, x) {
-					const w = state.widgets.find(w => w.id === x.id);
-					if (w) {
-						w.data = x.data;
-					}
-				},
-
-				//#region Deck
-				// TODO: deck関連は動的にモジュール読み込みしたい
-				addDeckColumn(state, column) {
-					if (column.name == undefined) column.name = null;
-					state.deck.columns.push(column);
-					state.deck.layout.push([column.id]);
-				},
-
-				removeDeckColumn(state, id) {
-					state.deck.columns = state.deck.columns.filter(c => c.id != id);
-					state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
-					state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
-				},
-
-				swapDeckColumn(state, x) {
-					const a = x.a;
-					const b = x.b;
-					const aX = state.deck.layout.findIndex(ids => ids.indexOf(a) != -1);
-					const aY = state.deck.layout[aX].findIndex(id => id == a);
-					const bX = state.deck.layout.findIndex(ids => ids.indexOf(b) != -1);
-					const bY = state.deck.layout[bX].findIndex(id => id == b);
-					state.deck.layout[aX][aY] = b;
-					state.deck.layout[bX][bY] = a;
-				},
-
-				swapLeftDeckColumn(state, id) {
-					state.deck.layout.some((ids, i) => {
-						if (ids.indexOf(id) != -1) {
-							const left = state.deck.layout[i - 1];
-							if (left) {
-								// https://vuejs.org/v2/guide/list.html#Caveats
-								//state.deck.layout[i - 1] = state.deck.layout[i];
-								//state.deck.layout[i] = left;
-								state.deck.layout.splice(i - 1, 1, state.deck.layout[i]);
-								state.deck.layout.splice(i, 1, left);
-							}
-							return true;
-						}
-					});
-				},
-
-				swapRightDeckColumn(state, id) {
-					state.deck.layout.some((ids, i) => {
-						if (ids.indexOf(id) != -1) {
-							const right = state.deck.layout[i + 1];
-							if (right) {
-								// https://vuejs.org/v2/guide/list.html#Caveats
-								//state.deck.layout[i + 1] = state.deck.layout[i];
-								//state.deck.layout[i] = right;
-								state.deck.layout.splice(i + 1, 1, state.deck.layout[i]);
-								state.deck.layout.splice(i, 1, right);
-							}
-							return true;
-						}
-					});
-				},
-
-				swapUpDeckColumn(state, id) {
-					const ids = state.deck.layout.find(ids => ids.indexOf(id) != -1);
-					ids.some((x, i) => {
-						if (x == id) {
-							const up = ids[i - 1];
-							if (up) {
-								// https://vuejs.org/v2/guide/list.html#Caveats
-								//ids[i - 1] = id;
-								//ids[i] = up;
-								ids.splice(i - 1, 1, id);
-								ids.splice(i, 1, up);
-							}
-							return true;
-						}
-					});
-				},
-
-				swapDownDeckColumn(state, id) {
-					const ids = state.deck.layout.find(ids => ids.indexOf(id) != -1);
-					ids.some((x, i) => {
-						if (x == id) {
-							const down = ids[i + 1];
-							if (down) {
-								// https://vuejs.org/v2/guide/list.html#Caveats
-								//ids[i + 1] = id;
-								//ids[i] = down;
-								ids.splice(i + 1, 1, id);
-								ids.splice(i, 1, down);
-							}
-							return true;
-						}
-					});
-				},
-
-				stackLeftDeckColumn(state, id) {
-					const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
-					state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
-					const left = state.deck.layout[i - 1];
-					if (left) state.deck.layout[i - 1].push(id);
-					state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
-				},
-
-				popRightDeckColumn(state, id) {
-					const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
-					state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
-					state.deck.layout.splice(i + 1, 0, [id]);
-					state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
-				},
-
-				addDeckWidget(state, x) {
-					const column = state.deck.columns.find(c => c.id == x.id);
-					if (column == null) return;
-					if (column.widgets == null) column.widgets = [];
-					column.widgets.unshift(x.widget);
-				},
-
-				removeDeckWidget(state, x) {
-					const column = state.deck.columns.find(c => c.id == x.id);
-					if (column == null) return;
-					column.widgets = column.widgets.filter(w => w.id != x.widget.id);
-				},
-
-				setDeckWidgets(state, x) {
-					const column = state.deck.columns.find(c => c.id == x.id);
-					if (column == null) return;
-					column.widgets = x.widgets;
-				},
-
-				renameDeckColumn(state, x) {
-					const column = state.deck.columns.find(c => c.id == x.id);
-					if (column == null) return;
-					column.name = x.name;
-				},
-
-				updateDeckColumn(state, x) {
-					const column = state.deck.columns.findIndex(c => c.id == x.id);
-					if (column > -1) return;
-					state.deck.columns[column] = x;
-				},
-				//#endregion
-
-				installPlugin(state, { id, meta, ast, token }) {
-					state.plugins.push({
-						...meta,
-						id,
-						active: true,
-						configData: {},
-						token: token,
-						ast: ast
-					});
-				},
-
-				uninstallPlugin(state, id) {
-					state.plugins = state.plugins.filter(x => x.id != id);
-				},
-
-				configPlugin(state, { id, config }) {
-					state.plugins.find(p => p.id === id).configData = config;
-				},
-
-				changePluginActive(state, { id, active }) {
-					state.plugins.find(p => p.id === id).active = active;
-				},
-			}
-		},
-
-		settings: {
-			namespaced: true,
-
-			state: defaultSettings,
-
-			mutations: {
-				set(state, x: { key: string; value: any }) {
-					nestedProperty.set(state, x.key, x.value);
-				},
-
-				init(state, x) {
-					for (const [key, value] of Object.entries(defaultSettings)) {
-						if (Object.prototype.hasOwnProperty.call(x, key)) {
-							state[key] = x[key];
-						} else {
-							state[key] = value;
-						}
-					}
-				},
-			},
-
-			actions: {
-				set(ctx, x) {
-					ctx.commit('set', x);
-
-					if (ctx.rootGetters.isSignedIn) {
-						api('i/update-client-setting', {
-							name: x.key,
-							value: x.value
-						});
-					}
-				},
-			}
+	public static get<T extends keyof typeof ColdDeviceStorage.default>(key: T): typeof ColdDeviceStorage.default[T] {
+		// TODO: indexedDBにする
+		//       ただしその際はnullチェックではなくキー存在チェックにしないとダメ
+		//       (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
+		const value = localStorage.getItem(PREFIX + key);
+		if (value == null) {
+			return ColdDeviceStorage.default[key];
+		} else {
+			return JSON.parse(value);
 		}
 	}
-});
+
+	public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
+		localStorage.setItem(PREFIX + key, JSON.stringify(value));
+
+		for (const watcher of this.watchers) {
+			if (watcher.key === key) watcher.callback(value);
+		}
+	}
+
+	public static watch(key, callback) {
+		this.watchers.push({ key, callback });
+	}
+
+	// TODO: VueのcustomRef使うと良い感じになるかも
+	public static ref<T extends keyof typeof ColdDeviceStorage.default>(key: T) {
+		const v = ColdDeviceStorage.get(key);
+		const r = ref(v);
+		// TODO: このままではwatcherがリークするので開放する方法を考える
+		this.watch(key, v => {
+			r.value = v;
+		});
+		return r;
+	}
+
+	/**
+	 * 特定のキーの、簡易的なgetter/setterを作ります
+	 * 主にvue場で設定コントロールのmodelとして使う用
+	 */
+	public static makeGetterSetter<K extends keyof typeof ColdDeviceStorage.default>(key: K) {
+		// TODO: VueのcustomRef使うと良い感じになるかも
+		const valueRef = ColdDeviceStorage.ref(key);
+		return {
+			get: () => {
+				return valueRef.value;
+			},
+			set: (value: unknown) => {
+				const val = value;
+				ColdDeviceStorage.set(key, val);
+			}
+		};
+	}
+}
+
+// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
+declare module '@vue/runtime-core' {
+	interface ComponentCustomProperties {
+		$store: typeof defaultStore;
+	}
+}
diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json
index e6a6b8eb2d..01a77cec3a 100644
--- a/src/client/tsconfig.json
+++ b/src/client/tsconfig.json
@@ -15,7 +15,7 @@
 		"removeComments": false,
 		"noLib": false,
 		"strict": true,
-		"strictNullChecks": false,
+		"strictNullChecks": true,
 		"experimentalDecorators": true,
 		"resolveJsonModule": true,
 		"baseUrl": ".",
diff --git a/src/client/ui/_common_/common.vue b/src/client/ui/_common_/common.vue
index 469220806d..a4d1661f46 100644
--- a/src/client/ui/_common_/common.vue
+++ b/src/client/ui/_common_/common.vue
@@ -16,8 +16,8 @@
 <script lang="ts">
 import { defineAsyncComponent, defineComponent } from 'vue';
 import { stream, popup, popups, uploads, pendingApiRequestsCount } from '@/os';
-import { store } from '@/store';
 import * as sound from '@/scripts/sound';
+import { $i, $i } from '@/account';
 
 export default defineComponent({
 	components: {
@@ -27,7 +27,7 @@ export default defineComponent({
 
 	setup() {
 		const onNotification = notification => {
-			if (store.state.i.mutingNotificationTypes.includes(notification.type)) return;
+			if ($i.mutingNotificationTypes.includes(notification.type)) return;
 
 			if (document.visibilityState === 'visible') {
 				stream.send('readNotification', {
@@ -42,7 +42,7 @@ export default defineComponent({
 			sound.play('notification');
 		};
 
-		if (store.getters.isSignedIn) {
+		if ($i) {
 			const connection = stream.useSharedConnection('main', 'UI');
 			connection.on('notification', onNotification);
 		}
diff --git a/src/client/ui/_common_/header.vue b/src/client/ui/_common_/header.vue
index 25f1c5f00c..e7944e4b44 100644
--- a/src/client/ui/_common_/header.vue
+++ b/src/client/ui/_common_/header.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="fdidabkb" :style="`--height:${height};`">
-	<transition :name="$store.state.device.animation ? 'header' : ''" mode="out-in" appear>
+	<transition :name="$store.state.animation ? 'header' : ''" mode="out-in" appear>
 		<button class="_button back" v-if="withBack && canBack" @click.stop="back()"><Fa :icon="faChevronLeft"/></button>
 	</transition>
 	<template v-if="info">
diff --git a/src/client/ui/_common_/stream-indicator.vue b/src/client/ui/_common_/stream-indicator.vue
index 7b020171a4..23578bf62e 100644
--- a/src/client/ui/_common_/stream-indicator.vue
+++ b/src/client/ui/_common_/stream-indicator.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="nsbbhtug" v-if="hasDisconnected && $store.state.device.serverDisconnectedBehavior === 'quiet'" @click="resetDisconnected">
+<div class="nsbbhtug" v-if="hasDisconnected && $store.state.serverDisconnectedBehavior === 'quiet'" @click="resetDisconnected">
 	<div>{{ $t('disconnectedFromServer') }}</div>
 	<div class="command">
 		<button class="_textButton" @click="reload">{{ $t('reload') }}</button>
diff --git a/src/client/ui/deck.vue b/src/client/ui/deck.vue
index b1219c34ff..6d8fe4748e 100644
--- a/src/client/ui/deck.vue
+++ b/src/client/ui/deck.vue
@@ -1,9 +1,9 @@
 <template>
-<div class="mk-deck" :class="`${$store.state.device.deckColumnAlign}`" v-hotkey.global="keymap">
+<div class="mk-deck" :class="`${deckStore.state.columnAlign}`" v-hotkey.global="keymap">
 	<XSidebar ref="nav"/>
 
 	<!-- TODO: deckMainColumnPlace を見て位置変える -->
-	<DeckColumn class="column" v-if="$store.state.device.deckAlwaysShowMainColumn || $route.name !== 'index'">
+	<DeckColumn class="column" v-if="deckStore.state.alwaysShowMainColumn || $route.name !== 'index'">
 		<template #header>
 			<XHeader :info="pageInfo"/>
 		</template>
@@ -26,8 +26,8 @@
 
 	<button @click="addColumn" class="_button add"><Fa :icon="faPlus"/></button>
 
-	<button v-if="$store.getters.isSignedIn" class="nav _button" @click="showNav()"><Fa :icon="faBars"/><i v-if="navIndicated"><Fa :icon="faCircle"/></i></button>
-	<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><Fa :icon="faPencilAlt"/></button>
+	<button v-if="$i" class="nav _button" @click="showNav()"><Fa :icon="faBars"/><i v-if="navIndicated"><Fa :icon="faCircle"/></i></button>
+	<button v-if="$i" class="post _buttonPrimary" @click="post()"><Fa :icon="faPencilAlt"/></button>
 
 	<XCommon/>
 </div>
@@ -48,6 +48,7 @@ import { getScrollContainer } from '@/scripts/scroll';
 import * as os from '@/os';
 import { sidebarDef } from '@/sidebar';
 import XCommon from './_common_/common.vue';
+import { deckStore, addColumn } from './deck/deck-store';
 
 export default defineComponent({
 	components: {
@@ -60,6 +61,7 @@ export default defineComponent({
 
 	data() {
 		return {
+			deckStore,
 			host: host,
 			pageInfo: null,
 			pageKey: 0,
@@ -70,17 +72,14 @@ export default defineComponent({
 	},
 
 	computed: {
-		deck() {
-			return this.$store.state.deviceUser.deck;
+		columns() {
+			return deckStore.reactiveState.columns.value;
 		},
-		columns(): any[] {
-			return this.deck.columns;
-		},
-		layout(): any[] {
-			return this.deck.layout;
+		layout() {
+			return deckStore.reactiveState.layout.value;
 		},
 		navIndicated(): boolean {
-			if (!this.$store.getters.isSignedIn) return false;
+			if (!this.$i) return false;
 			for (const def in this.menuDef) {
 				if (this.menuDef[def].indicated) return true;
 			}
@@ -160,7 +159,7 @@ export default defineComponent({
 			});
 			if (canceled) return;
 
-			this.$store.commit('deviceUser/addDeckColumn', {
+			addColumn({
 				type: column,
 				id: uuid(),
 				name: this.$t('_deck._columns.' + column),
diff --git a/src/client/ui/deck/antenna-column.vue b/src/client/ui/deck/antenna-column.vue
index 87df0fa870..5fe1835c35 100644
--- a/src/client/ui/deck/antenna-column.vue
+++ b/src/client/ui/deck/antenna-column.vue
@@ -14,6 +14,7 @@ import { faSatellite, faCog } from '@fortawesome/free-solid-svg-icons';
 import XColumn from './column.vue';
 import XTimeline from '@/components/timeline.vue';
 import * as os from '@/os';
+import { updateColumn } from './deck-store';
 
 export default defineComponent({
 	components: {
@@ -73,8 +74,9 @@ export default defineComponent({
 				showCancelButton: true
 			});
 			if (canceled) return;
-			this.column.antennaId = antenna.id;
-			this.$store.commit('deviceUser/updateDeckColumn', this.column);
+			updateColumn(this.column.id, {
+				antennaId: antenna.id
+			});
 		},
 
 		focus() {
diff --git a/src/client/ui/deck/column.vue b/src/client/ui/deck/column.vue
index f708da8846..d6432cf314 100644
--- a/src/client/ui/deck/column.vue
+++ b/src/client/ui/deck/column.vue
@@ -35,6 +35,7 @@ import { defineComponent } from 'vue';
 import { faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faArrowRight, faArrowLeft, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
 import { faWindowMaximize, faTrashAlt, faWindowRestore } from '@fortawesome/free-regular-svg-icons';
 import * as os from '@/os';
+import { renameColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn } from './deck-store';
 
 export default defineComponent({
 	props: {
@@ -145,50 +146,50 @@ export default defineComponent({
 						}
 					}).then(({ canceled, result: name }) => {
 						if (canceled) return;
-						this.$store.commit('deviceUser/renameDeckColumn', { id: this.column.id, name });
+						renameColumn(this.column.id, name);
 					});
 				}
 			}, null, {
 				icon: faArrowLeft,
 				text: this.$t('_deck.swapLeft'),
 				action: () => {
-					this.$store.commit('deviceUser/swapLeftDeckColumn', this.column.id);
+					swapLeftColumn(this.column.id);
 				}
 			}, {
 				icon: faArrowRight,
 				text: this.$t('_deck.swapRight'),
 				action: () => {
-					this.$store.commit('deviceUser/swapRightDeckColumn', this.column.id);
+					swapRightColumn(this.column.id);
 				}
 			}, this.isStacked ? {
 				icon: faArrowUp,
 				text: this.$t('_deck.swapUp'),
 				action: () => {
-					this.$store.commit('deviceUser/swapUpDeckColumn', this.column.id);
+					swapUpColumn(this.column.id);
 				}
 			} : undefined, this.isStacked ? {
 				icon: faArrowDown,
 				text: this.$t('_deck.swapDown'),
 				action: () => {
-					this.$store.commit('deviceUser/swapDownDeckColumn', this.column.id);
+					swapDownColumn(this.column.id);
 				}
 			} : undefined, null, {
 				icon: faWindowRestore,
 				text: this.$t('_deck.stackLeft'),
 				action: () => {
-					this.$store.commit('deviceUser/stackLeftDeckColumn', this.column.id);
+					stackLeftColumn(this.column.id);
 				}
 			}, this.isStacked ? {
 				icon: faWindowMaximize,
 				text: this.$t('_deck.popRight'),
 				action: () => {
-					this.$store.commit('deviceUser/popRightDeckColumn', this.column.id);
+					popRightColumn(this.column.id);
 				}
 			} : undefined, null, {
 				icon: faTrashAlt,
 				text: this.$t('remove'),
 				action: () => {
-					this.$store.commit('deviceUser/removeDeckColumn', this.column.id);
+					removeColumn(this.column.id);
 				}
 			}];
 
@@ -264,10 +265,7 @@ export default defineComponent({
 
 			const id = e.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
 			if (id != null && id != '') {
-				this.$store.commit('deviceUser/swapDeckColumn', {
-					a: this.column.id,
-					b: id
-				});
+				swapColumn(this.column.id, id);
 			}
 		}
 	}
diff --git a/src/client/ui/deck/deck-store.ts b/src/client/ui/deck/deck-store.ts
new file mode 100644
index 0000000000..2d0012b566
--- /dev/null
+++ b/src/client/ui/deck/deck-store.ts
@@ -0,0 +1,223 @@
+import { markRaw } from 'vue';
+import { Storage } from '../../pizzax';
+
+type ColumnWidget = {
+	name: string;
+	id: string;
+	data: Record<string, any>;
+};
+
+type Column = {
+	id: string;
+	type: string;
+	name: string | null;
+	width: number;
+	widgets?: ColumnWidget[];
+};
+
+function copy<T>(x: T): T {
+	return JSON.parse(JSON.stringify(x));
+}
+
+export const deckStore = markRaw(new Storage('deck', {
+	columns: {
+		where: 'deviceAccount',
+		default: [] as Column[]
+	},
+	layout: {
+		where: 'deviceAccount',
+		default: [] as Column['id'][][]
+	},
+	columnAlign: {
+		where: 'deviceAccount',
+		default: 'left' as 'left' | 'right' | 'center'
+	},
+	alwaysShowMainColumn: {
+		where: 'deviceAccount',
+		default: true
+	},
+	mainColumnPlace: {
+		where: 'deviceAccount',
+		default: 'left' as 'left' | 'right'
+	},
+	navWindow: {
+		where: 'deviceAccount',
+		default: true
+	},
+}));
+
+export function addColumn(column: Column) {
+	if (column.name == undefined) column.name = null;
+	deckStore.push('columns', column);
+	deckStore.push('layout', [column.id]);
+}
+
+export function removeColumn(id: Column['id']) {
+	deckStore.set('columns', deckStore.state.columns.filter(c => c.id !== id));
+	deckStore.set('layout', deckStore.state.layout
+		.map(ids => ids.filter(_id => _id !== id))
+		.filter(ids => ids.length > 0));
+}
+
+export function swapColumn(a: Column['id'], b: Column['id']) {
+	const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) != -1);
+	const aY = deckStore.state.layout[aX].findIndex(id => id == a);
+	const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) != -1);
+	const bY = deckStore.state.layout[bX].findIndex(id => id == b);
+	const layout = copy(deckStore.state.layout);
+	layout[aX][aY] = b;
+	layout[bX][bY] = a;
+	deckStore.set('layout', layout);
+}
+
+export function swapLeftColumn(id: Column['id']) {
+	const layout = copy(deckStore.state.layout);
+	deckStore.state.layout.some((ids, i) => {
+		if (ids.includes(id)) {
+			const left = deckStore.state.layout[i - 1];
+			if (left) {
+				layout[i - 1] = deckStore.state.layout[i];
+				layout[i] = left;
+				deckStore.set('layout', layout);
+			}
+			return true;
+		}
+	});
+}
+
+export function swapRightColumn(id: Column['id']) {
+	const layout = copy(deckStore.state.layout);
+	deckStore.state.layout.some((ids, i) => {
+		if (ids.includes(id)) {
+			const right = deckStore.state.layout[i + 1];
+			if (right) {
+				layout[i + 1] = deckStore.state.layout[i];
+				layout[i] = right;
+				deckStore.set('layout', layout);
+			}
+			return true;
+		}
+	});
+}
+
+export function swapUpColumn(id: Column['id']) {
+	const layout = copy(deckStore.state.layout);
+	const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
+	const ids = copy(deckStore.state.layout[idsIndex]);
+	ids.some((x, i) => {
+		if (x === id) {
+			const up = ids[i - 1];
+			if (up) {
+				ids[i - 1] = id;
+				ids[i] = up;
+
+				layout[idsIndex] = ids;
+				deckStore.set('layout', layout);
+			}
+			return true;
+		}
+	});
+}
+
+export function swapDownColumn(id: Column['id']) {
+	const layout = copy(deckStore.state.layout);
+	const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
+	const ids = copy(deckStore.state.layout[idsIndex]);
+	ids.some((x, i) => {
+		if (x === id) {
+			const down = ids[i + 1];
+			if (down) {
+				ids[i + 1] = id;
+				ids[i] = down;
+
+				layout[idsIndex] = ids;
+				deckStore.set('layout', layout);
+			}
+			return true;
+		}
+	});
+}
+
+export function stackLeftColumn(id: Column['id']) {
+	let layout = copy(deckStore.state.layout);
+	const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
+	layout = layout.map(ids => ids.filter(_id => _id !== id));
+	layout[i - 1].push(id);
+	layout = layout.filter(ids => ids.length > 0);
+	deckStore.set('layout', layout);
+}
+
+export function popRightColumn(id: Column['id']) {
+	let layout = copy(deckStore.state.layout);
+	const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
+	layout = layout.map(ids => ids.filter(_id => _id !== id));
+	layout.splice(i + 1, 0, [id]);
+	layout = layout.filter(ids => ids.length > 0);
+	deckStore.set('layout', layout);
+}
+
+export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
+	const columns = copy(deckStore.state.columns);
+	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
+	const column = copy(deckStore.state.columns[columnIndex]);
+	if (column == null) return;
+	if (column.widgets == null) column.widgets = [];
+	column.widgets.unshift(widget);
+	columns[columnIndex] = column;
+	deckStore.set('columns', columns);
+}
+
+export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
+	const columns = copy(deckStore.state.columns);
+	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
+	const column = copy(deckStore.state.columns[columnIndex]);
+	if (column == null) return;
+	column.widgets = column.widgets.filter(w => w.id != widget.id);
+	columns[columnIndex] = column;
+	deckStore.set('columns', columns);
+}
+
+export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
+	const columns = copy(deckStore.state.columns);
+	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
+	const column = copy(deckStore.state.columns[columnIndex]);
+	if (column == null) return;
+	column.widgets = widgets;
+	columns[columnIndex] = column;
+	deckStore.set('columns', columns);
+}
+
+export function updateColumnWidget(id: Column['id'], widgetId: string, data: any) {
+	const columns = copy(deckStore.state.columns);
+	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
+	const column = copy(deckStore.state.columns[columnIndex]);
+	if (column == null) return;
+	column.widgets = column.widgets.map(w => w.id === widgetId ? {
+		...w,
+		data: data
+	} : w);
+	columns[columnIndex] = column;
+	deckStore.set('columns', columns);
+}
+
+export function renameColumn(id: Column['id'], name: Column['name']) {
+	const columns = copy(deckStore.state.columns);
+	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
+	const column = copy(deckStore.state.columns[columnIndex]);
+	if (column == null) return;
+	column.name = name;
+	columns[columnIndex] = column;
+	deckStore.set('columns', columns);
+}
+
+export function updateColumn(id: Column['id'], column: Partial<Column>) {
+	const columns = copy(deckStore.state.columns);
+	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
+	const currentColumn = copy(deckStore.state.columns[columnIndex]);
+	if (currentColumn == null) return;
+	for (const [k, v] of Object.entries(column)) {
+		currentColumn[k] = v;
+	}
+	columns[columnIndex] = currentColumn;
+	deckStore.set('columns', columns);
+}
diff --git a/src/client/ui/deck/list-column.vue b/src/client/ui/deck/list-column.vue
index ee4bf2bf1b..b120236147 100644
--- a/src/client/ui/deck/list-column.vue
+++ b/src/client/ui/deck/list-column.vue
@@ -14,6 +14,7 @@ import { faListUl, faCog } from '@fortawesome/free-solid-svg-icons';
 import XColumn from './column.vue';
 import XTimeline from '@/components/timeline.vue';
 import * as os from '@/os';
+import { updateColumn } from './deck-store';
 
 export default defineComponent({
 	components: {
@@ -73,8 +74,9 @@ export default defineComponent({
 				showCancelButton: true
 			});
 			if (canceled) return;
-			this.column.listId = list.id;
-			this.$store.commit('deviceUser/updateDeckColumn', this.column);
+			updateColumn(this.column.id, {
+				listId: list.id
+			});
 		},
 
 		focus() {
diff --git a/src/client/ui/deck/notifications-column.vue b/src/client/ui/deck/notifications-column.vue
index a6281a766a..8718695905 100644
--- a/src/client/ui/deck/notifications-column.vue
+++ b/src/client/ui/deck/notifications-column.vue
@@ -13,6 +13,7 @@ import { faBell } from '@fortawesome/free-regular-svg-icons';
 import XColumn from './column.vue';
 import XNotifications from '@/components/notifications.vue';
 import * as os from '@/os';
+import { updateColumn } from './deck-store';
 
 export default defineComponent({
 	components: {
@@ -48,8 +49,7 @@ export default defineComponent({
 				}, {
 					done: async (res) => {
 						const { includingTypes } = res;
-						this.$store.commit('deviceUser/updateDeckColumn', {
-							...this.column,
+						updateColumn(this.column.id, {
 							includingTypes: includingTypes
 						});
 					},
diff --git a/src/client/ui/deck/tl-column.vue b/src/client/ui/deck/tl-column.vue
index de95857992..6b05f21c33 100644
--- a/src/client/ui/deck/tl-column.vue
+++ b/src/client/ui/deck/tl-column.vue
@@ -25,6 +25,7 @@ import { faMinusCircle, faHome, faComments, faShareAlt, faGlobe, faCog } from '@
 import XColumn from './column.vue';
 import XTimeline from '@/components/timeline.vue';
 import * as os from '@/os';
+import { removeColumn, updateColumn } from './deck-store';
 
 export default defineComponent({
 	components: {
@@ -71,9 +72,9 @@ export default defineComponent({
 		if (this.column.tl == null) {
 			this.setType();
 		} else {
-			this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && (
-				this.$store.state.instance.meta.disableLocalTimeline && ['local', 'social'].includes(this.column.tl) ||
-				this.$store.state.instance.meta.disableGlobalTimeline && ['global'].includes(this.column.tl));
+			this.disabled = !this.$i.isModerator && !this.$i.isAdmin && (
+				this.$instance.disableLocalTimeline && ['local', 'social'].includes(this.column.tl) ||
+				this.$instance.disableGlobalTimeline && ['global'].includes(this.column.tl));
 		}
 	},
 
@@ -96,12 +97,13 @@ export default defineComponent({
 			});
 			if (canceled) {
 				if (this.column.tl == null) {
-					this.$store.commit('deviceUser/removeDeckColumn', this.column.id);
+					removeColumn(this.column.id);
 				}
 				return;
 			}
-			this.column.tl = src;
-			this.$store.commit('deviceUser/updateDeckColumn', this.column);
+			updateColumn(this.column.id, {
+				tl: src
+			});
 		},
 
 		queueUpdated(q) {
diff --git a/src/client/ui/deck/widgets-column.vue b/src/client/ui/deck/widgets-column.vue
index f3ad5ab716..bb898d9b2e 100644
--- a/src/client/ui/deck/widgets-column.vue
+++ b/src/client/ui/deck/widgets-column.vue
@@ -20,12 +20,12 @@
 				<template #item="{element}">
 					<div class="customize-container" @click="widgetFunc(element.id)">
 						<button class="remove _button" @click.prevent.stop="removeWidget(element)"><Fa :icon="faTimes"/></button>
-						<component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" :column="column"/>
+						<component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" :column="column" @updateProps="saveWidget(element.id, $event)"/>
 					</div>
 				</template>
 			</XDraggable>
 		</template>
-		<component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :column="column"/>
+		<component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :column="column" @updateProps="saveWidget(element.id, $event)"/>
 	</div>
 </XColumn>
 </template>
@@ -38,6 +38,7 @@ import MkSelect from '@/components/ui/select.vue';
 import MkButton from '@/components/ui/button.vue';
 import XColumn from './column.vue';
 import { widgets } from '../../widgets';
+import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
 
 export default defineComponent({
 	components: {
@@ -75,10 +76,7 @@ export default defineComponent({
 				return this.column.widgets;
 			},
 			set(value) {
-				this.$store.commit('deviceUser/setDeckWidgets', {
-					id: this.column.id,
-					widgets: value
-				});
+				setColumnWidgets(this.column.id, value);
 			}
 		}
 	},
@@ -101,24 +99,22 @@ export default defineComponent({
 		addWidget() {
 			if (this.widgetAdderSelected == null) return;
 
-			this.$store.commit('deviceUser/addDeckWidget', {
-				id: this.column.id,
-				widget: {
-					name: this.widgetAdderSelected,
-					id: uuid(),
-					data: {}
-				}
+			addColumnWidget(this.column.id, {
+				name: this.widgetAdderSelected,
+				id: uuid(),
+				data: {}
 			});
 
 			this.widgetAdderSelected = null;
 		},
 
 		removeWidget(widget) {
-			this.$store.commit('deviceUser/removeDeckWidget', {
-				id: this.column.id,
-				widget
-			});
+			removeColumnWidget(this.column.id, widget);
 		},
+
+		saveWidget(id, data) {
+			updateColumnWidget(this.column.id, id, data);
+		}
 	}
 });
 </script>
diff --git a/src/client/ui/default.vue b/src/client/ui/default.vue
index c065b20a3a..32a5e99593 100644
--- a/src/client/ui/default.vue
+++ b/src/client/ui/default.vue
@@ -9,7 +9,7 @@
 		<main ref="main">
 			<div class="content">
 				<router-view v-slot="{ Component }">
-					<transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
+					<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
 						<keep-alive :include="['timeline']">
 							<component :is="Component" :ref="changePage"/>
 						</keep-alive>
@@ -31,7 +31,7 @@
 		<button class="button nav _button" @click="showNav" ref="navButton"><Fa :icon="faBars"/><i v-if="navIndicated"><Fa :icon="faCircle"/></i></button>
 		<button v-if="$route.name === 'index'" class="button home _button" @click="top()"><Fa :icon="faHome"/></button>
 		<button v-else class="button home _button" @click="$router.push('/')"><Fa :icon="faHome"/></button>
-		<button class="button notifications _button" @click="$router.push('/my/notifications')"><Fa :icon="faBell"/><i v-if="$store.state.i.hasUnreadNotification"><Fa :icon="faCircle"/></i></button>
+		<button class="button notifications _button" @click="$router.push('/my/notifications')"><Fa :icon="faBell"/><i v-if="$i.hasUnreadNotification"><Fa :icon="faCircle"/></i></button>
 		<button class="button widget _button" @click="widgetsShowing = true"><Fa :icon="faLayerGroup"/></button>
 	</div>
 
@@ -66,6 +66,7 @@ import XHeader from './_common_/header.vue';
 import XSide from './default.side.vue';
 import * as os from '@/os';
 import { sidebarDef } from '@/sidebar';
+import { ColdDeviceStorage } from '@/store';
 
 const DESKTOP_THRESHOLD = 1100;
 
@@ -104,8 +105,8 @@ export default defineComponent({
 		keymap(): any {
 			return {
 				'd': () => {
-					if (this.$store.state.device.syncDeviceDarkMode) return;
-					this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode });
+					if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
+					this.$store.set('darkMode', !this.$store.state.darkMode);
 				},
 				'p': os.post,
 				'n': os.post,
@@ -114,14 +115,6 @@ export default defineComponent({
 			};
 		},
 
-		widgets(): any {
-			return this.$store.state.deviceUser.widgets;
-		},
-
-		menu(): string[] {
-			return this.$store.state.deviceUser.menu;
-		},
-
 		navIndicated(): boolean {
 			for (const def in this.menuDef) {
 				if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
@@ -140,8 +133,8 @@ export default defineComponent({
 	created() {
 		document.documentElement.style.overflowY = 'scroll';
 
-		if (this.$store.state.deviceUser.widgets.length === 0) {
-			this.$store.commit('deviceUser/setWidgets', [{
+		if (this.$store.state.widgets.length === 0) {
+			this.$store.set('widgets', [{
 				name: 'calendar',
 				id: 'a', place: 'right', data: {}
 			}, {
diff --git a/src/client/ui/default.widgets.vue b/src/client/ui/default.widgets.vue
index 7f6fdbc102..b5b3ae83b5 100644
--- a/src/client/ui/default.widgets.vue
+++ b/src/client/ui/default.widgets.vue
@@ -15,7 +15,7 @@
 						<span class="handle"><Fa :icon="faBars"/></span>{{ $t('_widgets.' + element.name) }}<button class="remove _button" @click="removeWidget(element)"><Fa :icon="faTimes"/></button>
 					</header>
 					<div @click="widgetFunc(element.id)">
-						<component class="_inContainer_ _forceContainerFull_" :is="`mkw-${element.name}`" :widget="element" :ref="element.id" :setting-callback="setting => settings[element.id] = setting"/>
+						<component class="_inContainer_ _forceContainerFull_" :is="`mkw-${element.name}`" :widget="element" :ref="element.id" :setting-callback="setting => settings[element.id] = setting" @updateProps="saveWidget(element.id, $event)"/>
 					</div>
 				</div>
 			</template>
@@ -23,7 +23,7 @@
 		<button @click="editMode = false" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faCheck"/> {{ $t('editWidgetsExit') }}</button>
 	</template>
 	<template v-else>
-		<component v-for="widget in widgets" class="_inContainer_ _forceContainerFull_" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget"/>
+		<component v-for="widget in widgets" class="_inContainer_ _forceContainerFull_" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" @updateProps="saveWidget(element.id, $event)"/>
 		<button @click="editMode = true" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $t('editWidgets') }}</button>
 	</template>
 </div>
@@ -56,10 +56,10 @@ export default defineComponent({
 	computed: {
 		widgets: {
 			get() {
-				return this.$store.state.deviceUser.widgets;
+				return this.$store.reactiveState.widgets.value;
 			},
 			set(value) {
-				this.$store.commit('deviceUser/setWidgets', value);
+				this.$store.set('widgets', value);
 			}
 		},
 	},
@@ -87,20 +87,23 @@ export default defineComponent({
 			});
 			if (canceled) return;
 
-			this.$store.commit('deviceUser/addWidget', {
+			this.$store.set('widgets', [...this.$store.state.widgets, {
 				name: widget,
 				id: uuid(),
 				place: null,
 				data: {}
-			});
+			}]);
 		},
 
 		removeWidget(widget) {
-			this.$store.commit('deviceUser/removeWidget', widget);
+			this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id != widget.id));
 		},
 
-		saveHome() {
-			this.$store.commit('deviceUser/setWidgets', this.widgets);
+		saveWidget(id, data) {
+			this.$store.set('widgets', this.$store.state.widgets.map(w => w.id === id ? {
+				...w,
+				data: data
+			} : w));
 		}
 	}
 });
diff --git a/src/client/ui/desktop.vue b/src/client/ui/desktop.vue
index 6c5159e83f..7c5824c4c0 100644
--- a/src/client/ui/desktop.vue
+++ b/src/client/ui/desktop.vue
@@ -14,6 +14,7 @@ import XCommon from './_common_/common.vue';
 import * as os from '@/os';
 import XSidebar from '@/components/sidebar.vue';
 import { sidebarDef } from '@/sidebar';
+import { ColdDeviceStorage } from '@/store';
 
 export default defineComponent({
 	components: {
@@ -33,8 +34,8 @@ export default defineComponent({
 		keymap(): any {
 			return {
 				'd': () => {
-					if (this.$store.state.device.syncDeviceDarkMode) return;
-					this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode });
+					if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
+					this.$store.set('darkMode', !this.$store.state.darkMode);
 				},
 				'p': os.post,
 				'n': os.post,
@@ -44,7 +45,7 @@ export default defineComponent({
 		},
 
 		menu(): string[] {
-			return this.$store.state.deviceUser.menu;
+			return this.$store.state.menu;
 		},
 	},
 
diff --git a/src/client/ui/visitor/a.vue b/src/client/ui/visitor/a.vue
index 94866fdff5..6f8c5efd6b 100644
--- a/src/client/ui/visitor/a.vue
+++ b/src/client/ui/visitor/a.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-app">
-	<div class="banner" v-if="$route.path === '/'" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
+	<div class="banner" v-if="$route.path === '/'" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
 		<div>
 			<h1 v-if="meta"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
 			<div class="about" v-if="meta">
@@ -12,7 +12,7 @@
 			</div>
 		</div>
 	</div>
-	<div class="banner-mini" v-else :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
+	<div class="banner-mini" v-else :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
 		<div>
 			<h1 v-if="meta"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
 		</div>
@@ -25,7 +25,7 @@
 			</header>
 			<main ref="main">
 				<router-view v-slot="{ Component }">
-					<transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
+					<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
 						<component :is="Component" :ref="changePage"/>
 					</transition>
 				</router-view>
@@ -48,6 +48,7 @@ import * as os from '@/os';
 import MkPagination from '@/components/ui/pagination.vue';
 import MkButton from '@/components/ui/button.vue';
 import XHeader from './header.vue';
+import { ColdDeviceStorage } from '@/store';
 
 const DESKTOP_THRESHOLD = 1100;
 
@@ -78,8 +79,8 @@ export default defineComponent({
 		keymap(): any {
 			return {
 				'd': () => {
-					if (this.$store.state.device.syncDeviceDarkMode) return;
-					this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode });
+					if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
+					this.$store.set('darkMode', !this.$store.state.darkMode);
 				},
 				's': search,
 				'h|/': this.help
diff --git a/src/client/ui/visitor/b.vue b/src/client/ui/visitor/b.vue
index dde02c2b11..94d503c513 100644
--- a/src/client/ui/visitor/b.vue
+++ b/src/client/ui/visitor/b.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-app" :style="{ backgroundImage: root ? `url(${ $store.state.instance.meta.backgroundImageUrl })` : 'none' }">
+<div class="mk-app" :style="{ backgroundImage: root ? `url(${ $instance.backgroundImageUrl })` : 'none' }">
 	<a v-if="root" href="https://github.com/syuilo/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
 
 	<div class="side" v-if="!narrow">
@@ -13,7 +13,7 @@
 			<XHeader class="header" :info="pageInfo" v-if="!root"/>
 			<main>
 				<router-view v-slot="{ Component }">
-					<transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
+					<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
 						<component :is="Component" :ref="changePage"/>
 					</transition>
 				</router-view>
@@ -60,6 +60,7 @@ import XSignupDialog from '@/components/signup-dialog.vue';
 import MkButton from '@/components/ui/button.vue';
 import XHeader from './header.vue';
 import XKanban from './kanban.vue';
+import { ColdDeviceStorage } from '@/store';
 
 const DESKTOP_THRESHOLD = 1100;
 
@@ -92,8 +93,8 @@ export default defineComponent({
 		keymap(): any {
 			return {
 				'd': () => {
-					if (this.$store.state.device.syncDeviceDarkMode) return;
-					this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode });
+					if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
+					this.$store.set('darkMode', !this.$store.state.darkMode);
 				},
 				's': search,
 				'h|/': this.help
diff --git a/src/client/ui/visitor/kanban.vue b/src/client/ui/visitor/kanban.vue
index f35a2d017c..6816589d25 100644
--- a/src/client/ui/visitor/kanban.vue
+++ b/src/client/ui/visitor/kanban.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="rwqkcmrc" :style="{ backgroundImage: transparent ? 'none' : `url(${ $store.state.instance.meta.backgroundImageUrl })` }">
+<div class="rwqkcmrc" :style="{ backgroundImage: transparent ? 'none' : `url(${ $instance.backgroundImageUrl })` }">
 	<div class="back" :class="{ transparent }"></div>
 	<div class="contents">
 		<div class="wrapper">
diff --git a/src/client/ui/zen.vue b/src/client/ui/zen.vue
index e76b74d4bf..1c1334bece 100644
--- a/src/client/ui/zen.vue
+++ b/src/client/ui/zen.vue
@@ -7,7 +7,7 @@
 		<main ref="main">
 			<div class="content">
 				<router-view v-slot="{ Component }">
-					<transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
+					<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
 						<keep-alive :include="['timeline']">
 							<component :is="Component" :ref="changePage"/>
 						</keep-alive>
@@ -30,6 +30,7 @@ import { search } from '@/scripts/search';
 import XHeader from './_common_/header.vue';
 import XCommon from './_common_/common.vue';
 import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
 
 export default defineComponent({
 	components: {
@@ -50,8 +51,8 @@ export default defineComponent({
 		keymap(): any {
 			return {
 				'd': () => {
-					if (this.$store.state.device.syncDeviceDarkMode) return;
-					this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode });
+					if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
+					this.$store.set('darkMode', !this.$store.state.darkMode);
 				},
 				'p': os.post,
 				'n': os.post,
diff --git a/src/client/widgets/activity.vue b/src/client/widgets/activity.vue
index 8db13723ec..334526bd2a 100644
--- a/src/client/widgets/activity.vue
+++ b/src/client/widgets/activity.vue
@@ -57,7 +57,7 @@ export default defineComponent({
 	},
 	mounted() {
 		os.api('charts/user/notes', {
-			userId: this.$store.state.i.id,
+			userId: this.$i.id,
 			span: 'day',
 			limit: 7 * 21
 		}).then(activity => {
diff --git a/src/client/widgets/define.ts b/src/client/widgets/define.ts
index c199d38e72..b5498204b3 100644
--- a/src/client/widgets/define.ts
+++ b/src/client/widgets/define.ts
@@ -17,6 +17,8 @@ export default function <T extends Form>(data: {
 			}
 		},
 
+		emits: ['updateProps'],
+
 		data() {
 			return {
 				props: this.widget ? JSON.parse(JSON.stringify(this.widget.data)) : {}
@@ -66,12 +68,7 @@ export default function <T extends Form>(data: {
 			},
 
 			save() {
-				if (this.widget) {
-					this.$store.commit('deviceUser/updateWidget', {
-						...this.widget,
-						data: this.props
-					});
-				}
+				this.$emit('updateProps', this.props);
 			}
 		}
 	});
diff --git a/src/client/widgets/memo.vue b/src/client/widgets/memo.vue
index 8b14d61c73..9cde149f4f 100644
--- a/src/client/widgets/memo.vue
+++ b/src/client/widgets/memo.vue
@@ -42,9 +42,9 @@ export default defineComponent({
 	},
 
 	created() {
-		this.text = this.$store.state.settings.memo;
+		this.text = this.$store.state.memo;
 
-		this.$watch(() => this.$store.state.settings.memo, text => {
+		this.$watch(() => this.$store.reactiveState.memo, text => {
 			this.text = text;
 		});
 	},
@@ -57,10 +57,7 @@ export default defineComponent({
 		},
 
 		saveMemo() {
-			this.$store.dispatch('settings/set', {
-				key: 'memo',
-				value: this.text
-			});
+			this.$store.set('memo', this.text);
 			this.changed = false;
 		}
 	}
diff --git a/src/client/widgets/photos.vue b/src/client/widgets/photos.vue
index 2fa0a48574..26b4487ad1 100644
--- a/src/client/widgets/photos.vue
+++ b/src/client/widgets/photos.vue
@@ -74,7 +74,7 @@ export default defineComponent({
 		},
 
 		thumbnail(image: any): string {
-			return this.$store.state.device.disableShowingAnimatedImages
+			return this.$store.state.disableShowingAnimatedImages
 				? getStaticImageUrl(image.thumbnailUrl)
 				: image.thumbnailUrl;
 		},
diff --git a/yarn.lock b/yarn.lock
index b180ec6611..f1037abb62 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3119,7 +3119,7 @@ deep-is@^0.1.3, deep-is@~0.1.3:
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
 
-deepmerge@^4.0.0, deepmerge@^4.2.2:
+deepmerge@^4.0.0:
   version "4.2.2"
   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
   integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
@@ -8775,11 +8775,6 @@ shebang-regex@^3.0.0:
   resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
   integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 
-shvl@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/shvl/-/shvl-2.0.0.tgz#55fd550b6e81bf7574f2f576b8b5c1ffae74e10f"
-  integrity sha512-WbpzSvI5XgVGJ3A4ySGe8hBxj0JgJktfnoLhhJmvITDdK21WPVWwgG8GPlYEh4xqdti3Ff7PJ5G0QrRAjNS0Ig==
-
 sigmund@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
@@ -10253,19 +10248,6 @@ vuedraggable@4.0.1:
   dependencies:
     sortablejs "1.10.2"
 
-vuex-persistedstate@3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-3.1.0.tgz#a710d01000bff8336bc3b03fa3ef42e376094b71"
-  integrity sha512-nRiCe1qDdDrcveFQzTw0QGEj3dRpwN19BailSSwfhe4eUNNQ+9S/ApKnDEAuyw95cigOtSPciMEhdsC0qNUiKQ==
-  dependencies:
-    deepmerge "^4.2.2"
-    shvl "^2.0.0"
-
-vuex@4.0.0-rc.2:
-  version "4.0.0-rc.2"
-  resolved "https://registry.yarnpkg.com/vuex/-/vuex-4.0.0-rc.2.tgz#3681c84eb6f5171b039edaa17cc78105e20724f3"
-  integrity sha512-HCPzYGea1xL7fMpDoMiHKujC1Bi/HM9LS5ML0Kv55zQtZJvOl0Lq7eWvJoen+SI4Lf7p9V5AqcVsoLPXNBywjg==
-
 w3c-hr-time@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"