0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

やりがちなVue.jsの間違いと解決方法②

Last updated at Posted at 2024-09-11

①はこちら
https://qiita.com/kaitouEx/items/142ef33a8fce85f1679c

ローカルストレージに保存した値の確認方法

image.png

Google dev tools -> Application tab -> Storage

computed()内ではref()を使うべし

.vue
<script setup lang="ts">
import { computed } from 'vue'

const data = computed(() => {
  return localStorage.getItem('data')
})

function updateData(e: Event) {
  const data = (e.target as HTMLInputElement).value
  localStorage.setItem('data', data || '')
}
</script>

<template>
  <div>
    <input type="text" @input="updateData" :value="data" />
  </div>
  <pre>{{ data }}</pre>
</template>

input欄に適当な文字を入力してもimputの下に入力した値が表示されない。
また、さらにリロードするとさっき入力した値が表示される。

なぜか?

computedはこのメソッド内でリアクティブなデータを追跡するが、localStorage.getItem()はリアクティブではないから。

解決法

  • computed()を消してref()を使う
.vue
<script setup lang="ts">
import { computed,ref} from 'vue'
const reactiveData = ref(localStorage.getItem('data'))

function updateData(e: Event) {
  const data = (e.target as HTMLInputElement).value
  localStorage.setItem('data', data || '')
}
</script>

<template>
  <div>
    <input type="text" @input="updateData" :value="reactiveData" />
  </div>
  <pre>{{ reactiveData }}</pre>
</template>

上記コードで、SSRモードで動かしているとlocalStorage is not definedというエラーが発生してしまうため、その場合は以下に書き換える

const reactiveData = ref('')
onMounted(() => {
  reactiveData.value =localStorage.getItem('data') || ''
})

もしくは、

復習 

<script setup>にすると、CompositionAPIの書き方になる。
setupがつかない場合、OptionsAPIの書き方になる。

  • トップレベルのバインディング:
    <script setup>内で定義されたすべての変数、関数、インポートは自動的にテンプレートで使用可能になります。
    export default { ... } や return { ... } が不要になります。

  • コンポーネントのインポートと登録の簡素化:
    インポートしたコンポーネントをcomponentsオプションに登録する必要がなくなります。

  • Propsの定義の簡素化:
    defineProps()を使用してpropsを直接定義できます。

  • Emitsの定義の簡素化:
    defineEmits()を使用してemitsを直接定義できます。

  • コンポーネントのライフサイクルフックの直接使用:
    onMounted()などのライフサイクルフックを直接呼び出せます。

  • コンポーザブルの使用が容易に:
    コンポーザブル関数の結果を直接テンプレートにバインドできます。

  • TypeScriptのサポート強化:
    TypeScriptの型推論が改善され、より型安全なコードが書きやすくなります。

  • パフォーマンスの最適化:
    コンパイル時の最適化が可能になり、実行時のパフォーマンスが向上します。

  • コードの見通しが良くなる:
    ボイラープレートコードが減り、コンポーネントのロジックがより明確になります。

TypeScriptの恩恵

以下のコードの何が間違っているのか、少しの文字を追加するだけでわかるようになる。それは何か?

.vue
<script setup>
import { ref } from 'vue'

const user = ref({
  username: 'danielkelly_io',
  email: 'daniel@vueschool.io',
  name: 'Daniel Kelly'
})
</script>

<template>
  <ul>
    <li>
      <strong>Username</strong>
      <span>{{ user.username }}</span>
    </li>
    <li>
      <strong>Email</strong>
      <span>{{ user.mail }}</span>
    </li>
    <li>
      <strong>Name</strong>
      <span>{{ user.name }}</span>
    </li>
  </ul>
</template>

答え:

<script setup lang="ts">

にすること。

するとTypeScriptのチェック機能でuser.emailとすべきところがuser.mailになっていることを教えてくれる。

分割代入でリアクティブ性が消えないために

以下のコードを実行し、Vueのコンソール上から編集することができる(≒リアクティブ性を持っている)のはanimal.nameの欄だけなのはなぜか。

image.png

.vue
<script setup>
import { reactive, computed } from 'vue'

const animal = reactive({
  name: 'Lion',
  diet: 'Carnivore',
  lifespan: '8-12 years'
})
const name = computed(() => animal.name)

const { diet, lifespan } = animal
</script>

<template>
  <ul>
    <li>Name: {{ name }}</li>
    <li>Diet: {{ diet }}</li>
    <li>Lifespan: {{ lifespan }}</li>
  </ul>
</template>

Claudeの答え:

  • reactive を使用して animal オブジェクトを作成しています。これにより、オブジェクト全体がリアクティブになります。
    name プロパティは computed を使用して定義されています。これにより、name は animal.name の値を返す計算済みプロパティとなります。
    diet と lifespan は分割代入を使用して animal オブジェクトから直接取り出されています。

  • Vueのデベロッパーツールで編集可能性が異なる理由:
    name が編集可能:
    name は computed プロパティとして定義されているため、Vueはこれを特別に扱います。
    計算済みプロパティは通常読み取り専用ですが、Vueのデバッグツールは特別にこれを編集可能にしています。
    編集すると、実際には元の animal.name の値が変更されます。

  • diet と lifespan が編集不可:
    これらは reactive オブジェクトからプリミティブ値として分割代入されています。
    分割代入された時点で、これらの変数はリアクティブな性質を失い、単なるローカル変数となります。
    そのため、Vueのデバッグツールではこれらを編集不可として扱います。

重要な点:

  • animal オブジェクト自体は依然としてリアクティブです。
    テンプレート内で diet や lifespan を直接使用しているため、初期表示は正しく行われます。
    しかし、animal.diet や animal.lifespan が後から変更されても、コンポーネント内の diet と lifespan 変数は更新されません。

より良い実装:

.vue
<script setup>
import { reactive, toRefs } from 'vue'

const animal = reactive({
  name: 'Lion',
  diet: 'Carnivore',
  lifespan: '8-12 years'
})

const { name, diet, lifespan } = toRefs(animal)
</script>

この方法を使用すると、すべてのプロパティがリアクティブな参照として扱われ、デバッグツールでも編集可能になります。また、animal オブジェクトの変更にも適切に反応します。

これがいつ役に立つかというと、composablesなどのAPIからデータを取得するとき。

./composables/useLions.ts
import { reactive } from 'vue'

export const useLion = () => {
  const animal = reactive({
    name: 'Lion',
    diet: 'Carnivore',
    lifespan: '8-12 years'
  })

  return animal
}

上記useLions.tsからデータを取得するには、以下のようにコンポーネントを修正する。

.vue
<script setup>
import { useLion } from '@/composables/useLions'

const { diet, lifespan, name } = useLion() //ここで分割代入すると再びリアクティブ性が失われる
</script>

<template>
  <ul>
    <li>Name: {{ name }}</li>
    <li>Diet: {{ diet }}</li>
    <li>Lifespan: {{ lifespan }}</li>
  </ul>
</template>

useLions.tsの修正:

useLions.ts
import { reactive, toRefs } from 'vue'

export const useLion = () => {
  const animal = reactive({
    name: 'Lion',
    diet: 'Carnivore',
    lifespan: '8-12 years'
  })

  return toRefs(animal) //toRefsでラップして返すように修正すると再びリアクティブ性を取り戻す
}

image.png

この修正を加えた後、yarn devし直さないと切り替わらなかった。Claude AIに聞いたら大きな変更後(特にリアクティビティに関わる変更後)は開発サーバーを再起動する習慣をつけましょう、ということだった。

composableはcomponentやcomposableの定義の直下で使い、関数の中で使ってはいけない

  • componentの<script setup> の直下で使う
  • composableの中で別のcomposableをimportして、composableの定義の直下で使う
    どちらもfunctionの中で使うことは推奨されない。

なぜなのか。

詳しいことはlittle bit over his pay gradeだそうで説明はあまりなかった。
Claude AIに聞いてみると、

これらのガイドラインは Vue の Composition API の設計思想と最適な使用方法に基づいています。以下にその理由を説明します:

パフォーマンスとリアクティビティの最適化:
コンポーネントやコンポーザブルの直下でコンポーザブルを使用すると、それらは一度だけ実行され、結果がキャッシュされます。
関数内で使用すると、その関数が呼ばれるたびにコンポーザブルが再実行される可能性があり、不要な計算やメモリ使用につながります。

リアクティビティの一貫性:
トップレベルで使用することで、コンポーザブルが返すリアクティブな値や参照が常に同じインスタンスを指すことが保証されます。
関数内で使用すると、関数の呼び出しごとに新しいリアクティブなインスタンスが作成される可能性があり、予期せぬ動作を引き起こす可能性があります。

コードの可読性と予測可能性:
トップレベルでコンポーザブルを使用することで、コンポーネントやコンポーザブルの依存関係が明確になります。
これにより、コードの構造が整理され、デバッグや保守が容易になります。

ライフサイクルフックとの整合性:
コンポーザブル内で onMounted などのライフサイクルフックを使用する場合、それらはコンポーネントのライフサイクルと正しく同期します。
関数内で使用すると、これらのフックが予期せぬタイミングで実行される可能性があります。

副作用の管理:
トップレベルで使用することで、副作用(例:APIコール、イベントリスナーの設定)が適切に管理され、重複や漏れを防ぐことができます。

最適化の機会:
Vue のコンパイラやランタイムは、トップレベルで使用されるコンポーザブルに対して最適化を行いやすくなります。

テスト容易性:
トップレベルで使用されるコンポーザブルは、コンポーネントやコンポーザブルの外部からテストしやすくなります。

コード分割とツリーシェイキングの効率:
トップレベルでの使用は、ビルドツールがコード分割やツリーシェイキングを効率的に行うのに役立ちます。

これらの理由から、コンポーザブルはコンポーネントや他のコンポーザブルの直下で使用することが推奨されています。ただし、特定のユースケースでは関数内での使用が適切な場合もあるため、常に具体的な状況に応じて判断することが重要です。

v-htmlを使う際の注意点

XSS(クロスサイトスクリプティング)脆弱性:
信頼できないソースからのHTMLをそのまま挿入すると、悪意のあるスクリプトが実行される可能性がある。

JavaScriptのサニタイズ方法:

  • DOMPurify を使う
  • 可能な限りv-htmlの使用を避け、Vueのテンプレート機能を活用する
  • Vue用のプラグインやライブラリの使用:vue-sanitize,xss
    -ホワイトリストアプローチ:許可されたタグとプロパティのみを通すフィルターを実装
javascriptCopyfunction sanitizeHtml(html) {
  const allowedTags = ['p', 'br', 'strong', 'em'];
  const allowedAttributes = ['class', 'id'];
  // タグとプロパティをフィルターする実装
}

Vue.jsのアプリ内でVanilla JavascriptのDOM操作をしない

.vue
onMounted(() => {
  document.querySelector('#advice').innerText = 'hoge'
})
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?