MofuMofu2 / react-sandbox

React.js Sandbox
1 stars 0 forks source link

7-5 ステート管理ライブラリ「Recoil」 #70

Closed MofuMofu2 closed 11 months ago

MofuMofu2 commented 1 year ago

https://recoiljs.org/

MofuMofu2 commented 11 months ago

一定の規模ってどのくらいから「一定の規模」なんだろうか。 useContext + useReducerを使うのが3箇所以上とかになるのかな。


React.jsやVue.jsでグローバルステート管理ライブラリを導入するタイミングについて迷います。入れる指標などありますか?

回答

React.jsやVue.jsでグローバルステート管理ライブラリを導入するタイミングは、プロジェクトの複雑さや開発チームのニーズによって異なりますが、一般的に考慮すべきいくつかの指標があります。 1. **コンポーネント間でのステート共有の必要性**: 複数のコンポーネント間で状態を共有する必要がある場合、グローバルステート管理が有効です。例えば、ユーザー認証情報やアプリケーションのテーマ設定など、アプリケーション全体で利用されるデータはグローバルステートで管理すると良いでしょう。 2. **ステートの複雑さと管理の難易度**: アプリケーションの状態が複雑になり、ローカルステートの管理だけでは不十分になる場合、グローバルステート管理ライブラリの導入を検討します。例えば、多くの状態が相互に依存し合っている場合や、状態が多数の更新を受ける場合などです。 3. **パフォーマンスの懸念**: ステートの更新が頻繁に行われ、これがパフォーマンスの低下を引き起こす可能性がある場合、グローバルステート管理ライブラリが解決策となることがあります。ライブラリには効率的なデータフローと更新メカニズムが組み込まれているため、アプリケーションのパフォーマンスを向上させることができます。 4. **開発チームの規模と協力**: プロジェクトが大規模で、多くの開発者が協力して作業する場合、グローバルステート管理ライブラリを導入することで、コードの整合性と保守性を高めることができます。 5. **将来のスケーラビリティ**: プロジェクトの将来的な成長を見越して、初期段階からグローバルステート管理ライブラリを導入することも有効な戦略です。これにより、後になって大規模なリファクタリングを避けることができます。 これらの指標を踏まえて、プロジェクトの特定の要件とニーズに基づいて決定を行うことが重要です。また、導入するライブラリを選ぶ際には、そのライブラリがプロジェクトの技術スタックやチームのスキルセットに適合しているかどうかも考慮する必要があります。

5は微妙かなと思った。最適化が早すぎると結局使わずに負債になりそう。状態が相互に依存する、はありかもしれない。

グローバルステートを導入する場合のデメリットも知りたいです。それが指標に使えるかなと思いました。

回答

グローバルステート管理ライブラリを導入する際のデメリットも確かに重要な指標になります。以下に主なデメリットを挙げてみましょう: 1. **複雑性の増加**: グローバルステート管理ライブラリを導入することで、アプリケーションの全体的な複雑性が増します。状態管理のコードが増え、アプリケーションのアーキテクチャがより複雑になるため、新しい開発者が理解するのに時間がかかる可能性があります。 2. **学習曲線**: 特定のステート管理ライブラリには独自のAPIやコンセプトがあり、これらを習得するためには追加の学習時間が必要です。これは特に、そのライブラリに慣れていない開発者にとっては大きな障壁となる場合があります。 3. **パフォーマンスへの影響**: グローバルステート管理は、不適切に実装された場合、アプリケーションのパフォーマンスに悪影響を及ぼす可能性があります。例えば、不必要な再レンダリングや、ステートの更新の管理が難しくなることがあります。 4. **依存関係の増加**: 外部のステート管理ライブラリに依存することで、アプリケーションの依存関係が増え、将来的にライブラリが更新されなくなったり、互換性の問題が生じたりするリスクが高まります。 5. **オーバーエンジニアリング**: 小規模なプロジェクトやシンプルなアプリケーションにグローバルステート管理ライブラリを導入することは、しばしばオーバーエンジニアリングとなり得ます。この場合、追加の複雑さは無駄になり、開発プロセスを遅くする可能性があります。 これらのデメリットを考慮し、プロジェクトの規模、複雑さ、チームのスキルセット、将来の成長予測などを基に、グローバルステート管理の導入を検討することが重要です。すべてのプロジェクトがグローバルステート管理を必要とするわけではなく、時にはローカルステートの管理だけで十分な場合もあります。

メンバーレベルや人数も考慮して入れてねということか。規模の判断基準も聞いてみる。

Vue.jsやReact.jsのアプリだと、大規模と小規模はどのような基準を持って判別できそうですか。

回答

Vue.jsやReact.jsでアプリケーションの規模を判別する際には、いくつかの基準を考慮すると良いでしょう。ここでの「規模」とは、コードベースの大きさ、機能の複雑さ、チームの人数、およびアプリケーションの維持・拡張の難易度を指します。以下の基準が参考になります: 1. **コードベースの大きさ**: アプリケーションのコード行数やコンポーネントの数が多いほど、規模は大きいと言えます。大規模プロジェクトでは数百から数千のコンポーネントを持つことがあります。 2. **機能の複雑さ**: 機能が多岐にわたり、相互に依存している場合、アプリケーションは複雑であると言えます。例えば、複数のユーザーの役割、高度なデータ処理、リアルタイムの通信機能などが含まれる場合、大規模なアプリケーションです。 3. **ユーザーインターフェースの複雑さ**: UIの複雑さも規模の一つの指標です。複数の画面、高度なインタラクティブ要素、複雑なナビゲーション構造を持つアプリケーションは、より大規模であると考えられます。 4. **チームのサイズと構造**: プロジェクトに関わる開発者の数が多いほど、通常はプロジェクトの規模も大きいです。大規模プロジェクトでは、異なる機能やコンポーネントを担当する複数のチームが存在することが一般的です。 5. **データ管理の複雑さ**: アプリケーションで扱うデータの量とその管理の複雑さも、規模を判断する重要な要素です。大量のデータや複雑なデータ構造を扱うアプリケーションは、規模が大きいと言えます。 6. **プロジェクトの継続期間とメンテナンス**: 長期間にわたる開発と継続的なメンテナンスが必要なプロジェクトは、大規模であることが多いです。 これらの基準を用いて、アプリケーションが小規模か大規模かを評価することができます。ただし、これらの基準は相互に関連しており、一つの基準だけで判断するのではなく、全体的なコンテキストを考慮することが重要です。

ありがとうございます。グローバルステート管理を入れるタイミングが理解できそうな気がします。基本は依存しないようにした上で、複数のコンポーネントから参照する状態だけ管理するのが良さそうに思いました。

回答

おっしゃる通りです。グローバルステート管理の導入は、アプリケーションの特定のニーズに基づいて慎重に検討すべきです。以下のポイントを念頭に置くと良いでしょう: 1. **最小限の依存**: グローバルステート管理を可能な限り最小限に保ち、必要な場合のみ使用することで、アプリケーションの複雑性を抑えることができます。 2. **複数コンポーネントでの共有**: グローバルステートは、複数のコンポーネント間で共有されるべき状態に限定することが重要です。例えば、ユーザーのログイン状態、テーマ設定、アプリケーション全体で共有される設定などが該当します。 3. **パフォーマンスとメンテナンスの考慮**: グローバルステートを管理する際は、パフォーマンスへの影響とメンテナンスのしやすさも考慮に入れる必要があります。適切な管理を行うことで、パフォーマンスの問題を避け、将来の拡張やメンテナンスを容易にします。 4. **段階的な導入**: アプリケーションの成長と共に、グローバルステート管理の必要性が高まることがあります。小規模から始めて、必要に応じて段階的に導入することで、過剰な依存や複雑性の増大を避けることができます。 5. **チームとのコミュニケーション**: 開発チーム内での十分なコミュニケーションを通じて、グローバルステートの使用に関するガイドラインやベストプラクティスを共有し、一貫性を保つことも重要です。 グローバルステート管理の導入は、アプリケーションの成長とともに見直しを行い、常にその利点とコストをバランスさせることが肝心です。

https://chat.openai.com/share/845e33f4-1c1b-4f4a-b6f1-793903975d32

MofuMofu2 commented 11 months ago

本には出てこないけどJotaiもあるなと思った。

MofuMofu2 commented 11 months ago

VuexのStore = Atom VuexのGetter = Selector VuexのMutationやActionsはなにに当たるんだろ


1つわかってれば概念を応用してやれる、は本当にそうだなと思った。最初に1つ理解するのを決めたらいい

MofuMofu2 commented 11 months ago

Vuexがわかっていれば考え方をReactに当てはめただけとわかる。

MofuMofu2 commented 11 months ago

Atomのデフォルト値をセットするとき、ステートに型定義をつけておきたい。 型推論が効いて設定ミスを防げるが、型を定義する方が色々な箇所で使い回せて都合がいい。

todoAtomはRecoilのAtom(グローバルステート)です。デフォルト値の型を明記して型安全性を保ちたいです。

回答

> Recoilのatomを使用している場合、TypeScriptの型推論を利用して型安全性を保つことができます。まず、TODOリストのアイテムを表すインターフェースを定義します: ```typescript interface Todo { id: number; title: string; isDone: boolean; } ``` > 次に、このTodo型を使用してtodoAtomのデフォルト値の型を明示的に指定します: ```typescript import { atom } from 'recoil'; interface Todo { id: number; title: string; isDone: boolean; } export const todoAtom = atom({ key: "todosAtom", default: [ { id: 1, title: "Learn React", isDone: false }, { id: 2, title: "Learn Recoil", isDone: false }, { id: 3, title: "Learn Redux", isDone: false }, ], }); ```

参考ドキュメントを出してもらった。

Recoilの公式ドキュメント: https://recoiljs.org/docs/introduction/getting-started TypeScriptのジェネリクスに関するドキュメント: https://www.typescriptlang.org/docs/handbook/2/generics.html Recoilの公式ドキュメント(API Reference: atom): https://recoiljs.org/docs/api-reference/core/atom Recoil + TypeScriptの記事: https://blog.logrocket.com/state-management-with-recoil-and-typescript/ // これは404になる

MofuMofu2 commented 11 months ago

Selectorはselector.tsに書くパターンが多そう。Copilotでそのように推論されている。

また、本では末尾にアクセスするときArray.prototype.at()を使っている。

at() メソッドは、index が負でない場合、ブラケット記法と等価です。例えば、array[0] と array.at(0) は、どちらも最初の項目を返します。しかし、配列の末尾から要素を数える場合、PythonやRのように array[-1] を使用することはできません。角括弧内の値はすべて文字列プロパティとしてリテラルに扱われるため、結局、配列のインデックスではなく通常の文字列プロパティである array["-1"] を読むことになります。

普通はいいが、NaNや小数が入ると値を別のものに変換して返してしまう。 数値で指定するくらいならいいかもしれない。

https://qiita.com/petamoriken/items/da03f55cb626617c1958 https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/at

MofuMofu2 commented 11 months ago

グローバルステートのテストは今まで書けなかった(プロジェクト構成的に難しかった)。どんなテストをすべきか?

todoリストのグローバルステートtodoAtomsとグローバルステートの末尾を取り出すselectorにテストを書きたいです。どのようなテストを書くべきですか。

Recoilのグローバルステートとセレクターに対するテストを書く際には、以下のようなテストケースを考えることができます:

  1. todoAtomのデフォルト値が期待通りであることを確認するテスト
  2. todoLastIdSelectorがtodoAtomの最後の要素のIDを正しく返すことを確認するテスト
  3. todoAtomが空の場合、todoLastIdSelectorが0を返すことを確認するテスト

これらのテストを書くためには、RecoilのRecoilRootとuseRecoilValueを使用して、テスト環境でRecoilの状態を操作することができます。

以下に、これらのテストケースを実装するためのサンプルコードを示します:

import { renderHook } from '@testing-library/react-hooks';
import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil';
import { todoAtom, todoLastIdSelector } from './selector';

// ステートの初期値が指定通りになっている
test('todoAtom has correct default value', () => {
  const { result } = renderHook(() => useRecoilValue(todoAtom), { wrapper: RecoilRoot });
  expect(result.current).toEqual([
    { id: 1, title: "Learn React", isDone: false },
    { id: 2, title: "Learn Recoil", isDone: false },
    { id: 3, title: "Learn Redux", isDone: false },
  ]);
});

// selectorで末尾の値を取り出せる
test('todoLastIdSelector returns last id', () => {
  const { result } = renderHook(() => useRecoilValue(todoLastIdSelector), { wrapper: RecoilRoot });
  expect(result.current).toBe(3);
});

// atomに何も格納されていない場合は0を返す
test('todoLastIdSelector returns 0 when todoAtom is empty', () => {
  const { result } = renderHook(() => {
    const setTodo = useSetRecoilState(todoAtom);
    setTodo([]);
    return useRecoilValue(todoLastIdSelector);
  }, { wrapper: RecoilRoot });
  expect(result.current).toBe(0);
});

このコードは、@testing-library/react-hooksとrecoilを使用しています。これらのパッケージがプロジェクトにインストールされていない場合は、インストールする必要があります。

Recoilの公式ドキュメント(Testing Guide):https://recoiljs.org/docs/guides/testing/

MofuMofu2 commented 11 months ago

テストを回すと次のように出てくる

Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: [https://reactjs.org/link/switch-to-createroot](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)

React 18では、ReactDOM.renderの代わりに新しいAPIであるReactDOM.createRootを使用することが推奨されています。しかし、@testing-library/react-hooksのrenderHookは内部でReactDOM.renderを使用しているため、この警告が表示されます。

現在のところ、@testing-library/react-hooksはReact 18のcreateRootAPIをまだサポートしていません。そのため、この警告を解消するための直接的な解決策はありません。

しかし、この警告は@testing-library/react-hooksがReact 18を完全にサポートするまでの一時的なものであり、テストの動作には影響を与えません。そのため、現時点ではこの警告を無視することをお勧めします。

なお、@testing-library/react-hooksの最新情報については、そのGitHubリポジトリをチェックしてください:https://github.com/testing-library/react-hooks-testing-library

つまり、ライブラリ側のセットアップがcreateRootAPIにならない限りこの警告は出続ける。

MofuMofu2 commented 11 months ago
  const { result, waitForNextUpdate } = renderHook(() => {
    const setTodo = useSetRecoilState(todoAtom);
    useEffect(() => {
      setTodo([]);
    }, [setTodo]);
    return useRecoilValue(todoLastIdSelector);
  }, { wrapper: RecoilRoot });
  waitForNextUpdate();
  expect(result.current).toBe(0);

このエラーは、Reactのレンダリングフェーズ中に状態更新が行われていることを示しています。Reactでは、レンダリングフェーズ中に状態を更新することは許可されていません。

このエラーは、renderHookのコールバック内でuseSetRecoilStateフックを使用して状態を更新しているために発生している可能性があります。この問題を解決するためには、useSetRecoilStateをuseEffectフック内で使用することをお勧めします。

以下に修正したテストコードを示します:

test('todoLastIdSelector returns 0 when todoAtom is empty', () => {
  const { result, waitForNextUpdate } = renderHook(() => {
    const setTodo = useSetRecoilState(todoAtom);
    useEffect(() => {
      setTodo([]);
    }, [setTodo]);
    return useRecoilValue(todoLastIdSelector);
  }, { wrapper: RecoilRoot });
  waitForNextUpdate();
  expect(result.current).toBe(0);
});

Vuexと同様で値の更新中に別の箇所から値を更新するなと言っている。

MofuMofu2 commented 11 months ago

waitForNextUpdateを使うと、次のレンダリングのタイミングを待ってしまう。 Selctorのテストをするときはコンポーネントはないので、waitForでuseEffectの終了を待たないとタイムアウトしてテストが落ちる。

https://react-hooks-testing-library.com/reference/api#async-utilities https://zenn.dev/bom_shibuya/articles/5c3ae7745c5e94

MofuMofu2 commented 11 months ago

atomFamilyを使いステートを分割する例が出てくる。 selectorにgetterとsetterを定義すると次のようなエラーになる。要はsetは定義されていないからダメということらしい。

No overload matches this call.
  Overload 2 of 2, '(options: ReadOnlySelectorOptions<(TodoList | null)[]>): RecoilValueReadOnly<(TodoList | null)[]>', gave the following error.
    Object literal may only specify known properties, and 'set' does not exist in type 'ReadOnlySelectorOptions<(TodoList | null)[]>'.ts(2769)

しかしドキュメントにもsetの記載はある。なんで? https://recoiljs.org/docs/api-reference/core/selector/ https://zenn.dev/kyo9bo/articles/58b0ef35837462

MofuMofu2 commented 11 months ago

selectorの型を確認したが、setが使えると示されている。ジェネリクスで推論されるのでステートの型定義の指定がまずそう。ここよりも先にuseMemoなどを進めたいので置いておく。

 export interface ReadWriteSelectorOptions<T> extends ReadOnlySelectorOptions<T> {
  set: (
    opts: {
      set: SetRecoilState;
      get: GetRecoilValue;
      reset: ResetRecoilState;
    },
    newValue: T | DefaultValue,
  ) => void;
 }

 /**
  * Creates a selector which represents derived state.
  */
 export function selector<T>(options: ReadWriteSelectorOptions<T>): RecoilState<T>;