refactor(frontend): prefer.model, store.modelではcustomRefを使用するように (#17058)

* refactor(frontend): prefer.model, store.modelではcustomRefを使用するように

* fix: watchの解除に失敗してもエラーで落ちないように

* Update packages/frontend/src/lib/pizzax.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
かっこかり 2026-01-02 21:34:43 +09:00 committed by GitHub
parent b5454cb2c4
commit 443e1ed29e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 67 additions and 65 deletions

View File

@ -329,8 +329,8 @@ const canSaveAsServerDraft = computed((): boolean => {
return canPost.value && (textLength.value > 0 || files.value.length > 0 || poll.value != null); return canPost.value && (textLength.value > 0 || files.value.length > 0 || poll.value != null);
}); });
const withHashtags = computed(store.makeGetterSetter('postFormWithHashtags')); const withHashtags = store.model('postFormWithHashtags');
const hashtags = computed(store.makeGetterSetter('postFormHashtags')); const hashtags = store.model('postFormHashtags');
watch(text, () => { watch(text, () => {
checkMissingMention(); checkMissingMention();

View File

@ -7,7 +7,7 @@
// TODO: Misskeyのドメイン知識があるのでutilityなどに移動する // TODO: Misskeyのドメイン知識があるのでutilityなどに移動する
import { onUnmounted, ref, watch } from 'vue'; import { customRef, ref, watch, onScopeDispose } from 'vue';
import { BroadcastChannel } from 'broadcast-channel'; import { BroadcastChannel } from 'broadcast-channel';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
@ -223,44 +223,43 @@ export class Pizzax<T extends StateDef> {
} }
/** /**
* getter/setterを作ります * computed refを作ります
* vue上で設定コントロールのmodelとして使う用 * vue上で設定コントロールのmodelとして使う用
*/ */
// TODO: 廃止 public model<K extends keyof T, R = T[K]['default']>(
public makeGetterSetter<K extends keyof T, R = T[K]['default']>( key: K,
): Ref<R>;
public model<K extends keyof T, R extends Exclude<any, T[K]['default']>>(
key: K,
getter: (v: T[K]['default']) => R,
setter: (v: R) => T[K]['default'],
): Ref<R>;
public model<K extends keyof T, R>(
key: K, key: K,
getter?: (v: T[K]['default']) => R, getter?: (v: T[K]['default']) => R,
setter?: (v: R) => T[K]['default'], setter?: (v: R) => T[K]['default'],
): { ): Ref<R> {
get: () => R; return customRef<R>((track, trigger) => {
set: (value: R) => void; const watchStop = watch(this.r[key], () => {
} { trigger();
const valueRef = ref(this.s[key]);
const stop = watch(this.r[key], val => {
valueRef.value = val;
}); });
// NOTE: vueコンポーネント内で呼ばれない限りは、onUnmounted は無意味なのでメモリリークする onScopeDispose(() => {
onUnmounted(() => { watchStop();
stop(); }, true);
});
// TODO: VueのcustomRef使うと良い感じになるかも
return { return {
get: () => { get: () => {
if (getter) { track();
return getter(valueRef.value); return (getter != null ? getter(this.s[key]) : this.s[key]) as R;
} else {
return valueRef.value;
}
}, },
set: (value) => { set: (value) => {
const val = setter ? setter(value) : value; const val = setter != null ? setter(value) : value;
this.set(key, val); this.set(key, val as T[K]['default']);
valueRef.value = val;
}, },
}; };
});
} }
// localStorage => indexedDBのマイグレーション // localStorage => indexedDBのマイグレーション

View File

@ -78,7 +78,7 @@ const items = ref(prefer.s.menu.map(x => ({
}))); })));
const itemTypeValues = computed(() => items.value.map(x => x.type)); const itemTypeValues = computed(() => items.value.map(x => x.type));
const menuDisplay = computed(store.makeGetterSetter('menuDisplay')); const menuDisplay = store.model('menuDisplay');
const showNavbarSubButtons = prefer.model('showNavbarSubButtons'); const showNavbarSubButtons = prefer.model('showNavbarSubButtons');
async function addItem() { async function addItem() {

View File

@ -855,7 +855,7 @@ const $i = ensureSignin();
const lang = ref(miLocalStorage.getItem('lang')); const lang = ref(miLocalStorage.getItem('lang'));
const dataSaver = ref(prefer.s.dataSaver); const dataSaver = ref(prefer.s.dataSaver);
const realtimeMode = computed(store.makeGetterSetter('realtimeMode')); const realtimeMode = store.model('realtimeMode');
const overridedDeviceKind = prefer.model('overridedDeviceKind'); const overridedDeviceKind = prefer.model('overridedDeviceKind');
const pollingInterval = prefer.model('pollingInterval'); const pollingInterval = prefer.model('pollingInterval');

View File

@ -190,7 +190,7 @@ const $i = ensureSignin();
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
const reactionAcceptance = computed(store.makeGetterSetter('reactionAcceptance')); const reactionAcceptance = store.model('reactionAcceptance');
function assertVaildLang(lang: string | null): lang is keyof typeof langmap { function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
return lang != null && lang in langmap; return lang != null && lang in langmap;

View File

@ -3,11 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { computed, onUnmounted, ref, watch } from 'vue'; import { customRef, ref, watch, onScopeDispose } from 'vue';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import { host, version } from '@@/js/config.js'; import { host, version } from '@@/js/config.js';
import { PREF_DEF } from './def.js'; import { PREF_DEF } from './def.js';
import type { Ref, WritableComputedRef } from 'vue'; import type { Ref } from 'vue';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import { genId } from '@/utility/id.js'; import { genId } from '@/utility/id.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
@ -299,36 +299,39 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
* computed refを作ります * computed refを作ります
* vue上で設定コントロールのmodelとして使う用 * vue上で設定コントロールのmodelとして使う用
*/ */
public model<K extends keyof PREF, V extends ValueOf<K> = ValueOf<K>>( public model<K extends keyof PREF, V = ValueOf<K>>(
key: K,
): Ref<V>;
public model<K extends keyof PREF, V extends Exclude<any, ValueOf<K>>>(
key: K,
getter: (v: ValueOf<K>) => V,
setter: (v: V) => ValueOf<K>,
): Ref<V>;
public model<K extends keyof PREF, V>(
key: K, key: K,
getter?: (v: ValueOf<K>) => V, getter?: (v: ValueOf<K>) => V,
setter?: (v: V) => ValueOf<K>, setter?: (v: V) => ValueOf<K>,
): WritableComputedRef<V> { ): Ref<V> {
const valueRef = ref(this.s[key]); return customRef<V>((track, trigger) => {
const watchStop = watch(this.r[key], () => {
const stop = watch(this.r[key], val => { trigger();
valueRef.value = val;
}); });
// NOTE: vueコンポーネント内で呼ばれない限りは、onUnmounted は無意味なのでメモリリークする onScopeDispose(() => {
onUnmounted(() => { watchStop();
stop(); }, true);
});
// TODO: VueのcustomRef使うと良い感じになるかも return {
return computed({
get: () => { get: () => {
if (getter) { track();
return getter(valueRef.value); return (getter != null ? getter(this.s[key]) : this.s[key]) as V;
} else {
return valueRef.value;
}
}, },
set: (value) => { set: (value) => {
const val = setter ? setter(value) : value; const val = setter != null ? setter(value) : value;
this.commit(key, val); this.commit(key, val as ValueOf<K>);
valueRef.value = val;
}, },
};
}); });
} }

View File

@ -67,7 +67,7 @@ const props = defineProps<{
const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD); const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD);
const menu = ref(prefer.s.menu); const menu = ref(prefer.s.menu);
// const menuDisplay = computed(store.makeGetterSetter('menuDisplay')); // const menuDisplay = store.model('menuDisplay');
const otherNavItemIndicated = computed<boolean>(() => { const otherNavItemIndicated = computed<boolean>(() => {
for (const def in navbarItemDef) { for (const def in navbarItemDef) {
if (menu.value.includes(def)) continue; if (menu.value.includes(def)) continue;