概要
React Hookで useState
を利用する際、中身に関数を保持させる方法を記載したポストです。
なんかそういう処理を書かないといけないケースがあったんですよ。ただ一瞬ハマったので備忘録として書いておきます。
(他にいいやり方があるのかもしれないですけど、あまり調査に時間かけられなかったので付け焼き刃的な方法です。ご了承下さい)
やりたいこと
親コンポーネントMainと子コンポーネントChildがあるとして、Child側でセットした関数をMain側で呼び出したい
失敗したやり方
単純に渡した場合、うまくいきません。
type Func = () => void;
const Main: React.FC = () => {
const [func, setFunc] = useState<Func | undefined>(undefined);
const handleOnClick = useCallback(() => {
console.log("クリックされた");
if (func) {
func();
} else {
console.log("設定されてないよ");
}
},[func]);
return (
<div>
<Child onSetFunc={setFunc} />
<button onClick={handleOnClick}>MainButton</button>
</div>
);
};
const Child: React.FC<{ onSetFunc: (Func) => void }> = ({ onSetFunc }) => {
const handleSetFunc = useCallback(() => {
onSetFunc(() => {
console.log("呼び出されるよ");
});
},[onSetFunc]);
return (
<div>
<button onClick={handleSetFunc} >ChildButton</button>
</div>
);
};
これを実行し、 ChildButton -> MainButtonを押した場合に
クリックされた
呼び出されるよ
と出てほしいのですが、うまくいきません。
実際には ChildButtonをクリックしたタイミングで 呼び出されるよ
というコンソールログが出力され、 MainButtonをクリックしたら
クリックされた
設定されてないよ
と表示されることになります。
なぜそうなるのか
単純なことなんですが、 useStateのsetterって、関数でもセットできるんですよね。
useStateの定義を見ると以下のようになってます。
function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);
この SetStateAction
を見たらわかるように、普通の値だけじゃなく (変更前の状態) => 変更後の状態
という関数も受け付けてます。
言葉じゃ分かりづらいので具体例を乗せると、以下のような感じでも状態のセットができるわけです。
const [counter, setCounter] = useState<number>(0);
// 1が設定される
setCounter(1);
// 直前の状態に1を足した数が設定される
setCounter((prev) => {
return prev + 1;
});
どちらもよく使いますよね。
で、上の例だと handleSetFunc
内で行ってる onSetFunc
は、この関数での設定だと判断されてしまい、ChildButtonを押下した際に handleSetFunc
内で設定した関数がそのまま呼び出され、かつ中の関数が何も返していないため undefined が設定されて MainButton押下の際にも呼び出されないという自体になります。
解決方法
2つあります。
1つ目は、 onSetFunc を呼び出す際に関数を返してあげればいいです。
// Mainとかは同じなので省略
const Child: React.FC<{ onSetFunc: (Func) => void }> = ({ onSetFunc }) => {
const handleSetFunc = useCallback(() => {
onSetFunc(() => {
// 関数を返却する
return () => {
console.log("呼び出されるよ");
}
});
},[onSetFunc]);
return (
<div>
<button onClick={handleSetFunc} >ChildButton</button>
</div>
);
};
もう一つの解決方法は、関数を設定できないのだから関数を設定するのではなくinterface か typeでもぶっこんどけばちゃんと動きます。
// 型を変更
type Func = { onCall: () => void };
const Main: React.FC = () => {
const [func, setFunc] = useState<Func | undefined>(undefined);
const handleOnClick = useCallback(() => {
console.log("クリックされた");
if (func) {
func.onCall();
} else {
console.log("設定されてないよ");
}
},[func]);
return (
<div>
<Child onSetFunc={setFunc} />
<button onClick={handleOnClick}>MainButton</button>
</div>
);
};
const Child: React.FC<{ onSetFunc: (Func) => void }> = ({ onSetFunc }) => {
const handleSetFunc = useCallback(() => {
const handleCall = {
onCall: () => {
console.log("呼び出されるよ");
}
};
onSetFunc(handleCall);
},[onSetFunc]);
return (
<div>
<button onClick={handleSetFunc} >ChildButton</button>
</div>
);
};