🌟

Reactの状態管理ライブラリ『Valtio』の環境構築と使い方(Redux や Recoilより簡単)

2023/10/01に公開

こんにちは、AIQ株式会社のフロントエンドエンジニアのまさぴょんです!
今回は、Reactの状態管理ライブラリ『Valtio』の環境構築と使い方について解説します。
個人的には、このValtioは、かなりおすすめで、Reduxや、Recoilよりもシンプルで使いやすいです。

Valtioとは?

Valtioは、Reduxや、Recoilと同じくReactの状態管理ツールの1つです。
Reduxや、Recoilと比べて、構文がシンプルであり、使いやすい点が最大の特徴です。
(筆者は、実務で、Reduxも、Recoilも使ったことがあります)

個人的には、このValtioは、かなりおすすめで、日本でもこれから来るのではと考えております。

Valtioの特徴をまとめると、次のとおりです。

Valtioの特徴まとめ
  1. Proxyベースの状態管理: ValtioはJavaScriptのオブジェクトをProxyでラップし、これらのオブジェクトをState(状態)として管理します。
    • Proxyにより、状態の変更を監視し、Reactコンポーネントを再レンダリングすることが可能となります。
  2. 単純な構文: Valtioはシンプルな構文を提供し、状態を変更するためのAPIが直感的で使いやすいです。
  3. 非同期サポート: Valtioは非同期操作にも対応しており、非同期関数を使用して状態を変更できます。
  4. React Suspenseと統合: ValtioはReact Suspenseと統合されており、データの取得が完了するまでフォールバックコンテンツを表示できます。
  5. 性能最適化: ValtioはReactのコンテキストを使用して、コンポーネントの再レンダリングを最小限に抑えることができ、高性能な状態管理を実現します。

Valtioは、多機能であり、公式のREADMEにて、いろいろな使い方が紹介されていますが、
今回は、状態管理で必須となるState(状態)の定義と、状態のUpdateの部分をご紹介します。

https://github.com/pmndrs/valtio

Valtioの環境構築と使い方

まずは、Valtioをプロジェクトにインストールします。

bash
npm install valtio

# または

yarn add valtio

続いて、ValtioのTest用のComponentを作成してみます。

TestValtio.tsx
// 1. Valtio の proxy を import する
import { proxy } from "valtio";

// 2. proxy で、状態オブジェクト(State)を作成する
const state = proxy({ count: 0 });

export default function ValtioTest() {
  return (
    <div>
      <p>Count: {state.count}</p>
      {/* 3. State を Update する => 直接Stateを参照して、Updateできる */}
      <button onClick={() => (state.count += 1)}>プラス</button>
      <button onClick={() => (state.count -= 1)}>マイナス</button>
    </div>
  );
}

Sample Codeでは、最低限の使い方として、

  1. Valtioのproxyimportする。
  2. proxyで、状態オブジェクト(State)を作成する。
  3. StateをUpdateする。
    • 直接Stateを参照して、Updateできる。

という最低限のState保持ができるComponentを作成しています。

より実践的なValtioの使い方

ここからは、実際のプロジェクトでValtioを使用している形に近い使い方をご紹介します。

先にどんなファイルを作るかをご説明すると、次のとおりです。

  1. StoreとStoreで管理するDataの型定義ファイル
  2. Storeファイル
  3. Stateを実際に使用する(参照したり、更新したりする)ファイル

StoreとStoreで管理するDataの型定義ファイル

まずは、Storeの型定義ファイルであるstore.d.tsファイルを作ります。

これから、作成するValtioのStoreが外部にexportするStoreの型は、基本的にData参照のためのstatesと、Dataを参照したり、更新したりするためのactionsの2つを保有したObject型になります。

frontend/src/types/store.d.ts
/** 1. Valtio Store の型定義 */
export interface Store {
  states: Record<string | number | symbol, any>;
  actions: Record<string | number | symbol, (...param: any) => any>;
}

/** 2. Record<string | number | symbol, any> は、次のような Index Signatureの型定義と同様 */
interface StatesType {
  [key: string | number | symbol]: any;
}

/** 3. Record<string | number | symbol, (...param: any) => any> は、次のような Index Signatureの型定義と同様 */
interface ActionsType {
  [key: string | number | symbol]: (...param: any) => any;
}

続いて、今回使用する会員MemberのDataの型定義ファイルを作成します。

frontend/src/types/member.d.ts
/** Member情報 の型定義 */
export interface MemberType {
  id: number;
  full_name: string | null; //	ユーザーネーム
  biography: string | null; // 詳細・説明文
  thumbnail: string | null; //	プロフィール画像のURL
  follows: number | null; //	フォロー数
  followers: number | null; //	フォロワー数
  gender: number | null; //	性別
  generation: number | null; //	年代
  posts: { id: number; title: string; content: string }[]; // 投稿情報・List
}

Storeの定義

次に本丸であるValtio・Storeを作っていきます。

Storeのファイルは、細かい単位で分離して、作成していく方針でいきます。

今回は、「会員Member(Object)のList情報を管理するStore」と、
「現在、詳細情報を確認している会員Member(Object)と、その1つ前・1つ後のMember情報を保持するStore」を作ります。

MemberListStoreファイル

まず、「会員Member(Object)のList情報を管理するStore」は、次のような内容になります。

1つのStoreファイルで、State定義とSetterであるActionsの定義をして、Store Objectにまとめて、exportします。

frontend/src/store/MemberListStore.tsx
import { proxy } from "valtio";
import { MemberType } from "../types/member";
import { Store } from "../types/store";

/**
 * NOTE: MemberListStore
 * => MemberのListを保持する Store
 */

/** 1. States を定義する => 状態オブジェクトを作成する */
export const states = proxy({
  /** Member の検索結果・一覧 Data */
  memberList: [] as MemberType[],
});

/** 2. Setter を定義する => State を Update する Func */
export const actions = {
  setMemberList(v: MemberType[]) {
    states.memberList = v;
  },
};

/** 3. States(値)と、Actions(Setter) を保持した Store を定義する */
export const MemberListStore: Store = {
  states,
  actions,
};

export default MemberListStore;

SelectDetailMemberStoreファイル

続いて、「現在、詳細情報を確認している会員Member(Object)と、その1つ前・1つ後のMember情報を保持するStore」は次のような内容になります。

今回でいうと、現在、詳細情報を表示しているMemberの情報から、前後のMember情報を取得するFunctionを定義しています。

frontend/src/store/SelectDetailMemberStore.tsx
import MemberListStore from "./MemberListStore";
import { proxy } from "valtio";
import { MemberType } from "../types/member";
import { Store } from "../types/store";

/**
 * NOTE: SelectDetailMemberStore
 * => Userが、画面上で、Select して、詳細画面を表示している Member の 情報を保持する Store
 */
export const states = proxy({
  selectDetailMember: {}, // Detail 画面を表示中の Select 中の Member
  prevMember: {}, // 1つ前の Member
  nextMember: {}, // 次の Member
});

/** Setter */
export const actions = {
  setSelectDetailMember(currentMember: MemberType) {
    states.selectDetailMember = currentMember;
    // currentMember の変更後に、他、2つを算出する
    computeds.getPrevAndNextMember(currentMember);
  },
  setPrevMember(prevMember: MemberType) {
    states.prevMember = prevMember;
  },
  setNextMember(nextMember: MemberType) {
    states.nextMember = nextMember;
  },
};

/**
 * Stateの変更の後に 算出する_Functions
 * => Vue でいうような Computed のような役割を自作で定義してみる
 */
const computeds = {
  /** 現在のDetail表示 Memberの情報から、前後の Member情報を取得する Func */
  getPrevAndNextMember(currentMember: MemberType) {
    /** 表示中 Member の id */
    const currentMemberId = currentMember.id;
    /** Memeber の一覧 List */
    const MemberList = MemberListStore.states.MemberList;
    /** 表示中の Member の index */
    const currentMemberIdx = MemberList.findIndex(
      (Member: MemberType) => Member.id === currentMemberId
    );
    /**
     * NOTE: 前の Member が取得できる場合は、それをSetする
     * => currentMember が、配列の最初であるために、取得できない場合は、配列の最後のユーザーをSetする
     * => 循環するようにする
     */
    let prevMember = MemberList[currentMemberIdx - 1];
    // console.log('prevMember', prevMember);
    if (prevMember === undefined) {
      // currentMember が、配列の最初であるために、取得できない場合は、配列の最後のユーザーをSetする
      prevMember = MemberList[MemberList.length - 1];
    }
    actions.setPrevMember(prevMember);

    /**
     * NOTE: 後の Memberが取得できる場合は、それをSetする
     * => currentMember が、配列の最後であるために、取得できない場合は、配列の最初のユーザーをSetする
     * => 循環するようにする
     */
    let nextMember = MemberList[currentMemberIdx + 1];
    // console.log('nextMember', nextMember);
    if (nextMember === undefined) {
      // currentMember が、配列の最後であるために、取得できない場合は、配列の最初のユーザーをSetする
      nextMember = MemberList[0];
    }
    actions.setNextMember(nextMember);
  },
};

/** States(値)と、Actions(Setter) を保持した Store */
export const SelectDetailMemberStore: Store = {
  states,
  actions,
};

export default SelectDetailMemberStore;

Valtio・StoreをReact Component内部で使用する

最後に、Valtio・StoreをReact Component内部で使用する方法のSampleファイルをご紹介します。

このSampleファイルでの処理の特徴をまとめると次のとおりです。

  1. 定義したValtio・StoreであるMemberListStoreimportしています。
  2. 型定義や、ValtioのuseSnapshotimportしています。
  3. Reactコンポーネント内でuseSnapshotを使用してValtio Stateを取得しています。
  4. React Component から、State を Updateしています。

ちなみに、useSnapshotは、Valtio Stateの最新のスナップショットをReact コンポーネント内で利用するためのValtio のHooksになります。

React Component内部で使用する方法を伝えたいだけなので、React Component内部の処理は適当ですが、ご容赦ください。

frontend/src/components/views/RobotamaFanClub.tsx
import MemberListStore from "../../store/MemberListStore";
import { useSnapshot } from "valtio";
import { MemberType } from "../../types/member";

const RobotamaFanClub = () => {
  /** 1. React Component の内部で、Dataを参照する場合は、 useSnapshot() を使用する */
  const memberListStore = useSnapshot(MemberListStore.states);
  const memberList = memberListStore.memberList as MemberType[];

  const onClickHandler = () => {
    /** 2. React Component から、State を Updateする場合  */
    MemberListStore.actions.setMemberList(memberList);
  };

  return (
    <div>
      <button
        onClick={() => {
          onClickHandler();
        }}
      >
        State Update
      </button>
    </div>
  );
};

export default RobotamaFanClub;

まとめ

今回は、Test用のMemberのMockデータなどは、用意していませんが、ここまでの説明をしっかりと観てくださった方なら、Valtioの使い方が伝わったと思います。

Valtioは使いやすく、Store内でカスタマイズもしやすくおすすめです。
実務で使いながら、より使い方を深めていき、Zennでもご紹介していきたいと思います。

Recoilについては以前に、こちらの記事で使い方をまとめています。

https://masanyon.com/react-recoil-state-hooks/

注意事項

この記事は、AIQ 株式会社の社員による個人の見解であり、所属する組織の公式見解ではありません。

参考・引用

https://github.com/pmndrs/valtio

https://dev.classmethod.jp/articles/valtio-suspense/

求む、冒険者!

AIQ株式会社では、一緒に働いてくれるエンジニアを絶賛、募集しております🐱🐹✨

エンジニア視点での我が社のおすすめポイント

  1. フルリモート・フルフレックスの働きやすい環境!
    • 前の会社でアサインしてた現場は、フル出社だったので、ありがたすぎる。。。
    • もうフル出社には、戻れなくなります!
  2. 経験豊富なエンジニアの先輩方
    • 私は、3年目の駆け出しエンジニアなので、これが、かなりありがたいです!
  3. 自社開発とR&D(受託開発)を両方している会社なので、経験できる技術が多い。
    • 自社のProduct開発と、他社からの受託案件で、いろいろな技術を学ぶことができます。
  4. AI関連の最新の技術に触れられるチャンスが多い。
    • 自社で特許を持つほど、AI技術に強い会社で、プロファイリングを得意とした技術体系があります。
    • ChatGPTを自社アプリに搭載など、AIトレンドも、もちろん追っており、最新の技術に触れられるチャンスが多いです。
  5. たまに、札幌ラボ(東京から札幌) or 東京オフィス(札幌から東京)に出張で行ける!
    • 東京と、札幌に2拠点ある会社なので、会合などで集まる際に、出張で行けます。

採用技術 (一部抜粋)

  • FrontEnd: TypeScript, JavaScript, React.js, Vue.js, Next.js, Nuxt.js など
  • BackEnd: Node.js, Express,Python など
  • その他技術: Docker, AWS, Git, GitHub など

エントリー方法

  1. 私達と東京か札幌で一緒に働ける仲間を募集しています。
    詳しくは、Wantedly (https://www.wantedly.com/companies/aiqlab)を見てみてください。

Webエンジニア向け説明

https://www.wantedly.com/projects/1089410

データサイエンティスト向け説明

https://www.wantedly.com/projects/1089406

人事に直通(?)・ご紹介Plan(リファラル採用)

私経由で、ご紹介もできますので、興味のある方や気軽にどんな会社なのか知りたい方は、X(旧:Twitter)にて、DMを送ってくれても大丈夫です。
https://twitter.com/masanyon1212

AIQ Tech Blog (有志)

Discussion