blog

Vue Composition APIの落とし穴

React Hooksの登場以来、批判も多く、従来のClassの書き方とは異なり、実行順序に依存するため、かなりわかりにくく、精神的に負担がかかるという声も多く聞かれます。これに対して、Vue3のAP...

Oct 29, 2020 · 7 min. read
シェア

序文

React Hooksの登場以来、従来のClassの書き方と比較すると、useState/useEffectの実行順序依存性が分かりにくいため、精神的に負担がかかるという批判が多くありました。これに対して、 見ると、Vue3はCompositionAPIを既存の「レスポンシブ」なメンタルモデルに基づくより良いソリューションとして公式に記述しており、メンタルモデルの切り替えなしにすぐにCompositoin API開発に飛び込めるように思えました。しかし、しばらく試してみたところ、そうではなく、新しいComposition APIに適応するためには、まだいくつかの考え方の変更が必要であることがわかりました。

Setup

シンプルな罠

簡単なVue2の例から始めましょう:

<template>
 <div id="app">
 {{count}}
 <button @click="addCount"></button>
 </div>
</template>
<script>
export default {
 data() {
 return {
 count: 0
 }
 },
 methods: {
 addCount() {
 this.count += 1
 }
 }
};
</script>

Vue2のメンタルモデルでは、オブジェクトは常にデータで返され、オブジェクトの値が単純型であろうと参照型であろうと、上の例のようにレスポンシブシステムによってどちらもうまく処理されるため、問題にはなりません。しかし、メンタルモデルを変更せずにComposition APIを使い始めると、このようなコードを書くのは簡単です:

<template>
 <div id="app">
 {{count}}
 <button @click="addCount"></button>
 </div>
</template>
<script>
import { reactive } from '@vue/runtime-dom'
export default {
 setup() {
 const data = reactive({
 count: 0
 })
 function addCount() {
 data.count += 1
 }
 return {
 count: data.count,
 addCount
 }
 }
};
</script>

実際、このコードは正しく動作せず、ボタンをクリックしてもビューはデータの変化に反応しません。その理由は、まずカウントがデータから取り出され、thisにマージされるからです。しかし、一旦カウントが取り出されると、単なるデータ型になってしまい、応答性が失われてしまいます。

複雑性の罠

データ構造が複雑であればあるほど、ビジネスロジックの一部が以下のようにカスタムフックに抽象化されるという罠に陥りやすくなります:

// useSomeData.js import { reactive, onMounted } from '@vue/runtime-dom' export default function useSomeData() { const data = reactive({ userInfo: { name: 'default_name', role: 'default_role' }, projectList: [] }) onMounted(() => { // 非同期にデータを取得する fetch(...).then(result => { const { userInfo, projectList } = result data.userInfo = userInfo data.projectList = projectList }) }) return data }

そして、通常通りビジネス・コンポーネントで使ってください:

// App.vue
<template>
 <div>
 {{name}}
 {{role}}
 {{list}}
 </div>
</template>
<script>
import useSomeData from './useSomeData'
export default {
 setup() {
 const { userInfo, projectList } = useSomeData()
 return {
 name: userInfo.name // レスポンシブブレーク
 role: userInfo.role, // レスポンシブブレーク
 list: projectList // レスポンシブかブロークンか
 }
 }
}
</script>

レスポンシブのデータから取り出されたものは、レスポンシブが壊れ、ビューが更新されなくなります。

これらの問題の根本的な原因は、セットアップが一度しか実行されないことです。

新しいメンタル・モデルへの移行

  1. セットアップは一度しか実行されないことを覚えておいてください。
  2. 単純型を直接使用しないでください。
  3. 脱構築は危険です。
  4. 解体工事を安全に行うには、次のような方法があります。

ニュー・マインド・モデルによる問題解決

単純型の罠:単純型を直接使わない

<template>
 <div id="app">
 {{count}}
 <button @click="addCount"></button>
 </div>
</template>
<script>
import { reactive, ref } from '@vue/runtime-dom'
export default {
 setup() {
 const count = ref(0) // ここでは、参照コンテナをrefで包んでいる。
 function addCount() {
 count.value += 1
 }
 return {
 count,
 addCount
 }
 }
};
</script>

複雑性の罠 - 選択肢1:分解は危険。

// useSomeData.js
...
// App.vue
<template>
 <div>
 {{someData.userInfo.name}}
 {{someData.userInfo.role}}
 {{someData.projectList}}
 </div>
</template>
<script>
import useSomeData from './useSomeData'
export default {
 setup() {
 const someData = useSomeData()
 return {
 someData
 }
 }
}
</script>

複雑性の罠 - 選択肢2:COMPUTEDによって安全な脱構築が可能

// useSomeData.js import { reactive, onMounted, computed } from '@vue/runtime-dom' export default function useSomeData() { const data = reactive({ userInfo: { name: 'default_user', role: 'default_role' }, projectList: [] }) onMounted(() => { // 非同期にデータを取得する fetch(...).then(result => { const { userInfo, projectList } = result data.userInfo = userInfo data.projectList = projectList }) }) const userName = computed(() => data.userInfo.name) const userRole = computed(() => data.userinfo.role) const projectList = computed(() => data.projectList) return { userName, userRole, projectList } }
// App.vue export default { setup() { const { userName, userRole, projectList } = useSomeData() return { name: userName // これはコンピューテッド・プロパティであり、レスポンシブで壊れない。 role: userRole, // これはコンピューテッド・プロパティであり、レスポンシブで壊れない。 list: projectList // これはコンピューテッド・プロパティであり、レスポンシブで壊れない。 } } }

複雑さの罠 - オプション3: オプション2では、余計な計算属性を書く必要があり、より面倒です。

// useSomeData.js import { reactive, onMounted } from '@vue/runtime-dom' export default function useSomeData() { const data = reactive({ userInfo: { name: 'default_user', role: 'default_role' }, projectList: [] }) onMounted(() => { // 非同期にデータを取得する fetch(...).then(result => { const { userInfo, projectList } = result data.userInfo = userInfo data.projectList = projectList }) }) // toRefを使う return toRefs(data) }
// App.vue export default { setup() { // これで、userInfoとprojectListはref.Listのレイヤーに包まれた。 // ラッパーはテンプレートの中で自動的にアンラップされる。 const { userInfo, projectList } = useSomeData() return { name: userInfo.value.name, // ??? role: userInfo.value.role, // ??? list: projectList // ??? } } }

これで十分だと思いますか?実は、罠の中に罠があるのです。projectListは問題ないのですが、toRefが「浅く」ラップするだけなので、nameとroleがレスポンスよく切り離されたままなのです:

const someData = useSomeData() { userInfo: { value: { name: '...', // まだシンプルで、ラップされていない role: '...' // まだシンプルで、ラップされていない } }, projectList: { value: [...] } }

従って、useSomeDataは、toRefによって本当にデコンストラクションセーフにしたいのであれば、このように記述する必要があります:

// useSomeData.js import { reactive, onMounted } from '@vue/runtime-dom' export default function useSomeData() { ... // 各レベルにレフェリーのレイヤーを持たせる return toRefs({ projectList: data.projectList, userInfo: toRefs(data.userInfo) }) }

提案: カスタムフックを使用してデータを返す場合、データのレベルが単純な場合は toRef を直接使用してラップすることができますが、データのレベルが複雑な場合は computed を使用することをお勧めします。

罠を回避する方法

というのも、CompositionAPIはセットアップが一度しか実行されないように設計されているからです。しかし、最終的に分解できないレスポンシブデータに常に注意を払わなければならず、そうでなければ誤って落とし穴にチューニングしてしまいやすいため、精神的な負担が大きいことは否めません。

実際、これらの問題はすべてセットアップが一度しか実行されないことにあります。はい、JSXやhの書き方を使うことで、セットアップが一度しか実行されないという問題を回避することができます:

あるいは、このセキュリティ上危険なカスタムフック:

// useSomeData.js import { reactive, onMounted } from '@vue/runtime-dom' export default function useSomeData() { const data = reactive({ userInfo: { name: 'default_name', role: 'default_role' }, projectList: [] }) onMounted(() => { // 非同期にデータを取得する fetch(...).then(result => { const { userInfo, projectList } = result data.userInfo = userInfo data.projectList = projectList }) }) return data }

JSXを使うか、h

import useSomeData from './useSomeData' export default { setup() { const someData = useSomeData() return () => { const { userInfo: { name, role }, projectList } = someData return ( <div> { name } { role } { projectList } </div> ) } } }

JSXやhの使用では、セットアップは関数を返す必要があり、この関数は実際にはレンダー関数であり、それはデータが変更されたときに再実行されるので、レンダー関数にデコンストラクションのロジックを置くだけで、それはセットアップが一度だけ実行される問題を解決します。

あとがき

カスタムフックの使い方を規定する規約が必要かもしれません。しかし、これは公式には与えられていません。その結果、フックはさまざまな方法で書かれ、さまざまな穴があいてしまうでしょう。今のところ、"don't deconstruct "が最も安全な方法です。

yyxさんに相談したところ、"脱構築 "はなるべく使わないという "お約束 "をいただきました。しょうがないですね。実際、カスタムフックでミスをする可能性を減らす公式ツールがあればいいんですけどね。

Read next

暗号化と署名

DES は対称暗号アルゴリズムであり、鍵長 56bit で 64bit の平文を 64bit の暗号文に暗号化します。 実際には仕様上、鍵長は 64bit ですが、エラーチェックのためのビットが 7bit ごとに設定されるため、実質的な鍵長は 56bit となります。 DES は一度に 64bit のデータしか暗号化できないため、より大きなデータに遭遇した場合は D.C.C. 鍵を変更する必要があります。より大きなデータはD.C.C.で暗号化する必要があります。

Oct 29, 2020 · 12 min read