2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React Hook】useStateで関数を保持させる方法

Posted at

概要

React Hookで useStateを利用する際、中身に関数を保持させる方法を記載したポストです。

なんかそういう処理を書かないといけないケースがあったんですよ。ただ一瞬ハマったので備忘録として書いておきます。

(他にいいやり方があるのかもしれないですけど、あまり調査に時間かけられなかったので付け焼き刃的な方法です。ご了承下さい)

やりたいこと

親コンポーネントMainと子コンポーネントChildがあるとして、Child側でセットした関数をMain側で呼び出したい

image.png

失敗したやり方

単純に渡した場合、うまくいきません。

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>
    );
};
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?