Reactアプリケーションを構築する際、状態管理はアプリ全体の可読性や保守性に直結する重要な要素です。シンプルなアプリではuseStateで十分な場合もありますが、状態が複雑になると、より強力なツールが必要となります。そこで登場するのがuseReducerです。このフックは、複数の状態を効率的に管理し、状態遷移を明確にするのに役立ちます。
さらに、TypeScriptを組み合わせることで、型安全性を確保しながら開発を進めることが可能になります。本記事では、useReducerの基本的な使い方から、TypeScriptを使った型定義、実践例、デバッグ、パフォーマンス最適化まで、包括的に解説します。React開発における状態管理のスキルをレベルアップさせるための完全ガイドとして、ぜひ最後までお読みください。
useReducerの基本概念
useReducerは、Reactの組み込みフックで、複雑な状態管理を簡潔かつ効率的に行うためのツールです。useStateと似た機能を持ちながら、状態遷移ロジックをReducer関数に分離することで、コードの可読性と再利用性を向上させます。
useReducerの構文
useReducerは以下のシンプルな構文で利用できます:
const [state, dispatch] = useReducer(reducer, initialState);
reducer
: 現在の状態とアクションを受け取り、新しい状態を返す純粋関数。initialState
: 状態の初期値。state
: 現在の状態。dispatch
: アクションをトリガーするための関数。
Reducer関数の役割
Reducer関数は、状態遷移のロジックを集中管理する役割を担います。以下は典型的なReducer関数の例です:
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};
シンプルなuseReducerの例
以下はカウンターアプリにuseReducerを使用した例です:
import React, { useReducer } from 'react';
const initialState = { count: 0 };
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
throw new Error('Unknown action type');
}
};
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</div>
);
}
export default Counter;
このようにuseReducerを使うと、状態遷移のロジックをReducer関数内にまとめることで、状態管理がシンプルで追跡しやすくなります。次のセクションでは、これにTypeScriptを組み合わせて型安全性を高める方法を解説します。
TypeScriptでの型定義
useReducerをTypeScriptで使用することで、型安全性を高め、開発中のエラーを未然に防ぐことができます。特に、状態の型やアクションの型を明示的に定義することで、状態遷移ロジックの誤りを回避できます。
Reducer関数の型定義
Reducer関数にTypeScriptの型を付ける際、以下のポイントを定義します:
- State型: 状態を表すオブジェクトの型。
- Action型: アクションオブジェクトの型。
以下は型定義を含むReducer関数の例です:
type State = {
count: number;
};
type Action =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};
useReducerの型付け
useReducerを呼び出す際に型定義を適用します。以下は、型定義を反映したuseReducerの使用例です:
import React, { useReducer } from 'react';
type State = {
count: number;
};
type Action =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' };
const initialState: State = { count: 0 };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</div>
);
};
export default Counter;
型定義による利点
- 型安全性: 状態やアクションの型が明確になるため、間違った型を渡した場合にコンパイルエラーが発生します。
- 可読性の向上: 型定義があることで、状態やアクションの構造が一目で分かります。
- 開発効率の向上: 型補完機能が有効になるため、アクションの種類やプロパティが自動補完されます。
次のセクションでは、この型定義を活用し、具体的なアプリケーション例を通じてuseReducerとTypeScriptの応用を解説します。
実用例:フォーム状態管理
useReducerとTypeScriptを活用したフォーム状態管理の実装例を紹介します。この方法は、複数の入力フィールドを持つフォームの状態を効率的に管理するのに適しています。
フォームの構造と状態の定義
まず、フォームの状態とアクションを型で定義します。
type FormState = {
name: string;
email: string;
password: string;
};
type FormAction =
| { type: 'SET_NAME'; payload: string }
| { type: 'SET_EMAIL'; payload: string }
| { type: 'SET_PASSWORD'; payload: string }
| { type: 'RESET' };
const initialState: FormState = {
name: '',
email: '',
password: '',
};
Reducer関数の実装
Reducer関数で状態遷移ロジックを記述します。
const formReducer = (state: FormState, action: FormAction): FormState => {
switch (action.type) {
case 'SET_NAME':
return { ...state, name: action.payload };
case 'SET_EMAIL':
return { ...state, email: action.payload };
case 'SET_PASSWORD':
return { ...state, password: action.payload };
case 'RESET':
return initialState;
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};
useReducerでフォーム状態を管理
useReducerを利用してフォーム状態を管理し、入力イベントでdispatchを呼び出します。
import React, { useReducer } from 'react';
const FormComponent = () => {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Form Data:', state);
dispatch({ type: 'RESET' }); // フォームのリセット
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>
Name:
<input
type="text"
value={state.name}
onChange={(e) =>
dispatch({ type: 'SET_NAME', payload: e.target.value })
}
/>
</label>
</div>
<div>
<label>
Email:
<input
type="email"
value={state.email}
onChange={(e) =>
dispatch({ type: 'SET_EMAIL', payload: e.target.value })
}
/>
</label>
</div>
<div>
<label>
Password:
<input
type="password"
value={state.password}
onChange={(e) =>
dispatch({ type: 'SET_PASSWORD', payload: e.target.value })
}
/>
</label>
</div>
<button type="submit">Submit</button>
</form>
);
};
export default FormComponent;
ポイント解説
- 状態の一元管理: 複数の入力フィールドの状態を一つのReducer関数で管理できるため、コードが簡潔になります。
- 型安全性の向上: TypeScriptを活用することで、入力フィールドの更新に誤ったデータ型を渡すミスを防げます。
- 拡張性: 新しいフィールドを追加する際も、型定義とReducerに少しの修正を加えるだけで対応可能です。
このように、useReducerとTypeScriptを使うことで、フォーム状態を効率的かつ型安全に管理することが可能になります。次のセクションでは、複雑なロジックを分割して管理する方法を解説します。
複雑なロジックの分割と管理
アプリケーションが大規模化すると、状態管理のロジックも複雑になります。このセクションでは、useReducerを用いた複雑な状態管理ロジックの分割と効率的な管理方法を解説します。
Reducer関数の分割
1つの巨大なReducer関数は、管理が難しく、バグの原因になります。状態を複数の部分に分割し、それぞれ専用のReducerを作成して組み合わせることが推奨されます。
以下は、複雑なフォームの状態を複数のReducerで管理する例です。
// 個別のReducerを定義
const nameReducer = (state: string, action: { type: string; payload: string }) => {
switch (action.type) {
case 'SET_NAME':
return action.payload;
default:
return state;
}
};
const emailReducer = (state: string, action: { type: string; payload: string }) => {
switch (action.type) {
case 'SET_EMAIL':
return action.payload;
default:
return state;
}
};
// combineReducers関数を作成
const combineReducers = (reducers: any) => {
return (state: any, action: any) => {
const newState: any = {};
for (let key in reducers) {
newState[key] = reducers[key](state[key], action);
}
return newState;
};
};
// combineReducersで統合
const rootReducer = combineReducers({
name: nameReducer,
email: emailReducer,
});
useReducerで統合Reducerを利用
分割したReducerを統合し、useReducerで使用します。
import React, { useReducer } from 'react';
const initialState = {
name: '',
email: '',
};
const FormComponent = () => {
const [state, dispatch] = useReducer(rootReducer, initialState);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Form Data:', state);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>
Name:
<input
type="text"
value={state.name}
onChange={(e) =>
dispatch({ type: 'SET_NAME', payload: e.target.value })
}
/>
</label>
</div>
<div>
<label>
Email:
<input
type="email"
value={state.email}
onChange={(e) =>
dispatch({ type: 'SET_EMAIL', payload: e.target.value })
}
/>
</label>
</div>
<button type="submit">Submit</button>
</form>
);
};
export default FormComponent;
分割の利点
- モジュール化: Reducerを機能ごとに分割することで、再利用性が向上します。
- テストの容易さ: 小さなReducer関数は、単体テストを簡単に行えます。
- メンテナンス性の向上: 状態管理ロジックが明確になり、変更箇所を特定しやすくなります。
注意点
- Reducerを分割する際、状態間の依存関係が強い場合は、統合が複雑になる可能性があります。このような場合は、状態設計や依存関係を再評価することが重要です。
このように、Reducer関数を分割することで、複雑な状態管理ロジックを効率的に扱うことができます。次のセクションでは、状態遷移図を活用して状態管理をさらに直感的に行う方法を解説します。
状態遷移図とその応用
状態遷移図は、アプリケーションの状態とその遷移を視覚的に表現する手法です。useReducerで状態管理を行う場合、状態遷移図を利用すると、複雑なロジックを整理しやすくなり、バグの防止にも役立ちます。
状態遷移図とは
状態遷移図は、状態(State)とアクション(Action)間の関係をノードと矢印で表現した図です。状態がどのような条件で遷移するのかを視覚的に示します。
以下は、シンプルなフォームの状態遷移図の例です:
状態: 'IDLE' -> アクション: 'SUBMIT' -> 状態: 'SUBMITTING'
状態: 'SUBMITTING' -> アクション: 'SUCCESS' -> 状態: 'SUCCESS'
状態: 'SUBMITTING' -> アクション: 'ERROR' -> 状態: 'ERROR'
状態遷移図をReducerに反映
状態遷移図を元にReducerを実装します。
type State = 'IDLE' | 'SUBMITTING' | 'SUCCESS' | 'ERROR';
type Action =
| { type: 'SUBMIT' }
| { type: 'SUCCESS' }
| { type: 'ERROR' };
const reducer = (state: State, action: Action): State => {
switch (state) {
case 'IDLE':
if (action.type === 'SUBMIT') return 'SUBMITTING';
break;
case 'SUBMITTING':
if (action.type === 'SUCCESS') return 'SUCCESS';
if (action.type === 'ERROR') return 'ERROR';
break;
default:
return state;
}
throw new Error(`Unhandled state transition: ${state} -> ${action.type}`);
};
視覚化ツールの活用
状態遷移図を視覚化するために、ツールを活用するのもおすすめです。以下は一般的なツールの例です:
- XState Visualizer: 状態遷移を視覚的に確認できます。
- PlantUML: コードで状態遷移図を記述してレンダリングします。
例:PlantUMLでの状態遷移図記述
@startuml
[*] --> IDLE
IDLE --> SUBMITTING : SUBMIT
SUBMITTING --> SUCCESS : SUCCESS
SUBMITTING --> ERROR : ERROR
@enduml
応用例:フォームの状態遷移
フォーム送信の状態遷移を管理する例を以下に示します。
import React, { useReducer } from 'react';
const FormComponent = () => {
const [state, dispatch] = useReducer(reducer, 'IDLE');
const handleSubmit = () => {
dispatch({ type: 'SUBMIT' });
setTimeout(() => {
Math.random() > 0.5
? dispatch({ type: 'SUCCESS' })
: dispatch({ type: 'ERROR' });
}, 2000);
};
return (
<div>
<p>Current State: {state}</p>
<button onClick={handleSubmit} disabled={state === 'SUBMITTING'}>
{state === 'SUBMITTING' ? 'Submitting...' : 'Submit'}
</button>
</div>
);
};
export default FormComponent;
状態遷移図のメリット
- 視覚的理解: 状態遷移が一目でわかり、仕様の確認が容易。
- バグ防止: 未定義の状態遷移を防ぎやすくなる。
- チーム間の共有: 複雑な状態ロジックを明確に共有できる。
状態遷移図を使うことで、useReducerによる状態管理がさらに直感的で強力なものになります。次のセクションでは、デバッグとテストのベストプラクティスを紹介します。
デバッグとテストのベストプラクティス
useReducerを利用した状態管理では、デバッグとテストが品質を保つために重要な役割を果たします。このセクションでは、効率的にReducerの動作を確認する方法とテストのベストプラクティスを解説します。
Reducerのデバッグ方法
1. console.logを活用する
Reducer関数内で、状態遷移やアクション内容を確認できます。開発初期の迅速なデバッグに役立ちます。
const reducer = (state: State, action: Action): State => {
console.log('Current State:', state);
console.log('Dispatched Action:', action);
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};
2. ブラウザのデバッグツールを活用
- React Developer Tools: useReducerの状態をコンポーネントツリーで確認可能。
- Redux DevTools: カスタムミドルウェアを用いるとuseReducerの状態とアクションをRedux DevToolsで可視化できます。
3. 状態遷移ログを自動化
デバッグ用のミドルウェアを作成し、状態遷移ログを自動記録する仕組みを実装します。
const loggingMiddleware = (reducer: any) => {
return (state: any, action: any) => {
console.log('Previous State:', state);
console.log('Action:', action);
const newState = reducer(state, action);
console.log('Next State:', newState);
return newState;
};
};
const enhancedReducer = loggingMiddleware(reducer);
const [state, dispatch] = useReducer(enhancedReducer, initialState);
Reducerのテスト手法
テストはReducerが正しく動作することを保証します。以下に主要なテスト手法を示します。
1. 単体テスト
Reducer関数は純粋関数であるため、単体テストが非常に効果的です。
import { describe, it, expect } from 'jest';
const initialState = { count: 0 };
describe('Reducer Tests', () => {
it('should increment the count', () => {
const action = { type: 'INCREMENT' };
const state = reducer(initialState, action);
expect(state).toEqual({ count: 1 });
});
it('should decrement the count', () => {
const action = { type: 'DECREMENT' };
const state = reducer(initialState, action);
expect(state).toEqual({ count: -1 });
});
it('should throw an error for unknown actions', () => {
const action = { type: 'UNKNOWN' };
expect(() => reducer(initialState, action)).toThrow();
});
});
2. 統合テスト
コンポーネントとReducerの動作を統合的にテストします。
import { render, fireEvent, screen } from '@testing-library/react';
import Counter from './Counter';
it('should increment and decrement count', () => {
render(<Counter />);
const incrementButton = screen.getByText('+');
const decrementButton = screen.getByText('-');
fireEvent.click(incrementButton);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
fireEvent.click(decrementButton);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
3. 境界ケースのテスト
- 状態が空の場合の動作確認。
- 想定外のアクションに対するエラーハンドリング。
ベストプラクティス
- エラーハンドリングの実装: Reducer関数内で未定義のアクションを適切に処理する。
- 型安全性の保証: TypeScriptを使用して、アクションと状態の型を明確に定義。
- テストカバレッジの確保: 単体テストと統合テストの両方を組み合わせて実施する。
デバッグとテストを適切に行うことで、useReducerを使った状態管理の品質と信頼性を大幅に向上させることができます。次のセクションでは、外部ライブラリとの連携について解説します。
外部ライブラリとの連携
useReducerを活用する際、外部ライブラリと組み合わせることで、より高度な状態管理やデータ取得を効率化できます。このセクションでは、React QueryやRedux ToolkitなどのライブラリとuseReducerを併用する方法を解説します。
React Queryとの連携
React Queryは、データ取得やキャッシュ管理を効率化するライブラリです。useReducerと併用することで、ローカル状態とサーバーサイドデータを統合的に管理できます。
実装例:APIデータの管理
useReducerをローカル状態の管理に使用し、React QueryでAPIデータを取得する例を示します。
import React, { useReducer } from 'react';
import { useQuery } from '@tanstack/react-query';
type State = {
filter: string;
};
type Action = { type: 'SET_FILTER'; payload: string };
const initialState: State = { filter: '' };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'SET_FILTER':
return { ...state, filter: action.payload };
default:
throw new Error('Unknown action type');
}
};
const fetchItems = async (filter: string) => {
const response = await fetch(`/api/items?filter=${filter}`);
return response.json();
};
const ItemList = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { data, isLoading, error } = useQuery(['items', state.filter], () =>
fetchItems(state.filter)
);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error loading data</p>;
return (
<div>
<input
type="text"
value={state.filter}
onChange={(e) => dispatch({ type: 'SET_FILTER', payload: e.target.value })}
/>
<ul>
{data.map((item: any) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
};
export default ItemList;
ポイント
- useReducer: ローカルの状態(フィルタ)を管理。
- React Query: サーバーサイドデータの取得とキャッシュ管理を担当。
Redux Toolkitとの連携
useReducerで管理するローカル状態を、Redux Toolkitのグローバルストアと組み合わせることで、状態のスコープを柔軟に設定できます。
実装例:ローカルとグローバル状態の併用
フォームの入力状態はuseReducerで管理し、送信後のグローバル状態はReduxで管理します。
import React, { useReducer } from 'react';
import { useDispatch } from 'react-redux';
import { saveFormData } from './formSlice';
type FormState = {
name: string;
email: string;
};
type FormAction =
| { type: 'SET_NAME'; payload: string }
| { type: 'SET_EMAIL'; payload: string };
const initialState: FormState = { name: '', email: '' };
const formReducer = (state: FormState, action: FormAction): FormState => {
switch (action.type) {
case 'SET_NAME':
return { ...state, name: action.payload };
case 'SET_EMAIL':
return { ...state, email: action.payload };
default:
throw new Error('Unknown action type');
}
};
const FormComponent = () => {
const [state, dispatch] = useReducer(formReducer, initialState);
const reduxDispatch = useDispatch();
const handleSubmit = () => {
reduxDispatch(saveFormData(state));
console.log('Form submitted:', state);
};
return (
<form onSubmit={(e) => e.preventDefault()}>
<input
type="text"
value={state.name}
onChange={(e) => dispatch({ type: 'SET_NAME', payload: e.target.value })}
/>
<input
type="email"
value={state.email}
onChange={(e) => dispatch({ type: 'SET_EMAIL', payload: e.target.value })}
/>
<button onClick={handleSubmit}>Submit</button>
</form>
);
};
export default FormComponent;
ポイント
- useReducer: ローカルな入力状態の管理に特化。
- Redux Toolkit: フォームデータをグローバルストアに保存。
外部ライブラリとの連携のメリット
- 役割の分担: useReducerでローカル状態を、React QueryやReduxでグローバルなデータを管理。
- 拡張性: ライブラリを組み合わせることで、状態管理の規模や要件に応じた柔軟な対応が可能。
- 効率化: React Queryのキャッシュ機能やRedux Toolkitの効率的な状態操作を活用できる。
外部ライブラリとuseReducerを効果的に組み合わせることで、アプリケーションの状態管理をさらに強化できます。次のセクションでは、パフォーマンス最適化の方法を解説します。
パフォーマンス最適化
useReducerを活用した状態管理では、適切な最適化を行うことで、アプリケーションのパフォーマンスを向上させることができます。このセクションでは、一般的なパフォーマンスの課題とその解決方法について解説します。
1. 不要な再レンダリングの防止
useReducerの状態が更新されるたびに、コンポーネントが再レンダリングされます。再レンダリングを必要最小限に抑えるためのテクニックを紹介します。
React.memoの活用
状態に依存しないコンポーネントをReact.memo
でラップすることで、不要な再レンダリングを防ぎます。
const ChildComponent = React.memo(({ value }: { value: number }) => {
console.log('Rendering ChildComponent');
return <p>Value: {value}</p>;
});
useCallbackでの関数再生成の防止
ディスパッチ関数をトリガーするコールバックが頻繁に再生成されないよう、useCallback
を使用します。
const handleIncrement = useCallback(() => {
dispatch({ type: 'INCREMENT' });
}, [dispatch]);
2. 状態分割で再レンダリングを最小化
状態が大きすぎる場合、useReducerの状態の一部だけが変更されても、関連するすべてのコンポーネントが再レンダリングされる可能性があります。状態を細かく分割して管理することで、影響を限定できます。
例:複数のReducerでの状態分割
const countReducer = (state: number, action: { type: string }) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
const nameReducer = (state: string, action: { type: string; payload: string }) => {
switch (action.type) {
case 'SET_NAME':
return action.payload;
default:
return state;
}
};
// 各Reducerを個別にuseReducerで管理
const [count, dispatchCount] = useReducer(countReducer, 0);
const [name, dispatchName] = useReducer(nameReducer, '');
3. コンポーネントの遅延レンダリング
状態に依存するコンポーネントを必要なタイミングでレンダリングすることで、パフォーマンスを向上させます。
{state.showDetails && <DetailsComponent />}
4. 遅延初期化
useReducerの初期化関数に遅延初期化を使用して、初期化コストを削減します。
const init = (initialCount: number) => {
return { count: initialCount };
};
const [state, dispatch] = useReducer(reducer, 0, init);
5. useReducerのロジックの最適化
Reducer内のロジックが複雑であると、更新のたびに時間がかかります。計算を最小限に抑えるようリファクタリングします。
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'INCREMENT':
// 計算を事前にキャッシュ
const incrementedValue = state.count + 1;
return { ...state, count: incrementedValue };
default:
return state;
}
};
6. メモリ消費の削減
useReducerで管理する状態が大きい場合、必要な部分だけを保持するよう設計することで、メモリの消費を抑えます。
const reducer = (state: any, action: any) => {
switch (action.type) {
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] };
case 'CLEAR_ITEMS':
return { ...state, items: [] }; // 明確に不要データを削除
default:
return state;
}
};
パフォーマンス最適化のポイントまとめ
- 再レンダリングの最小化:
React.memo
やuseCallback
を活用。 - 状態分割: 状態を細分化して管理し、影響範囲を限定。
- 遅延処理: 初期化やレンダリングを必要なタイミングに限定。
- Reducerの簡略化: 複雑なロジックは事前計算や関数分割で軽減。
これらのテクニックを駆使することで、useReducerを用いたアプリケーションでも高いパフォーマンスを実現できます。次のセクションでは、全体のまとめを行います。
まとめ
本記事では、ReactでのuseReducerとTypeScriptを活用した高度な状態管理について解説しました。useReducerの基本的な構文やTypeScriptによる型定義、実用的なフォーム管理の例から、複雑なロジックの分割、状態遷移図を使った視覚化、デバッグやテストのベストプラクティス、さらに外部ライブラリとの連携やパフォーマンス最適化まで幅広く取り上げました。
これらのテクニックを活用することで、アプリケーションの規模が大きくなっても、状態管理を効率的に行えるようになります。useReducerとTypeScriptを組み合わせることで、型安全性と柔軟性を兼ね備えた高品質なコードを構築できます。状態管理のスキルを向上させ、Reactプロジェクトの可能性をさらに広げてください。
コメント