Reactアプリケーションを開発する際、状態管理は避けて通れない課題です。小規模なプロジェクトでは、Reactの内部状態やコンテキストAPIを利用して十分なケースもありますが、アプリケーションが成長するにつれて、状態管理は次第に複雑化し、保守性が低下することがあります。ReduxやMobXのようなツールが存在する一方で、それらは高機能であるがゆえに、設定や学習コストが高くなることが懸念されます。
本記事では、軽量かつシンプルでありながら、大規模アプリケーションにも適用可能な状態管理ライブラリ「Zustand」に焦点を当てます。Zustandの基本的な使い方から、大規模アプリケーションでの具体的な設計パターンやトラブルシューティングまでを網羅し、開発者がReactプロジェクトにおいて効率的に状態管理を行うための実践的なガイドを提供します。
Zustandとは
Zustandは、Reactアプリケーション向けの軽量な状態管理ライブラリです。名前はドイツ語で「状態」を意味し、そのシンプルさとパフォーマンスの高さが特徴です。Reduxのような従来のライブラリに比べ、Zustandはミニマリズムを追求しており、わずかな設定で使い始めることができます。
他の状態管理ライブラリとの違い
Zustandは、以下の点で他の状態管理ライブラリと異なります。
1. 簡潔で直感的なAPI
Zustandは、状態の定義や更新が非常にシンプルです。コード量が少なく、直感的な設計が可能です。
2. Reactに依存しない
Zustandは、React以外の環境でも使用可能な状態管理ソリューションです。これにより、Reactに限定されない柔軟な設計が可能です。
3. 高いパフォーマンス
Zustandは、状態が変更された場合に必要なコンポーネントのみを再レンダリングします。これにより、ReduxやMobXよりも軽量かつ高速に動作します。
Zustandの適用シーン
Zustandは以下のようなシーンで特に有用です。
- 小規模から中規模のアプリケーション開発
- Reduxの設定が過剰に感じられるプロジェクト
- 大規模プロジェクトでの部分的な状態管理(スライスの導入)
Zustandは、シンプルさを保ちながら高い柔軟性を提供する、React開発者にとって強力なツールと言えます。
Zustandの基本的な使い方
Zustandは、その軽量さゆえに非常に簡単に使い始めることができます。このセクションでは、ZustandをReactプロジェクトに導入し、基本的なAPIを活用する手順を説明します。
1. Zustandのインストール
Zustandはnpmまたはyarnで簡単にインストールできます。以下のコマンドを実行して、プロジェクトに追加します。
npm install zustand
# または
yarn add zustand
2. 基本的なストアの作成
Zustandでは、状態はストアという形式で管理されます。ストアを作成するには、create
関数を使用します。以下は、カウンターアプリケーションの状態管理ストアを作成する例です。
import create from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
この例では、count
という状態と、それを増減させるincrement
とdecrement
のアクションを定義しています。
3. 状態の使用方法
作成したストアをReactコンポーネント内で使用するには、useStore
フックを呼び出します。以下の例では、状態を表示し、ボタンで増減操作を行います。
import React from 'react';
import useStore from './store';
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
const decrement = useStore((state) => state.decrement);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
export default Counter;
4. 初期値の設定
ストアを作成する際に、状態の初期値を簡単に指定できます。上記の例では、count
の初期値を0に設定しています。
5. ZustandのAPIの特徴
ZustandのAPIには以下の特徴があります:
- シンプルな状態取得:
useStore.getState()
で状態の即時取得が可能 - リスナー追加:
useStore.subscribe()
で状態変更のリスニングが可能
このように、Zustandは導入が非常に容易で、シンプルなプロジェクトから高度なアプリケーションまで柔軟に対応できます。
大規模アプリケーションにおける課題
Reactアプリケーションが成長するにつれ、状態管理は次第に複雑化し、いくつかの課題が浮上します。これらの課題を理解し、それに対処するための適切なツールや設計が求められます。
1. 状態の分散による管理の煩雑さ
大規模アプリケーションでは、状態が複数のコンポーネントやモジュール間に分散しがちです。その結果、以下の問題が発生します:
- 同期の難しさ: 状態の更新が複数の箇所で発生し、予期しない競合が発生する。
- デバッグの困難: 状態の流れが追いにくく、バグの特定が難しくなる。
2. パフォーマンスの低下
大規模アプリケーションでは、状態の更新が頻繁に行われるため、非効率な再レンダリングが発生する可能性があります。Reactコンポーネント全体が不要に再レンダリングされると、ユーザー体験に影響を与える可能性があります。
3. スケーラビリティの欠如
状態管理の設計がスケーラブルでない場合、以下のような課題に直面します:
- モジュール化の困難: 新しい機能を追加するたびに既存コードに手を加える必要がある。
- メンテナンス性の低下: チームが増員された場合に、状態管理の全体像を理解するコストが高くなる。
4. チーム間での理解不足
大規模プロジェクトでは複数の開発者が関与するため、状態管理の方法に統一性が欠けると混乱を招きます。
5. リアルタイム更新の管理
例えば、WebSocketやAPIを介したリアルタイムデータの更新を管理する場合、状態の整合性を維持することがさらに難しくなります。
Zustandがこれらの課題を解決する方法
Zustandは以下の特性により、大規模アプリケーションの課題を効果的に解決します:
- 簡単なモジュール化: 状態を「スライス」として分割し、各モジュールが独立して機能する設計が可能。
- 効率的な再レンダリング: 必要なコンポーネントだけが再レンダリングされる仕組みを採用。
- 柔軟なミドルウェア: 状態の変更や非同期操作を統一的に管理可能。
- React以外でも利用可能: 非React環境でも適用できるため、統一した状態管理が可能。
大規模アプリケーションでの状態管理は一筋縄ではいきませんが、Zustandのシンプルさとパフォーマンスを活用することで、効率的かつスケーラブルな設計が実現します。
Zustandを用いた設計パターン
Zustandは、シンプルかつ柔軟な設計が可能であるため、大規模アプリケーションでも効果的に活用できます。このセクションでは、Zustandを使った状態管理の設計パターンを紹介します。
1. スライスパターン
大規模アプリケーションでは、状態を機能ごとに分割する「スライス」パターンが有効です。このアプローチでは、各スライスが独立した状態とアクションを持ちます。これにより、コードが整理され、モジュール化が促進されます。
例: ユーザー管理と商品管理のスライス
import create from 'zustand';
const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
}));
const useProductStore = create((set) => ({
products: [],
addProduct: (product) =>
set((state) => ({ products: [...state.products, product] })),
clearProducts: () => set({ products: [] }),
}));
このように分割することで、各スライスが独立しており、チームでの開発が容易になります。
2. 状態のリスニング
状態変更を検知して特定の処理を実行するリスナーを追加するパターンです。useStore.subscribe
を使うことで、グローバルな副作用を簡単に管理できます。
例: ユーザーのログイン状態に応じたリスニング
const useUserStore = create((set) => ({
isLoggedIn: false,
setLoggedIn: (status) => set({ isLoggedIn: status }),
}));
useUserStore.subscribe(
(state) => state.isLoggedIn,
(isLoggedIn) => {
if (isLoggedIn) {
console.log('User logged in');
} else {
console.log('User logged out');
}
}
);
3. 非同期操作の統一管理
非同期操作(APIリクエストなど)はZustandでも簡単に扱えます。ミドルウェアや非同期関数を組み合わせて、状態とロジックを統一的に管理する方法が一般的です。
例: APIからデータを取得するアクション
const useDataStore = create((set) => ({
data: null,
fetchData: async () => {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
set({ data: result });
},
}));
4. コンポーネントごとの分離
各コンポーネントが必要な状態だけを取得するように設計します。この方法は、アプリケーションのパフォーマンスを向上させます。
例: 商品リストコンポーネント
function ProductList() {
const products = useProductStore((state) => state.products);
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
5. グローバル設定の共有
Zustandを使うことで、テーマや設定情報などのグローバルな状態を簡単に管理できます。
例: アプリケーションのテーマ設定
const useThemeStore = create((set) => ({
theme: 'light',
toggleTheme: () =>
set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
}));
function ThemeToggle() {
const theme = useThemeStore((state) => state.theme);
const toggleTheme = useThemeStore((state) => state.toggleTheme);
return (
<button onClick={toggleTheme}>
Current Theme: {theme}
</button>
);
}
まとめ
Zustandを用いた設計パターンは、アプリケーションの規模や要件に応じて柔軟に適用できます。スライスの導入、非同期操作の統一管理、リスニングの活用などを組み合わせることで、開発効率とコードの可読性を大幅に向上させることが可能です。
Zustandのミドルウェア活用
Zustandはミドルウェアを使用することで、状態管理をより柔軟かつ強力に拡張できます。ミドルウェアを活用すると、状態の永続化、デバッグ、ログ機能の追加などが可能です。このセクションでは、Zustandのミドルウェアを活用した具体的な方法を紹介します。
1. Zustandのミドルウェアとは
ミドルウェアは、状態の変更やアクションの実行を拡張または監視するための関数です。Zustandのcreate
関数にミドルウェアを渡すことで、状態管理の機能を強化できます。
2. ログ機能を追加する
状態変更時にログを出力するミドルウェアを実装します。この方法は、状態の変更履歴を追跡しやすくし、デバッグに役立ちます。
実装例: ログミドルウェア
import create from 'zustand';
const log = (config) => (set, get, api) =>
config(
(args) => {
console.log('Previous state:', get());
set(args);
console.log('Next state:', get());
},
get,
api
);
const useStore = create(
log((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
);
// 使用例
useStore.getState().increment();
// コンソールに状態変更が出力される
3. 状態の永続化
状態をブラウザのlocalStorage
やsessionStorage
に保存することで、ページのリロード後も状態を保持できます。
実装例: 永続化ミドルウェア
import create from 'zustand';
import { persist } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{
name: 'counter-storage', // Storageキー名
}
)
);
// 状態はlocalStorageに保存される
4. 状態変更の制限
特定の条件下でのみ状態変更を許可するミドルウェアを追加することも可能です。これにより、無効な状態変更を防ぎます。
実装例: 制限付きミドルウェア
const restrict = (config) => (set, get, api) =>
config(
(args) => {
const state = get();
if (state.count >= 0) {
set(args);
} else {
console.warn('Count cannot be negative');
}
},
get,
api
);
const useStore = create(
restrict((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
);
5. 非同期操作の統合
非同期操作を状態管理に組み込む際もミドルウェアが役立ちます。たとえば、APIリクエスト中にローディング状態を管理する場合に適しています。
実装例: 非同期操作ミドルウェア
const asyncMiddleware = (config) => (set, get, api) =>
config(
async (fn) => {
set({ isLoading: true });
await fn();
set({ isLoading: false });
},
get,
api
);
const useStore = create(
asyncMiddleware((set) => ({
data: null,
isLoading: false,
fetchData: async () => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
set({ data });
},
}))
);
6. デバッグ用ツールとの連携
Redux DevToolsのようなデバッグツールと連携するためのミドルウェアも提供されています。これにより、状態変更の監視やタイムトラベルデバッグが可能になります。
実装例: Redux DevToolsとの連携
import { devtools } from 'zustand/middleware';
const useStore = create(
devtools((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
);
まとめ
Zustandのミドルウェアを活用することで、ログ出力や状態の永続化、非同期操作の管理、デバッグツールの統合など、柔軟な状態管理が可能になります。これらの機能を適切に組み合わせることで、アプリケーションの規模や要件に応じた高度な状態管理が実現します。
Zustandでのコード分割戦略
大規模アプリケーションでは、状態管理のコードを適切に分割し、モジュール化することが重要です。Zustandは柔軟な設計が可能であり、コード分割によって保守性とスケーラビリティを向上させることができます。このセクションでは、Zustandを用いたコード分割戦略を具体例とともに解説します。
1. スライスを使ったモジュール化
Zustandのスライスパターンを活用することで、状態を機能ごとに分割できます。これにより、各スライスが独立しており、チーム開発時にも変更の影響範囲を抑えられます。
例: ユーザー情報スライスと商品情報スライス
// userSlice.js
export const createUserSlice = (set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
});
// productSlice.js
export const createProductSlice = (set) => ({
products: [],
addProduct: (product) =>
set((state) => ({ products: [...state.products, product] })),
clearProducts: () => set({ products: [] }),
});
複数のスライスを統合して1つのストアを作成します。
// store.js
import create from 'zustand';
import { createUserSlice } from './userSlice';
import { createProductSlice } from './productSlice';
const useStore = create((set) => ({
...createUserSlice(set),
...createProductSlice(set),
}));
export default useStore;
2. 機能ごとのストア分離
特定の機能やモジュールに固有の状態を、独立したストアとして分離する方法もあります。このアプローチは、グローバルな状態とローカルな状態を明確に区別するのに役立ちます。
例: 認証ストアと設定ストアの分離
// authStore.js
import create from 'zustand';
export const useAuthStore = create((set) => ({
isLoggedIn: false,
login: () => set({ isLoggedIn: true }),
logout: () => set({ isLoggedIn: false }),
}));
// settingsStore.js
import create from 'zustand';
export const useSettingsStore = create((set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}));
各ストアを特定のコンポーネントやモジュール内で使用できます。
3. 非同期データのモジュール化
非同期操作を伴うデータの管理もモジュール化できます。APIとのやり取りやキャッシュの管理を独立したストアで行うと、コードの整理が進みます。
例: 商品データ取得ストア
// productDataStore.js
import create from 'zustand';
export const useProductDataStore = create((set) => ({
products: [],
isLoading: false,
fetchProducts: async () => {
set({ isLoading: true });
const response = await fetch('/api/products');
const data = await response.json();
set({ products: data, isLoading: false });
},
}));
4. 再利用可能なカスタムフック
Zustandのストアをカスタムフックとしてラップすることで、コードの再利用性を向上させることができます。これにより、複数のコンポーネント間で簡単に状態を共有できます。
例: 認証状態を管理するカスタムフック
export const useAuth = () => {
const isLoggedIn = useAuthStore((state) => state.isLoggedIn);
const login = useAuthStore((state) => state.login);
const logout = useAuthStore((state) => state.logout);
return { isLoggedIn, login, logout };
};
5. 状態の依存性の分離
特定の状態が他の状態に依存する場合、状態の依存性を明確に定義することで、コードの可読性を高めます。
例: ログイン状態に応じたユーザー情報の管理
const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
const useAuthStore = create((set) => ({
isLoggedIn: false,
login: () => {
set({ isLoggedIn: true });
// ログイン時にユーザー情報を設定
useUserStore.getState().setUser({ name: 'John Doe', email: 'john@example.com' });
},
logout: () => {
set({ isLoggedIn: false });
// ログアウト時にユーザー情報をクリア
useUserStore.getState().setUser(null);
},
}));
まとめ
Zustandを用いたコード分割戦略は、状態のモジュール化、ストアの分離、依存性の明確化によって、開発効率を高めると同時にコードの保守性を向上させます。アプリケーションの規模に応じて適切な分割方法を選択し、効率的な開発を実現しましょう。
実装例:大規模アプリケーションでの適用
Zustandを使った状態管理は、大規模アプリケーションでもシンプルさと効率性を保てます。このセクションでは、実際の大規模アプリケーションでのZustandの適用例を紹介します。以下は、ECサイトを例にした実装例です。
1. アプリケーションの要件
ECサイトには以下の状態管理が必要です:
- ユーザー管理: ユーザーのログイン状態やプロフィール情報。
- 商品管理: 商品リストや商品詳細情報。
- カート管理: ショッピングカート内の商品情報。
- 注文履歴: 過去の購入履歴。
これらをZustandのスライスと独立したストアを組み合わせて管理します。
2. Zustandスライスの定義
ユーザーストア
// userSlice.js
export const createUserSlice = (set) => ({
user: null,
isLoggedIn: false,
login: (userData) => set({ user: userData, isLoggedIn: true }),
logout: () => set({ user: null, isLoggedIn: false }),
});
商品ストア
// productSlice.js
export const createProductSlice = (set) => ({
products: [],
selectedProduct: null,
fetchProducts: async () => {
const response = await fetch('/api/products');
const data = await response.json();
set({ products: data });
},
selectProduct: (product) => set({ selectedProduct: product }),
});
カートストア
// cartSlice.js
export const createCartSlice = (set) => ({
cartItems: [],
addToCart: (item) =>
set((state) => ({ cartItems: [...state.cartItems, item] })),
removeFromCart: (id) =>
set((state) => ({
cartItems: state.cartItems.filter((item) => item.id !== id),
})),
clearCart: () => set({ cartItems: [] }),
});
3. 統合ストアの作成
複数のスライスを統合して一つのストアを作成します。
// store.js
import create from 'zustand';
import { createUserSlice } from './userSlice';
import { createProductSlice } from './productSlice';
import { createCartSlice } from './cartSlice';
const useStore = create((set) => ({
...createUserSlice(set),
...createProductSlice(set),
...createCartSlice(set),
}));
export default useStore;
4. 実際の使用例
商品リストページ
import React, { useEffect } from 'react';
import useStore from './store';
function ProductList() {
const products = useStore((state) => state.products);
const fetchProducts = useStore((state) => state.fetchProducts);
useEffect(() => {
fetchProducts();
}, [fetchProducts]);
return (
<div>
<h1>Products</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}
export default ProductList;
ショッピングカートページ
import React from 'react';
import useStore from './store';
function Cart() {
const cartItems = useStore((state) => state.cartItems);
const removeFromCart = useStore((state) => state.removeFromCart);
return (
<div>
<h1>Shopping Cart</h1>
<ul>
{cartItems.map((item) => (
<li key={item.id}>
{item.name} - {item.price}
<button onClick={() => removeFromCart(item.id)}>Remove</button>
</li>
))}
</ul>
</div>
);
}
export default Cart;
ユーザー認証コンポーネント
import React from 'react';
import useStore from './store';
function UserAuth() {
const user = useStore((state) => state.user);
const isLoggedIn = useStore((state) => state.isLoggedIn);
const login = useStore((state) => state.login);
const logout = useStore((state) => state.logout);
return (
<div>
{isLoggedIn ? (
<div>
<p>Welcome, {user.name}</p>
<button onClick={logout}>Logout</button>
</div>
) : (
<button onClick={() => login({ name: 'John Doe' })}>Login</button>
)}
</div>
);
}
export default UserAuth;
5. アプリケーションの統合
これらのコンポーネントを統合し、React Routerなどを使用してアプリケーションを構築します。
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import ProductList from './ProductList';
import Cart from './Cart';
import UserAuth from './UserAuth';
function App() {
return (
<Router>
<UserAuth />
<Switch>
<Route path="/" exact component={ProductList} />
<Route path="/cart" component={Cart} />
</Switch>
</Router>
);
}
export default App;
まとめ
この実装例では、Zustandを用いてユーザー情報、商品データ、カート情報を分離し、再利用可能でスケーラブルな状態管理を実現しました。各スライスを独立して扱うことで、コードの保守性が向上し、大規模アプリケーションにおいても効率的な開発が可能になります。
トラブルシューティングとベストプラクティス
Zustandを使用した状態管理はシンプルですが、運用する中でいくつかの課題やトラブルに直面する可能性があります。このセクションでは、よくある問題とその解決策、さらにZustandを活用する上でのベストプラクティスを紹介します。
1. よくある問題と解決策
1.1 状態変更が反映されない
原因:
- コンポーネントが正しく状態を監視していない。
- 関連する状態を直接操作しようとしている。
解決策:
- Zustandのフックを正しく使用して状態を取得します。
- 状態の更新は必ず
set
関数を通じて行います。
例:
const count = useStore((state) => state.count); // 正しい方法
useStore.getState().count = 10; // NG:これでは再レンダリングが発生しません
1.2 状態のリセットが複雑になる
原因:
- 状態が複数のスライスに分割されている場合、リセット操作が煩雑になる。
解決策:
- 状態リセット用の関数を統一的に設計します。
例:
const useStore = create((set) => ({
count: 0,
reset: () => set({ count: 0 }),
}));
1.3 パフォーマンスの問題(不要な再レンダリング)
原因:
- 状態変更がすべてのコンポーネントに影響を与えている。
解決策:
- Zustandのセレクターを活用して、必要な状態だけを取得するようにします。
例:
const count = useStore((state) => state.count); // 必要な状態のみを取得
2. ベストプラクティス
2.1 状態のスライス化を徹底する
アプリケーションの状態をモジュールごとにスライス化することで、コードの可読性と保守性が向上します。また、各スライスを独立してテストできるため、開発効率も向上します。
例:
const createUserSlice = (set) => ({
user: null,
setUser: (user) => set({ user }),
});
const createProductSlice = (set) => ({
products: [],
setProducts: (products) => set({ products }),
});
2.2 再利用可能なカスタムフックを作成
状態管理ロジックをカスタムフックとして抽象化することで、複数のコンポーネント間で再利用可能になります。
例:
export const useUser = () => {
const user = useStore((state) => state.user);
const setUser = useStore((state) => state.setUser);
return { user, setUser };
};
2.3 非同期操作とローディング状態の管理
APIコールや非同期操作を扱う際、ローディング状態を状態に組み込むと、状態の一貫性が向上します。
例:
const useStore = create((set) => ({
isLoading: false,
fetchData: async () => {
set({ isLoading: true });
const data = await fetch('/api/data').then((res) => res.json());
set({ data, isLoading: false });
},
}));
2.4 状態の永続化を検討する
状態をlocalStorage
やsessionStorage
に永続化すると、ページリロード後も状態を保持できます。
例:
import { persist } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{ name: 'app-storage' }
)
);
2.5 デバッグツールを活用する
Redux DevToolsなどのデバッグツールと連携すると、状態の変更履歴を簡単に追跡できます。
例:
import { devtools } from 'zustand/middleware';
const useStore = create(
devtools((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
);
3. トラブルを未然に防ぐ設計のポイント
- 状態の最小化: 必要最小限の状態だけを管理し、冗長な状態を避ける。
- 状態の依存関係を整理: 状態間の依存を明確にし、過剰な結合を防ぐ。
- 開発初期に設計を検討: 状態管理の設計を開発の初期段階で計画し、将来の拡張性を確保する。
まとめ
Zustandを用いた状態管理は、軽量でシンプルですが、設計の工夫やトラブルシューティングが重要です。本セクションで紹介した解決策とベストプラクティスを参考に、効率的でスケーラブルな状態管理を実現しましょう。
まとめ
本記事では、Zustandを活用した大規模Reactアプリケーションの状態管理について解説しました。Zustandはそのシンプルさと柔軟性から、小規模から大規模まで幅広いプロジェクトで利用可能です。スライスパターンやミドルウェアの活用、コード分割戦略、非同期操作の統一管理などの方法を通じて、開発効率と保守性を大幅に向上させられます。
状態管理はアプリケーションの根幹を支える重要な要素です。Zustandの導入により、複雑な状態管理を効率的に解決し、スケーラブルで信頼性の高いReactアプリケーションを構築しましょう。
コメント