TaikiTkwkbysh
@TaikiTkwkbysh (WAKA Engineer)

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

【Next.js_Jest】onChange() が何をしているのか教えてください。

解決したいこと

Next.jsのJestを現在学習中の者です。
参考書にて下記ソースを拝見したのですが、その中の

onChange(e);

が果たして何の役割をしているのかわからず困っております。

問題のソース

import React, { useState, useCallback, useRef } from "react";

type DelayButtonProps = {
    onChange: React.ChangeEventHandler<HTMLInputElement>;
}

export const DelayInput = (props: DelayButtonProps) => {

    const { onChange } = props;

    const [isTyping, setIsTyping] = useState<boolean>(false);

    const [inputValue, setInputValue] = useState<string>('');

    const [viewValue, setViewValue] = useState<string>('');

    const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

    const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {

        setIsTyping(true);

        setInputValue(e.target.value);

        if(timerRef.current !== null) {
            clearTimeout(timerRef.current);
        }

        timerRef.current = setTimeout(() => {
            timerRef.current = null;

            setIsTyping(false);

            setViewValue(e.target.value);

            onChange(e); // ←
        }, 1000);
        
    }, [onChange])

    const text = isTyping ? '入力中...' : `入力したテキスト:${viewValue}`;
    return (
        <div>
            <input type="text" data-testid="input-text" value={inputValue} onChange={handleChange} />
            <span data-testid="display-text">{text}</span>
        </div>
    )
}

内容は簡単なもので、inputタグに文字を入力したら、入力した文字が1秒後にspanタグの箇所に出力されるものです。

onChangeの箇所をコメントアウトしてテストを実行しても正常に終了しました。

【個人的な見解】

onChangeイベントが発生した時、handleChangeメソッドが実行されるが、実はonChangeイベントそのものがpropsで渡るようになっている。

1秒後onChange(e)を実行することで、useCallbackの依存関係であるonChangeに変化があることを認識させて再レンダリングを行わせている。

と考えましたが、コメントアウトしてもテストが正常に終了した理由がわからないです。

何卒ご教示の程、宜しくお願い致します。

1

1Answer

最小限の機能から、少しずつ機能をリッチにしていく方向で解説します。

以下のようなコードを書くと、 input タグを 制御されたコンポーネント (Controlled Component) として状態管理できるようになります。

const [inputValue, setInputValue] = useState<string>('');
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
  setInputValue(e.target.value)
}, []);

return <input type="text" value={inputValue} onChange={handleChange} />;

React で保持したステートの値を input の value Prop に渡して、入力があると input の onChange Prop (DelayInput のほうではありません) に渡した関数が実行されます。 handleChange 関数では、イベントオブジェクトから input 要素の値 (e.target.value) を取り出して、 set 関数に渡しています。

handleChange に使っている useCallback は、ここでは必要ありませんが、「関数をメモ化する」という機能です。

つまり、何もなしに関数を書いた場合にレンダー(stateの変更時には対象のコンポーネントの中身がすべて再計算される)ごとに新しい関数を使うことになるところを、古い関数をそのまま使いまわしてくれる機能です。


次に、DelayButtonProps に含まれる onDelayedChange が加わります。

元コードでは onChange という名前でしたが、この解説では input タグの onChange prop (これは名前変更できない)とカブって理解し辛いので、あえて異なる名前を付けています。

これによって、コンポーネント内で input を制御するだけでなく、 onDelayedChange を通して値の変更を「コンポーネントの外側(利用者側)」に通知できるようになります。

const { onDelayedChange } = props;
const [inputValue, setInputValue] = useState<string>('')
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
  setInputValue(e.target.value)
  onDelayedChange(e)
}, [onDelayedChange]);

return <input type="text" value={inputValue} onChange={handleChange} />

(2022/12/11 22:38 追記)

// 利用者側のコード(ここでは、 DelayInput を利用するコンポーネント)
const App: React.FC = () => {
  const handleDelayedChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  }, [])
  return <DelayInput onDeleyedChange={handleDelayedChange} />
} 

これで、テキスト入力があったときには、

  1. onChange Prop に渡した handleChange 関数が実行される
  2. -> setInputValue が実行される -> 3 が終わると新しい値で再レンダリングされ、 input の値 (value として渡す) に反映される
  3. 同時に onDeleyedChange Prop として App コンポーネントによってセットされた関数 handleDelayedChange が実行される

という順序で、イベントが子→親の方向に上がって行きます。

(追記ここまで)


そして、「onDeleyedChange の呼び出しを 1s 遅れで実行する (1s以内にもう一度入力があったら取りやめる)」

という意味になるのが、以下のコードです。

「前回のonChange イベント発火時の timeoutID」 を記憶する必要があるため、 useRef を使っています。 (描画に使う状態は useState, イベントハンドラの関数内でのみ使う状態は useRef を使います。)

const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

// const handleChange = ... (中略)
  if(timerRef.current !== null) {
    // 前回のイベント発火時に、タイマーセットしたタスクを取りやめる
    clearTimeout(timerRef.current);
  }

  // 1s 後にタスクを実行するようにタイマーをセット
  timerRef.current = setTimeout(() => {
    timerRef.current = null;
    onChange(e)
  }, 1000)

これに isTyping を組み合わせることで、

<DelayInput onDeleyedChange={(e) => {console.log(e.target.value);}} />

と書くことができ、

「キー入力しはじめると『入力中』状態になり、1s 間入力しないと元の状態に戻り、onChange イベントを外側に通知する(上のコード例では、 console.log が呼び出される)」
という仕様を満たすコンポーネントになっています。

1Like

Comments

  1. @TaikiTkwkbysh

    Questioner

    @honey32様

    この度はご教示頂き、誠にありがとうございます。
    ご丁寧な解説で大変分かりやすく、とても助かります。

    頂いた解説を読ませていただいた上で、2点質問がございます。
    よろしければご教示頂きたいです。

    ①inputタグの中の"onChange"はイベントハンドラと、コンポーネントにonChangeというpropsを渡すという2つの役割を持っているということでしょうか。
    ※reactの公式ドキュメントにも類似したような内容があったので確認したいです。
    https://ja.reactjs.org/docs/faq-functions.html

    もしくは、
    DelayInputコンポーネント内部にあるinputタグのonChangeはイベントハンドラとしての役割で、
    DelayInputタグに渡すonChangeがPropsとしての役割を果たしているということになるのでしょうか。

    ②この onChange(e) は、これ自体がコンポーネントより外(ブラウザ?)に"値が変わったという情報"を知らせるためのメソッドなのでしょうか。
    それとも、それは親からどういう内容で渡すかによって変わり、今回はイベントが発火したらconsole.log出力するという関数を渡したから外部に通知できるようになったということなのでしょうか。

    利用している参考書が全く解説を載せていないので分からず、申し訳ございません....。
    長文で申し訳ございませんが、ご教示いただけますと幸いです。
  2. ①とも②とも言い難いですね...

    元コードでは onChange という名前でしたが、この解説では input タグ
    の onChange prop (これは名前変更できない)とカブって理解し辛かったので、 onDelayedChange と名前を変更して、もとの回答を修正しました。再確認お願いします。

    (このコメントにはコードブロックが書けないのでこのように回答しています。)



    また、「コンポーネントの外側(利用者側)」という表現も伝わっていなかったようなので、この DelayInput コンポーネントを利用するコンポーネントの例(Appコンポーネント)も、もとの回答に追記しました。
  3. @TaikiTkwkbysh

    Questioner

    @honey様
    追加のご教示を頂き、誠にありがとうございます。
    onDelayedChangeに変更して頂いたことで、より理解することができました。

    コンポーネントの外側(利用者側)の件も、非常に理解できました。
    かなりonChangeが紛らわしくしているみたいですね...。
    参考書のレビューで「サンプルコードの質がとても悪い」というレビューが多かったので、おそらくこのことかと思います。

    貴重なお時間を頂いてご教示いただき、誠にありがとうございました。
    また機会がございましたら、何卒ご教示のほど宜しくお願い致します!

Your answer might help someone💌