2017/11/01

[Vue.js]Vuexを使わずにコンポーネント間のデータやり取り・状態管理する方法3パターン

Vue.jsで小規模なSPAを開発している。Vuexを導入するほどでもなく、Vue.jsの標準機能でコンポーネント間でデータをやりとりする方法がないか考えていた。

親子コンポーネント間についてはProps down/Events upでデータのやりとりができる。
子子コンポーネント間(いわゆる兄弟間)はどうするのが良いのかを3つのパターンを紹介する。

  • $on/$emitでのイベントによるやり取り
  • 親コンポーネントをコントローラにして子コンポーネント間を取り持つ
  • Storeパターンの適用


親子コンポーネント間のデータのやり取りについては、以下の記事を参照してほしい。


$on/$emitでのイベントによるやり取り

一番簡単なのが、この$on/$emitでのイベントによるやり取り。
共有のViewModel(vueインスタンス)を用意し、そこにイベントを登録/発火させることで親子に関係なくデータのやりとりができる。
<div id="parent" class="container">
  <h3 class="title is-4">方法2)子1からEventEmitで子2に渡す方法</h3>
  
  <!-- Child1 -->
  <child-1></child-1>
  <!-- Child2 -->
  <child-2></child-2>
</div>

<!-- 子コンポーネント1 -->
<script type="text/x-template" id="child-1">
  <div class="field">
    <label class="label">child-1</label>
    <input class="input" type="text" v-model="text">
    <button class="button is-light" @click="submit">Sent Text to Child-2</button>
  </div>
</script>

<!-- 子コンポーネント2 -->
<script type="text/x-template" id="child-2">
  <div class="field">
    <label class="label">child-2</label>
    <p class="subtitle" v-text="text"></p>
  </div>
</script>
const vm = new Vue();
const Child1 = Vue.component('child-1', {
  template: '#child-1',
  data () {
    return {
      text: ''
    }
  },
  methods: {
    submit () {
      // send-textイベントを発火させる
      vm.$emit('send-text', this.text)
    }
  }
});

const Child2 = Vue.component('child-2', {
  template: '#child-2',
  data () {
    return {
      text: ''
    }
  },
  created () {
    // どこから来るかわからないけど`send-text`イベントを待ち受ける
    vm.$on('send-text', this.changeText)
  },
  methods: {
    changeText (text) {
      this.text = text;
    }
  }
});

new Vue({
  el: "#parent",
  components: {
    Child1,
    Child2
  }
});
サンプルコードを見やすくするためにx-templateを使っているが、プロダクトなどでは非推奨

子コンポーネント1ではvm.$emit('send-text', this.text)を実行し、send-textイベントを発火させ、引数としてthis.textを渡している。

子コンポーネント2ではvm.$on('send-text', this.changeText)でsend-textイベントをリッスンし、this.changeText = (text) => { this.text = text }を実行しデータを受け取り、自分のthis.textに設定している。

この方法は、親子や兄弟など関係なくどんなコンポーネント間でもデータのやりとりができる。

しかしその反面、イベント名がコンフリクトしないように気をつかったり、どこから来てどこへ行くのかもわからない。
個人的には、あまり使いたくない方法だ。


参考: コンポーネント#親子間以外の通信




親コンポーネントをコントローラにして子コンポーネント間を取り持つ

親コンポーネントをコントローラとして、子コンポーネントからデータを受け取り、別の子コンポーネントにデータを渡すという方法だ。
<div id="parent" class="container">
  <h3 class="title is-4">方法1)親コンポーネントが橋渡しする方法</h3>
  <!-- Parent -->
  <div class="field">
    <label class="label">parent</label>
    <p class="subtitle" v-text="parentText"></p>
  </div>
  
  <!-- Child1 -->
  <child-1 v-model="parentText"></child-1>
  <!-- Child2 -->
  <child-2 :text="parentText"></child-2>
</div>

<script type="text/x-template" id="child-1">
  <div class="field">
    <label class="label">child-1</label>
    <input class="input" type="text" @input="changeInput">
  </div>
</script>

<script type="text/x-template" id="child-2">
  <div class="field">
    <label class="label">child-2</label>
    <p class="subtitle" v-text="text"></p>
  </div>
</script>
const ChildA1 = Vue.component('child-a-1', {
  template: '#child-a-1',
  methods: {
    changeInput (e) {
      // v-modelで指定したparentTextを変更するイベント
      this.$emit('input', e.target.value);
    }
  }
});

const ChildA2 = Vue.component('child-a-2', {
  template: '#child-a-2',
  props: {
    // 親からtextをもらう
    text: String
  }
});

const vm1 = new Vue({
  el: "#parent-a",
  components: {
    ChildA1,
    ChildA2
  },
  data () {
    return {
      parentText: ''
    }
  }
});

やっていることは親子コンポーネント間でProps down/Events upをしているだけだ。
子コンポーネント1→親コンポーネント→子コンポーネント2と親コンポーネントを経由してデータを渡す。

先ほどの$on/$emitに比べてシンプルな状態に保てるが、やりとりするデータやコンポーネントの量が増えると親コンポーネントが肥大化してしまう。
またコンポーネントの階層が深くなると、それぞれが密結合してしまい汎用性がなくなってしまう。

小さなアプリケーションならこれで良いが、ちょっとでも大きくなりそうなら考え直す必要がありそうだ。


参考: コンポーネント#カスタムイベントを使用したフォーム入力コンポーネント




Storeパターンの適用

状態管理を一元管理することにより、どんなコンポーネントからでも参照できる。
ただStore.stateを直接弄るようなことはせず、なんらかのイベントを使って変更する。
<div id="parent">
  <div class="panel parent">
    <label class="label">Parent</label>
    <p v-text="state.message"></p>
  </div>

  <child></child>
  <grand-child></grand-child>
</div>

<script type="text/x-template" id="grand-child">
  <div class="panel grand-child">
    <label class="label">GrandChild</label>
    <input class="input" type="input" v-model="text">
    <button class="button" type="button" @click="setMessage">Set</button>
    <button class="button" type="button" @click="clearMessage">Clear</button>
  </div>
</script>

<script type="text/x-template" id="child">
  <div class="panel child">
    <label class="label">Child</label>
    <p v-text="state.message"></p>
  </div>
</script>
const store = {
  state: {
    message: ''
  },
  setMessage (msg) {
    this.state.message = msg;
  },
  clearMessage (msg) {
    this.state.message = '';
  }
};

const GrandChild = Vue.component('grand-child', {
  template: '#grand-child',
  data () {
    return {
      text: '',
      state: store.state
    }
  },
  methods: {
    setMessage () {
      store.setMessage(this.text);
    },
    clearMessage () {
      store.clearMessage();
    }
  }
});

const Child = Vue.component('child', {
  template: '#child',
  data () {
    return {
      state: store.state
    }
  }
});

const Parent = new Vue({
  el: "#parent",
  components: {
    Child,
    GrandChild
  },
  data () {
    return {
      state: store.state
    }
  }
});

前述の2パターンと比べるとかなりシンプルになる。
また、どんなにコンポーネントの階層が深くても単一のStoreを参照できるメリットがある。

実装してみた感覚は、Vuexのstate、actionsだけを使っている感じだ。


参考: 状態管理#シンプルな状態管理をゼロから作る




デモ





まとめ


3パターン紹介してきたが、どれも一長一短あるので自分が実現したい機能との兼ね合いで選択したほうが良さそう。

オススメ度としては Storeパターン >>> 親コントローラ >>>>>>>>>>>>> イベント みたいな感じだ。


ちなみに冒頭に述べた小規模SPAは「親コンポーネントをコントローラとして子コンポーネント間を取り持つ」方法を採用した。

理由は、シンプルさを保つため。
Storeパターンもシンプルなのだが、「Storeパターン」について知っていないとコードを読む時間が増えてしまうからだ。


こうやっていろいろなパターンを試してみた結果、ちょっとでも大きくなりそうなら迷わず最初からVuexを導入するのが良い選択だと思う。


以上

written by @bc_rikko

0 件のコメント :

コメントを投稿