はじめに
ReactのHooksの登場以降、ステート共有の一般的な方法としてはカスタムフックの方がより推奨されることが多いです。
カスタムフックを使うと、高階関数よりもさらに柔軟で、コンポーネントのネストも深くならないため、よりシンプルにロジックを再利用できます。
以下に、カスタムフックを使って前の例と同じステート共有を実現するサンプルコードを紹介します。
実装
import React, { useState, useCallback } from 'react';
// カウンターのステートと更新ロジックを提供するカスタムフック
const useCounter = () => {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
const decrement = useCallback(() => {
setCount((prevCount) => prevCount - 1);
}, []);
return { count, increment, decrement };
};
// カウンターを表示する子コンポーネント
const CounterDisplay = () => {
const { count } = useCounter();
return <div>現在のカウント: {count}</div>;
};
// カウントを増やすボタンを持つ子コンポーネント
const IncrementButton = () => {
const { increment } = useCounter();
return <button onClick={increment}>増やす</button>;
};
// カウントを減らすボタンを持つ子コンポーネント
const DecrementButton = () => {
const { decrement } = useCounter();
return <button onClick={decrement}>減らす</button>;
};
// 親コンポーネント
const ParentComponent = () => {
return (
<div>
<CounterDisplay />
<IncrementButton />
<DecrementButton />
</div>
);
};
export default ParentComponent;
コードの説明:
-
useCounterカスタムフック:- この関数は、Reactのフック (
useState,useCallback) を内部で使用しています。 -
countステートと、それを更新するincrementおよびdecrement関数を定義しています。 - これらの値と関数をオブジェクトとして返します。
- この関数は、Reactのフック (
-
CounterDisplayコンポーネント:-
useCounterフックを呼び出し、返されたcountを受け取って表示します。
-
-
IncrementButtonコンポーネント:-
useCounterフックを呼び出し、返されたincrement関数をボタンのonClickハンドラに設定します。
-
-
DecrementButtonコンポーネント:-
useCounterフックを呼び出し、返されたdecrement関数をボタンのonClickハンドラに設定します。
-
-
ParentComponent:- 子コンポーネント (
CounterDisplay,IncrementButton,DecrementButton) を直接レンダリングします。
- 子コンポーネント (
カスタムフックの利点:
- シンプルで直感的: コンポーネント内で必要なステートや関数を直接フックから取得するため、propsの受け渡しを意識する必要が減ります。
- 柔軟性: 複数の関連するロジックを1つのフックにまとめることができ、コンポーネントは必要なものだけを選択して利用できます。
- 再利用性の向上: ロジックをコンポーネントのレンダーパスから分離できるため、より純粋なJavaScriptの関数としてテストしやすくなります。
- コンポーネントのネストが浅くなる: 高階関数のようにコンポーネントをラップする必要がないため、コンポーネントツリーがよりシンプルになります。
カスタムフックの注意点:
- カスタムフックは、Reactの関数型コンポーネントまたは別のカスタムフック内でのみ呼び出すことができます(Hookのルール)。
- 同じカスタムフックを複数のコンポーネントで呼び出した場合、それぞれのコンポーネントは独立したステートを持ちます。上記の例では、
CounterDisplay、IncrementButton、DecrementButtonはそれぞれuseCounter()を呼び出しているので、実際にはそれぞれが独立したcountステートを持つことになります。
ステートを共有するためのカスタムフック:
もし、複数のコンポーネント間で同じステートを共有したい場合は、カスタムフック内で useState を一度だけ呼び出し、そのステートと更新関数を複数のコンポーネントに提供する必要があります。これを行う一般的なパターンは、カスタムフック内でコンテキスト (React.createContext と useContext) を利用する方法です。
import React, { useState, useCallback, createContext, useContext } from 'react';
// カウンターのコンテキストを作成
const CounterContext = createContext(null);
// カウンターのステートと更新ロジックを提供するプロバイダー
const CounterProvider = ({ children }) => {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
const decrement = useCallback(() => {
setCount((prevCount) => prevCount - 1);
}, []);
return (
<CounterContext.Provider value={{ count, increment, decrement }}>
{children}
</CounterContext.Provider>
);
};
// カウンターのステートと関数を利用するためのカスタムフック
const useCounterContext = () => {
const context = useContext(CounterContext);
if (!context) {
throw new Error("useCounterContext must be used within a CounterProvider");
}
return context;
};
// カウンターを表示する子コンポーネント
const CounterDisplay = () => {
const { count } = useCounterContext();
return <div>現在のカウント: {count}</div>;
};
// カウントを増やすボタンを持つ子コンポーネント
const IncrementButton = () => {
const { increment } = useCounterContext();
return <button onClick={increment}>増やす</button>;
};
// カウントを減らすボタンを持つ子コンポーネント
const DecrementButton = () => {
const { decrement } = useCounterContext();
return <button onClick={decrement}>減らす</button>;
};
// 親コンポーネント
const ParentComponent = () => {
return (
<CounterProvider>
<CounterDisplay />
<IncrementButton />
<DecrementButton />
</CounterProvider>
);
};
export default ParentComponent;
この例では、CounterProvider が count ステートと更新関数を管理し、CounterContext.Provider を通じてその値を子コンポーネントに提供します。useCounterContext カスタムフックは、このコンテキストの値を利用するための便利なインターフェースを提供します。これにより、CounterDisplay, IncrementButton, DecrementButton は同じ count ステートを共有し、ボタンの操作によってすべてのコンポーネントの表示が更新されます。
このように、カスタムフックとコンテキストを組み合わせることで、より効率的かつ柔軟にコンポーネント間でステートを共有することができます。大規模なアプリケーションでは、このパターンやより高度なステート管理ライブラリの利用が検討されます。
推奨ファイル構成
カスタムフックとコンテキストのデザインパターンでは、コンポーネント分割もよりしやすくなる為、以下のようなファイル分けが可能になり、長文ファイルになる事態を避けることも可能となります。
src/
├── contexts/
│ └── SampleContext.tsx
├── hooks/
│ └── useSampleState.tsx
├── providers/
│ └── SampleProvider.tsx
└── components/
├── SampleComponent.tsx
└── OtherComponent.tsx
1. contexts/: コンテキストの定義
// contexts/SampleContext.tsx
import { createContext } from 'react';
export type SampleContextType = {
value: string;
setValue: (value: string) => void;
};
export const SampleContext = createContext<SampleContextType | undefined>(undefined);
2. hooks/: カスタムフックの実装
// hooks/useSampleState.tsx
import { useContext } from 'react';
import { SampleContext, SampleContextType } from '../contexts/SampleContext';
export const useSampleState = (): SampleContextType => {
const context = useContext(SampleContext);
if (!context) {
throw new Error('useSampleState must be used within a SampleProvider');
}
return context;
};
3. providers/: Providerコンポーネントの実装
// providers/SampleProvider.tsx
import { ReactNode, useState } from 'react';
import { SampleContext } from '../contexts/SampleContext';
type Props = {
children: ReactNode;
};
export const SampleProvider = ({ children }: Props) => {
const [value, setValue] = useState('');
return (
<SampleContext.Provider value={{ value, setValue }}>
{children}
</SampleContext.Provider>
);
};
4. components/: 実際に使用するコンポーネント
// components/SampleComponent.tsx
import { useSampleState } from '../hooks/useSampleState';
export const SampleComponent = () => {
const { value, setValue } = useSampleState();
return (
<div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</div>
);
};
このような構成のメリット:
1. 関心の分離:
- コンテキスト定義
- ステート管理ロジック
- UIコンポーネント
がそれぞれ独立して管理できます。
2. 再利用性の向上:
カスタムフックとして切り出すことで、他のプロジェクトでも再利用が容易になります。
3. テスタビリティの向上:
各層が分離されているため、ユニットテストが書きやすくなります。
4. 保守性の向上:
ファイルの役割が明確に分かれているため、コードの変更や機能追加が容易になります。
使用例:
// App.tsx
function App() {
return (
<SampleProvider>
<SampleComponent />
<OtherComponent />
</SampleProvider>
);
}
このパターンを使用することで、コードの見通しが良くなり、チーム開発での協業もスムーズになります。