🐥 React × TypeScript でのメモアプリ作成 #7(カスタムフック化・Enterで発火)
作成日: 2022/02/15
3

学習メモ

カスタムフック化

App.tsx

import classes from "../styles/App.module.scss";
import { useState, useRef, FC, useCallback } from "react";
import { InputArea } from "./InputArea";
import { OutputArea } from "./OutputArea";
import { useMemoList } from "../hooks/useMemoList";

export const App: FC = () => {
  const inputElement = useRef<HTMLInputElement>(null);
  const {outputText, addTodo, deleteTodo} = useMemoList();

  /*
   * 追加ボタン押下時
   */
  const onClickAddButton = () => {
    addTodo(inputElement.current?.value);

    // フォームに入力した値をリセットする
    inputElement.current!.value = "";
  }

  /*
   * 削除ボタン押下時
   * @param {number} index リストの何番目が押されたかを示す番号
   */
  const onClickDeleteButton = useCallback((index: number) => {
    deleteTodo(index);
  }, [deleteTodo]);

  return(
    <div className={classes.container}>
      <h1 className={classes.title}>簡単メモアプリ</h1>

      <InputArea inputElement={inputElement} onClickAddButton={onClickAddButton} />
      <OutputArea outputText={outputText} onClickDeleteButton={onClickDeleteButton} />

    </div>
  );
};

hooks/useMemoList.ts

import { useCallback, useState } from "react";

export const useMemoList = () => {
    const [outputText, setOutputText] = useState<string[] | null>([]);

    /*
     * リストを追加
     */
    const addTodo = useCallback((inputText: any) => {
        if (inputText === "") return; // フォームに入力された文字が空白の場合は何もしない

        // フォームに入力した値を配列に入れる
        const newTexts = [...outputText!, inputText];
        setOutputText(newTexts);
    }, [outputText]);

    /*
     * リストを削除
     */
    const deleteTodo = useCallback((index: number) => {
        // 表示されている配列を取得
        const newTexts = [...outputText!];

        // 配列内の該当の要素を削除
        newTexts.splice(index, 1);

        // リストを更新
        setOutputText(newTexts);
    }, [outputText]);

    return { outputText, addTodo, deleteTodo };
};

エンターキーを押したときに追加できるように変更

App.tsx

// …省略
  /*
   * エンターキー押下時
   */
  const handleKeyDown = (event: React.KeyboardEvent) => {
    if (event.key === "Enter" && event.keyCode === 13) {
      addTodo(inputElement.current?.value);

      // フォームに入力した値をリセットする
      inputElement.current!.value = "";
    }
  }
// …省略
  return(
    <div className={classes.container}>
      <h1 className={classes.title}>簡単メモアプリ</h1>

      <InputArea inputElement={inputElement} handleKeyDown={handleKeyDown} onClickAddButton={onClickAddButton} />
      <OutputArea outputText={outputText} onClickDeleteButton={onClickDeleteButton} />

    </div>
  );
// …省略

inputArea.tsx

import classes from "../styles/App.module.scss";
import { FC } from "react";

type Props = {
    inputElement: React.RefObject<HTMLInputElement>;
    onClickAddButton: () => void;
    handleKeyDown: (event: React.KeyboardEvent) => void;
}

export const InputArea: FC<Props> = props => {
    const { inputElement, handleKeyDown, onClickAddButton } = props;

    return (
        <div className={classes.inputArea}>
        <input ref={inputElement} onKeyDown={handleKeyDown} className={classes.inputAreaTextArea} type="text" placeholder="メモを入力" />
        <button onClick={onClickAddButton} className={classes.inputAreaButton}>追加</button>
      </div>
    )
}

変換時にイベントが発火してしまう問題への対処

event.keyCode が非推奨となっていたため、以下を参考に対応

https://github.com/facebook/react/issues/3926#issuecomment-929799564

App.tsx

  const [onComposition, setOnComposition] = useState(false);

  /*
   * キーボードが入力中どうかの判定
   * 変換中にイベントが発火してしまう問題への対処
   * https://github.com/facebook/react/issues/3926#issuecomment-929799564
  */
  const handleComposition = (event: React.CompositionEventHandler<HTMLInputElement> | any) => {
    if (event.type === "compositionstart") {
      setOnComposition(true);
    }
    if (event.type === "compositionend") {
      setOnComposition(false);
    }
  }
// …省略
  /*
   * エンターキー押下時
   */
  const handleKeyDown = (event: React.KeyboardEvent) => {
    if (event.key === "Enter" && !onComposition) {
      addTodo(inputElement.current?.value);

      // フォームに入力した値をリセットする
      inputElement.current!.value = "";
    }
  }
// …省略
  return(
    <div className={classes.container}>
      <h1 className={classes.title}>簡単メモアプリ</h1>

      <InputArea inputElement={inputElement} handleKeyDown={handleKeyDown} onClickAddButton={onClickAddButton} handleComposition={handleComposition} />
      <OutputArea outputText={outputText} onClickDeleteButton={onClickDeleteButton} />

    </div>
  );

InputArea.tsx

import classes from "../styles/App.module.scss";
import { FC } from "react";

type Props = {
    inputElement: React.RefObject<HTMLInputElement>;
    onClickAddButton: () => void;
    handleKeyDown: (event: React.KeyboardEvent) => void;
    handleComposition:(event: React.CompositionEventHandler<HTMLInputElement> | any) => void;

}

export const InputArea: FC<Props> = props => {
    const { inputElement, handleKeyDown, handleComposition, onClickAddButton } = props;

    return (
        <div className={classes.inputArea}>
        <input ref={inputElement} onKeyDown={handleKeyDown} onCompositionStart={handleComposition} onCompositionEnd={handleComposition} className={classes.inputAreaTextArea} type="text" placeholder="メモを入力" />
        <button onClick={onClickAddButton} className={classes.inputAreaButton}>追加</button>
      </div>
    )
}

Safariで確認したらだめだった

全ブラウザに対応したければこちら
compositionstartcompositionendイベントの監視を自作する方法で対処することになりそう

所感

目標までは終わりました。
まだまだ覚えることがたくさんあるので、できることを増やしていきたいです。