🍍

Pinia開発者の議論から学ぶ、良いコードを書くための4つのルール🍍

2024/12/03に公開

この記事では、PiniaのEslintに関するissueやDiscussionでの議論を通じて、どのように書き方を改善できるかを考えていきます!

こんにちはmocchantaiです。
Piniaは使ってないけど、Piniaが可愛いので布教したいと思っています。
先日Piniaについて以下の記事を書きました。
https://zenn.dev/sdb_blog/articles/006-pinia-tutorial

今回参考にしたGithubのissueとdiscussionは以下の2つです。
これらの内容を元に、読みやすく加工してみました。
この記事をきっかけに、vuejs, piniaのGithubリポジトリを覗いて見てください😁
https://github.com/vuejs/pinia/

ディスカッションの登場人物

  • MickL
    • Pinia Eslintプラグインの必要性を感じてissueを最初に立てた人。
  • posva
    • Piniaのメンテナーであり、Vue.jsのコアチームメンバー。
  • lisilinhart

ディスカッション

↓詳しくはこちらのディスカッションをご覧ください!
https://github.com/vuejs/pinia/discussions/2638

MickL「PiniaにEslintプラグインがあったら最高じゃない?」

  • 「PiniaにEslintプラグインがあったら最高じゃない?例えば、setupストアで定義した変数や関数をエクスポートし忘れたら警告してくれるとかさ。他にもベストプラクティスとかアンチパターンを指摘してくれると便利だと思うんだよね。」

lisilinhart「4つルール考えたんだけどどう?」

  • 「それめっちゃいいね!以下の4つのルールはどうかな?」
    • prefer-use-store-naming-convention: ストア名をuseで始め、ストア名の後に接尾辞(例: Store)を付ける規約を強制。
    • prefer-single-store-per-file: 1つのストアを1ファイルに定義することを推奨。
    • require-setup-store-exports: setup関数内で定義されたstateプロパティがすべてエクスポートされているかをチェック。
    • no-return-global-properties: setupストアからrouteappProvidedのようなグローバルプロパティを返すことを禁止し、警告を出す。

posva「素晴らしいルールだね!それぞれ少しフィードバックするね!」

  • 1. prefer-use-store-naming-convention
    • 「ストア名をuseで始めるというのは良いアイデアだね。ただ、Storeという接尾辞を必須にするのではなく、Serviceなど他の名前を使いたいケースもあるから、これをオプションで設定可能にした方が良いと思う。」
  • 2. prefer-single-store-per-file
    • 「1つのファイルに1つのストアを定義するルールも良いけど、これはデフォルトで有効にしない方がいいかもね。特にプライベートストアを使うケースでは、ストアを分けると不便な場合があるし、テスト目的でプライベートストアをエクスポートするケースも考えられるから。」
  • 3. require-setup-store-exports
    • 「setupストアのエクスポートについては、状態管理に関連するrefreactiveだけを対象にするのが良さそうだね。functions, computed, raw dataは対象外で問題ないと思う。」
  • 4. no-return-global-properties
    • 👍

posva「他にもこんなルールあったらいいかもね!」

posvaのフィードバックを踏まえた推奨の書き方

1. prefer-use-store-naming-convention

ストア名はuseで始めることを推奨します。ただし、Storeという接尾辞はオプションで設定可能とし、開発者が自由に命名できる柔軟性を残します。

遵守している例

// useCounterStore.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0);

  const increment = () => {
    count.value++;
  };

  return { count, increment };
});

遵守していない例

// counter.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';

- export const useCounterStore = defineStore('counter', () => {
+ export const counter = defineStore('counter', () => {
  const count = ref(0);

  const increment = () => {
    count.value++;
  };

  return { count, increment };
});

2. require-setup-store-exports

setup関数内で定義されたrefやreactiveのstateはすべてエクスポートされるべきです。ただし、計算プロパティや関数、その他の補助的なデータは対象外とします。

遵守している例

// useTodoStore.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useTodoStore = defineStore('todo', () => {
  const todos = ref<string[]>([]);

  const addTodo = (todo: string) => {
    todos.value.push(todo);
  };

  return { todos, addTodo }; // 全てエクスポート
});

遵守していない例

// useTodoStore.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useTodoStore = defineStore('todo', () => {
  const todos = ref<string[]>([]);

  const addTodo = (todo: string) => {
    todos.value.push(todo);
  };
- return { todos, addTodo }; // 全てエクスポート
+ return { addTodo }; // `todos` をエクスポートし忘れている
});

3. prefer-single-store-per-file

1つのファイルに1つのストアを定義することを推奨します。ただし、テストやプライベートストアなど特定のユースケースでは無効化も可能とします。

遵守している例

// useUserStore.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useUserStore = defineStore('user', () => {
  const name = ref('Alice');

  const setName = (newName: string) => {
    name.value = newName;
  };

  return { name, setName };
});

遵守していない例

// stores.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useUserStore = defineStore('user', () => {
  const name = ref('Alice');

  const setName = (newName: string) => {
    name.value = newName;
  };

  return { name, setName };
});

+ export const useCounterStore = defineStore('counter', () => {
+   const count = ref(0);
+ 
+   const increment = () => {
+     count.value++;
+   };
+ 
+   return { count, increment };
+ });

4. no-return-global-properties

setupストアからrouteやappProvidedなどのグローバルプロパティを返すことを禁止します。これにより、予期せぬ副作用を防ぎます。

遵守していない例(routeのケース)

// useAuthStore.ts
import { defineStore } from 'pinia';
import { useRoute } from 'vue-router';

export const useAuthStore = defineStore('auth', () => {
  const route = useRoute(); // グローバルプロパティ

  return { route }; // routeをエクスポートしている
});

遵守していない例(appProvidedのケース)

// useAuthStore.ts
import { defineStore } from 'pinia';
import { inject } from 'vue';

export const useAuthStore = defineStore('auth', () => {
  const appProvided = inject('someGlobalValue'); // グローバルプロパティ

  return { appProvided }; // appProvidedをエクスポートしている
});

少しでもPiniaに興味をもってくれたら嬉しいです!
これからもPiniaについて少しずつブログを書いていきたいです🍍

ソーシャルデータバンク テックブログ

Discussion