-
Notifications
You must be signed in to change notification settings - Fork 309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
vuexをやめてpiniaにする #1004
Comments
幻?のVuex 5があって、それとほぼ同じなのがPiniaなんですね。 ざっと眺めた感じ大変になりそうだったのは次の2点です。
Fluxではなくなるらしく、mutationが消えると同時にdispatchも消える(ただの関数になる)のですが、まあこの影響はあまりない気がします。むしろ楽になりそう。 ちなみに、複数のstoreがお互いのstateを見るのってどうやって実装すればいいかってご存知だったりしますか・・・? @sousuke0422 |
どちらかというと前向きに検討したいと思ってる理由をメリデメとして列挙してみます。
TypeScriptがちゃんとサポートされてること、順番を気にせず書けることが嬉しいポイントです。 |
ルールが一応見つかりました・・・ |
おーーー!!Cookbookの方にあったんですね、見落としてました。 |
piniaでは同期的にデータの整合性を保つためのmutationに相当する方法として$patchがあるようです。 懸念としては、
逆にストアを分割すればMUTEXを細かく設定して、コマンドを依存ごとに並列に実行するなどもできるかもしれません。 |
seguさんありがとうございます!!
こちらの懸念については、微妙な策かもですがundo/redoしたい全stateだけを持つstoreを作るとかで解決できるかもと思いました! |
piniaについてちょっと調べてみたのですが、メリットに上げている |
前のコメントで
と書きましたがpiniaは同期的にstateを更新できるようなので、非同期的なコードを挟まない限り整合性は保証されそうでした。 |
piniaへの移行、現実的なのかどうか若干見積もれないですね…。 ちょっとずつ移行する方法も思いつかない。うーん。 |
undo/redoも含め、piniaを用いたときの設計を考えてました!! vuexからの移行を考えると、fluxじゃなくなる点も考慮ポイントっぽかったです。 コマンドに関してはstoreを跨ぐときのことを考える必要がありそうです。 ↓なんとなくこんな感じかなというコードを書いてみました。(文法正しくないので動かないです) const commandStore = (otherStores: Store[]) => {
const redoCommands = Command[];
const undoCommands = Command[];
const createCommand = (stores: Store[], mutation: Mutation) => {
// どういう実装?
// immerのcreateDraftを作る場合(できる?)
return () => {
const drafts = stores.map((store) => immer.createDraft(store.state));
mutation(...drafts);
const redoPatches1, undoPatchs1 = immer.finishDraft(drafts[0]);
const redoPatches2, undoPatchs2 = immer.finishDraft(drafts[1]);
redoCommands.push(Command(redoPatches1 + redoPatches2)); // まとめれる?
undoCommands.push(Command(undoPatchs1 + undoPatchs2));
store1.$patch(() => {apply(redoPatches1)})
store2.$patch(() => {apply(redoPatches2)})
};
// パッチを分解して各Storeに渡す場合
return () => {
const redoPatches, undoPatchs = immer.produceWithPatches(mutation);
redoCommands.push(Command(redoPatches));
undoCommands.push(Command(undoPatchs));
const redoPatches1 = filter(redoPatches)
store1.$patch(() => {apply(redoPatches1)})
const redoPatches2 = filter(redoPatches)
store2.$patch(() => {apply(redoPatches2)})
};
}
return {
createCommand,
}
}
const store1 = (storeName: string) => {
const commandStore = useCommandStore();
const getters = {}; // ちゃんと型つく?
const actions = {}; // ちゃんと型つく?
const state = reactive({
count: 0,
})
const mutationA = (state) => {state.count++};
actions.commandActionS = commandStore.createCommand(this, (draft) => { draft.count++ });
actions.commandActionT = commandStore.createCommand(this, (draft) => { mutationA(draft) });
// コードジャンプはちゃんとここになる?
actions.actionX = () => {this.$patch(() => state.count++ );};
actions.actionY = () => {this.$patch((state) => mutationA(state));};
getters.getterX = computed(() => state.count * 10); // WritableComputedは禁止
return {
...readonly(state), // ちゃんとreactiveに解体できる?
...getters,
...actions,
}
}
const store2 = (storeName2: string) => {
const commandStore = useCommandStore();
const store1 = useStore1();
const state = reactive({
count2: 0,
})
actions.commandActionU = createCommand(
[this, store1],
(draft, draft1) => {draft.count2++; draft1.count++;},
);
return {
...readonly(state),
...actions,
}
} メモ
|
@Hiroshiba 解決策としては
composition styleは |
@Segu-g あ~~~ なるほどです!!
他の手としては、将来 function patchFactory<S>(s: S) {
return (func: (s: S) => void) => {
func(s)
}
}
const $patch = patchFactory(state) 次手として |
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { defineCommand } from './command';
export const useText = defineStore('text', () => {
const text = ref('');
return {
text,
};
});
export const useTextCommand = defineStore('textCommand', () => {
const textStore = useText();
const changeText = defineCommand({ textStore }, ({ textStore }, text) => {
textStore.text += text;
});
return { changeText };
}); |
@Segu-g おー!!!!!
1~3はなんとかなりそうなのですが、4だけは解決策を思いつかないでいます。。 |
const commandStore = useCommand();
const commandA = defineCommand(commandStore, ()=>{}) のようにするのも冗長かなと感じたので、 const { defineCommand } = useCommandContext(); と書けるくらいのヘルパー関数は書いてもいいと思うのですがどうでしょうか?
undo, redoの時はcommandから各ストアの const getUseStoreArr = () => [useCounter, useText];
プロパティを(型的に)Readonlyにすること自体は // storeHelper.ts
import { StoreDefinition } from 'pinia';
import type { DeepReadonly } from 'ts-essentials';
type ToReadonlyStoreDefinition<SD> = SD extends StoreDefinition<
infer Id,
infer S,
infer G,
infer A
>
? StoreDefinition<Id, DeepReadonly<S>, G, A>
: SD;
export function toReadonlyStoreDefinition<SD>(useStore: SD) {
return useStore as ToReadonlyStoreDefinition<SD>;
}
// index.ts
import { useCounter as _useCounter } from './countStore';
import { useText as _useText } from './textStore';
import { toReadonlyStoreDefinition } from './storeHelper';
export const useCounter = toReadonlyStoreDefinition(_useCounter);
export const useText = toReadonlyStoreDefinition(_useText); 今気づいたのですが今のコードはmutationの型が PS Draftを外したところちゃんとReadonlyなStoreはmutation内でも操作できないことを確認しました。 |
なるほどコンポーザブルなんですね!
stateの方のstoreだけ型を置き換えたりラップしたりするのなるほどです!! (ちょっと別の方法としては、今stateを持っている方のStoreを |
なんかpiniaに置き換えること自体は通れる道な気がしました!!
1と2に問題があるかどうかをいろんな人に聞いてみるフェーズかなと思いました!! |
ちゃんと検証は済んでないですが1の問題は多分大丈夫だと思います。 javascriptだとその言語的な特性から(piniaがWorkerとか建ててなければですが)実行されるスレッドは常に1つで、 そもそもvuexでcreateCommandAcitonが上手くいかない原因がcommitしたpatchが適応されるタイミングが非同期であることだったので、$patchで同期的に状態が書き換えられるpiniaでは問題にならないでしょう。 |
なるほどです!!! ということはまあ以前と一緒の使い勝手になる感じですかね・・・!! |
そもそもスプレッドするために付けなくちゃいけない return { commandChangeText, ...storeToRefs(textStore) }; |
storeToRefs良いですね!! |
ルールを整備したりしないと結構危うい気がしてきたのでちょっと考えてみました!
で、実装としての候補はこんな感じ・・・?
個人的にはとりあえず1でいいのかなとかちょっと思ってます。 |
上記のコメントを踏まえて少しサンプルを書いてみました。 方針としてはstateを持つstoreは$patch利用のために export const usePresetStore = defineStore("preset", () => {
const { state, defMut, defAct } = useStoreAsState(usePresetState);
// getter
const defaultPresetKeySets = computed(() => {
return new Set(Object.values(state.defaultPresetKeys));
});
// action
const setDefaultPresetMap = defAct(
async ({
defaultPresetKeys,
}: {
defaultPresetKeys: Record<VoiceId, PresetKey>;
}) => {
window.electron.setSetting("defaultPresetKeys", defaultPresetKeys);
setDefaultPresetMapMut.act({ defaultPresetKeys });
}
);
// mutation
const setDefaultPresetMapMut = defMut(
(
state,
{ defaultPresetKeys }: { defaultPresetKeys: Record<VoiceId, PresetKey> }
) => {
state.defaultPresetKeys = defaultPresetKeys;
}
);
return { ...storeToRefs(state) , defaultPresetKeySets, setDefaultPresetMap };
}): |
また別の話になりますが これは
const changeText = defineCommand({ textStore, hogeStore }, ({ textStore, hogeStore }, text) => {
textStore.text += text;
hogeStore.count += 1;
}); |
ちょっといろいろ考えたのですが全然まとまってないけど一旦ちょっとコメントだけ・・・!
アイデア1今のVuexラッパーみたいにして書いて、pinia用のobjectにする functionName: {
mutation: (state) => {},
action: ( {state, actions, mutations} ) => {}
} アイデア2mutをstateStore側に書く。 アイデア3module直下にmutationを書き、他stateも渡すようにする アイデア4prefixかsuffixに |
別のアイデアが思い浮かんだのでメモ。 アイデア5piniaのdefine関数内でmutationやactionを定義するとき、専用の型を使うようにする。 ESLintで使用禁止や外部変数の利用禁止するのはChatGPTくんに聞いた感じできるかも・・・? |
@Segu-g |
@Segu-g pinia化、まだ記憶が残っているうちに進めておかないと多分もったいないので、ちょっと危機感を感じ始めました・・・! |
ESLintの設定を頑張ろうとしてみてたのですが、動かす方法がよくわかりませんでした・・・。 |
@Hiroshiba とりあえず書いてみたものがこちらになります stateを定義するときは import { defineStore, storeToRefs } from 'pinia';
import { defineCommandableState } from './command';
import { useStore } from '@/vuex-store';
export const CountState = defineCommandableState({
id: 'count/state',
state: () => ({
counter: 0,
}),
});
export const useCount = defineStore('count', () => {
const { state, defMut, asCmd } = CountState.useContext();
const increment = defMut((state) => {
state.counter += 1;
});
const countUpWithVuex = () => {
const vuexStore = useStore();
vuexStore.commit('increment');
};
return {
state: storeToRefs(state),
commandIncrement: asCmd(increment),
countUpWithVuex,
};
}); このブランチでは |
インターフェースを改良しました. export const TextState = defineCommandableState({
id: 'text/state',
state: () => ({
text: '',
name: '',
}),
});
export const useText = defineStore('text', () => {
const { state, defGet, defMut, asCmd } = TextState.useContext();
// mutation
const changeTextMut = defMut((state, text: string) => {
state.text = text;
});
const changeNameMut = defMut((state, text: string) => {
state.name = text;
});
// action
const changeTextAndName = (text: string) => {
changeNameMut.commit(text);
changeNameMut.commit(text);
};
// command
const commandChangeText = asCmd(changeTextMut);
// getter
const textGet = defGet((state) => state.text);
const isTextSameToName = defGet((state) => state.name == textGet(state));
return {
state: storeToRefs(state),
changeTextMut,
changeNameMut,
changeTextAndName,
commandChangeText,
textGet,
isTextSameToName,
};
}); |
ありがとうございます!!!!!!すごく勉強になりました!!! defCmdとasCmdがありますが、defCmdは結構リッチなのとasCmdでの書き換えが割と想像しやすいので、シンプルにasCmdの方だけでもいいかもと思いました! あとdefGetは他のstoreのstateやgetterを使えることがわかるように、stateを与えなくても良いようにできるかも? mutationの中でaction(呼んではいけない関数)を実行しているのかどうかが分かりにくいかもと思いました。 案
|
pinia化の流れですが、audio.tsの置き換えはコマンドを一気に置き換えないといけない、つまり3000行にわたる全てのコードの置き換えを一気にやる必要がある気がしています。 この点で一番困りそうなのが、API構成周りが現状のコードと合いそうかどうかという点です。 そこで提案なのですが、1回実際に一部のコマンドを実装してみて、それをプルリクエストにしていただいてレビューし、そこが固まってから コードを読んでいましたが、コマンドの中だとAudioItemを足す Line 1850 in 37d52de
この2つだけとりあえず実装してみてプルリクを出していただく・・・という進め方はいかがでしょうか・・・!! @Segu-g |
#1666 にてデモ実装をしてみました。
前との差分としては
です |
おおお、ありがとうございます!!!!!
素晴らしいと思います!!!! |
ESLintで独自の型チェックを細かくする方法が分からなかったのですが、こちらに頂いたプルリクエストがとても参考になると思うのでメモです!! |
内容
vuexをやめてpiniaに移行します。
vuexはメンテナンスモードへなったそうです。
Pros 良くなる点
typescriptとの相性が良くなり開発体験が良くなる。
Cons 悪くなる点
そこそこ大きな改修が必要。
実現方法
VOICEVOXのバージョン
0.?.0
OSの種類/ディストリ/バージョン
その他
The text was updated successfully, but these errors were encountered: