原則として状態は親に持つ
- 投稿日:
Next.jsなどの画面をはじめとしたプログラミング設計において、状態管理は非常に重要な要素だ。特に、複雑なアプリケーションでは、状態がどこに存在するかがコードの可読性や保守性に大きな影響を与える。
基本的に状態は親にすべて望むのが望ましい。これは子が状態を持っている場合に、親がこれを利用することが困難だからだ。例えば以下のようなコンポーネント構造において、子コンポーネントAでAPIをコールし、その結果を持っているとする。仕様変更でこのデータを子コンポーネントBでも利用したいとなった場合、子コンポーネントAから状態を引きはがす必要があるが、実装によっては状態と実装が密結合になっており、特にスパゲッティ化しているコードでは容易に引きはがせないこともあるだろう。
親コンポーネント
├子コンポーネントA
└子コンポーネントB
しかし親であらかじめ状態を持っておけば、このような苦労は不要になる。子AでAPIをコールした結果を親で持っておけば、それをそのままBに引き渡せばいいだけだからだ。
これは信頼できる唯一の情報源にも通じる話で、情報源が分散していると誤って同じ状態を二つ持ってしまったり、実装が複雑化してしまうことがある。酷い場合では子コンポーネントAと親の両方でAPIをコールするような実装もあるだろう。APIコールのタイミングがずれることで期待した通りの結果が得られなかったり、通信が増えることでロードが増えUXを毀損したり、APサーバーの負荷が高まる懸念もある。
基本的に同じ責務を持つ実装はDRYであることが望ましいので、このような悲劇を生まないためにも状態は親で持っておくことが重要だと考える。
逆に責務の範囲に閉じることができるのであれば、子で持ってよいケースもある。例えば親画面から子画面を出すときに、子画面だけに状態をもっていたいケースがある。こういう場合には子に一時的な状態を持つのはありだろう。例えば以下のような保存ボタンを押したときにモーダルの内容を確定するようなものであれば、モーダル内の状態として持ってよい。これは親で使わないからだ。その代わり画面状態の初期化時は親から状態を渡してやり、保存ボタンを押したときはコールバックで通知するという構造だ。
type HogeModalProps = {
shown: boolean;
hoge: string;
onClose: () => void;
onApplyClose: (state: { hoge: string }) => void;
};
export const HogeModal = (props: HogeModalProps) => {
const [hoge, setHoge] = useState(props.hoge);
const shownStyle = props.shown ? { display: 'block' } : { display: 'hidden' };
const updateHoge = (ev: FormEvent<HTMLInputElement>) => {
setHoge(ev.currentTarget.value);
};
const closeWithApply = () => {
props.onApplyClose({ hoge });
};
return (
<dialog style={shownStyle}>
<input type="text" value={hoge} onInput={updateHoge} />
<button onClick={closeWithApply}>保存</button>
<button onClick={props.onClose}>キャンセル</button>
</dialog>
);
};
このようなケースでは、子の状態を親で管理すると煩雑になるため子に持たせておいた方が良い。