2
0

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】実際に初学者が詰まったところと解消方法

Last updated at Posted at 2024-10-07

はじめに

フロントエンド学習のため、ReactもTypeScriptもノータッチだったところからReact×TypeSqriptでTodoアプリをゼロから作成しました。
その中でたくさん詰まったところがあったので、備忘も兼ねてアウトプットします。

なお、学習のためにやったことは下に記載しますが、本記事ではTODOアプリの作成についてのみアウトプットします。

やったこと

成果物のイメージ

レコーディング-2024-10-07-191816.gif

実装した機能

  • 新規タスクの登録
  • 登録済タクスの表示
  • 削除機能
  • 絞り込み機能

詰まったところと解消方法

型定義:'void'と'() => void'の違い

該当のソースコード

// 引数なしの関数の型定義
type RegisterBtnProps = {
  onRegisterBtnClick: () => void;
};

// 引数ありの関数の型定義
type InputProps = {
  onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
};

詰まったポイント

  • 関数の指定=voidだと勘違いしていた
  • 正確には以下の通りでした
    • void:戻り値なし を表す
    • () => void:戻り値なし、引数なしの関数を表す
    • (i :number) => void:戻り値あり、引数あり(型は数値)の関数を表す

解消方法

  • 指定したいプロパティが関数なのであれば() => voidを記述する

子コンポーネントで発生するイベントオブジェクトを親コンポーネントで使いたい

該当のソースコード

// 省力した箇所は...で表しています

type InputProps = {
    ...
    onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void; //ここで関数に引数が有ることを定義
};

const Input: React.FC<InputProps> = ({
  ...
  onInputChange, // ここでプロパティとして親コンポーネントの関数を受け取る。このときにはonInputChange(e)みたいには記述しない
}) => {
  ...
  return (
    <div className="input">
      ...
      <input ... onChange={onInputChange} /> // ここで親コンポーネントの関数にトリガーを設定。このときにはonInputChange(e) みたいには記述しない
    </div>
  );
};

const InputTaskArea: React.FC<InputTaskAreaProps> = ({
  tasks,
  setTasks,
  name,
  setName,
  person,
  setPerson,
  deadline,
  setDeadline,
}) => {
  ...
  const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setName(e.target.value);
  };  // ここで関数を定義、引数にeを持つことは関数の定義時に記述する

  const handlePersonChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPerson(e.target.value);
  }; // ここで関数を定義、引数にeを持つことは関数の定義時に記述する

  const handleDeadlineChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setDeadline(e.target.value);
  }; // ここで関数を定義、引数にeを持つことは関数の定義時に記述する

  return (
    <div className="inputTaskArea">
      <Input ... onInputChange={handleNameChange} /> // ここで子コンポーネントにプロパティを渡す
      <Input ... onInputChange={handlePersonChange} /> // ここで子コンポーネントにプロパティを渡す
      <Input ... onInputChange={handleDeadlineChange} /> // ここで子コンポーネントにプロパティを渡す
    </div>
  );
};

詰まったポイント

  • nameなどの変数宣言を親コンポーネント(この場合はInputTaskAreaより更に上のコンポーネント)で行っているため、子コンポーネントにプロパティとして関数を渡し、親コンポーネントにて発火ささせることで変数を上書きしています
  • そのため、子コンポーネント(Input)で発生するイベントの変数eをどうやって親コンポーネント(InputTaskArea)で使えばいいか悩みました

解消方法

  • 引数の設定は通常通り関数の定義時のみ行う
  • プロパティによる関数の受け渡しの際や読み込みの際には引数の記述はしない

子コンポーネントの繰り返し処理時に使用するidを親コンポーネントで定義した関数に引き渡したい

該当のソースコード

function App() {  // 一番大元のコンポーネント

  ...
  const handleDeleteBtnClick = (task_id: number) => {  //  ここで関数と引数を定義
    const deletedTasks = tasks.filter((task) => task.id !== task_id);
    setTasks(deletedTasks);
  };
  
  ...
  return (
    <div className="container">

      ... 
      <TasksArea
        tasks={filteredTasks}
        onDeleteBtnClick={handleDeleteBtnClick} // ここで関数を子コンポーネントにわたす
      />
    </div>
  );
}  

type TasksAreaProps = {

  ...
  onDeleteBtnClick: (i: number) => void;  // ここでプロパティの定義
};

const TasksArea: React.FC<TasksAreaProps> = ({ tasks, onDeleteBtnClick }) => { //ここで親コンポーネントから関数を受け取る。このときには引数は記述しない
  return (

      ...
      <tbody className="tasksTable_tbody">
        {tasks.map((task: task) => {
          return (
            <tr key={task.id}>
              <td>{task.id}</td>
              <td>{task.name}</td>
              <td>{task.person}</td>
              <td>{task.deadline}</td>
              <td>
                <button onClick={() => onDeleteBtnClick(task.id)}>削除</button> // ここでtask.idを引数にして関数を埋め込む
              </td>
            </tr>
          );
        })}
      </tbody>

    ...
  );

詰まったポイント

  • 先程のものに似ていますが、今度はイベントオブジェクトではなく、子コンポーネントの繰り返し処理時に使用したidを引数にして、該当のタスクを削除するためのロジックを組んでいます
  • 先程と同様に子コンポーネントで指定したidをどうやって親コンポーネントで定義した関数に渡せばよいか悩みました

解消方法

  • 引数の定義は関数の定義時、プロパティの型定義時に行う
  • 子コンポーネントでの関数呼び出し時に引数を設定する
  • 逆に上記以外では引数についての記述を行わない

複数回呼び出すコンポーネントを識別したい

const Input: React.FC<InputProps> = ({
  ...
}) => {
  const id = useId();  // useIdで一意の識別子を設定する
  return (
    <div className="input">
      <label htmlFor={id} className="input_label">  // htmlForで紐づけ先のインプットタグを指定
        {label}
      </label>
      <input
        id={id}                                     // ここで IDを付与
        type={inputType}
        placeholder={placeholder}
        onChange={onInputChange}
        value={value}
      />
    </div>
   
  );
};

const InputTaskArea: React.FC<InputTaskAreaProps> = ({
  ...
}) => {
  ...
    return (
    <div className="inputTaskArea">
      <Input ...     /> // 複数回呼び出すため、idがないとlabelの紐づけ先がなくなる
      <Input ...     /> // 複数回呼び出すため、idがないとlabelの紐づけ先がなくなる
      <Input ...     /> // 複数回呼び出すため、idがないとlabelの紐づけ先がなくなる
};

詰まったポイント

  • ラベルタグの設定のために一意のidがほしい
  • 複数回呼び出すことを前提にしたコンポーネントのため、動的にidを設定したい
  • 普通に設定してしまうと、同じidが複数個存在することになってしまう

解消方法

  • useIdを使用する

useId は、React 18 から導入されたフックで、コンポーネントごとに一意の ID を生成するために使用されます。これにより、クライアントとサーバーのレンダリングで一貫した ID を生成できるため、主にフォーム要素やアクセシビリティのラベル( と )など、ユニークな識別子を必要とする場合に役立ちます。 ※chatGPTによる返答

useStateでの更新が一動作分送れる

※本項についてはコメント欄のhaney32様の補足、訂正の参照をお願いします。認識の誤りがあります。一旦、自身の現状認識のため記事はそのままにします

該当のソースコード

うまくいかないコード
  const [filteringWord, setFilteringWord] = useState("");            // フィルター用の語句を定義
  const [filteredTasks, setFilteredTasks] = useState<Array<task>>(tasks);  // フィルター用の語句を使ってフィルターされたタスクの配列を定義
  const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {  // フィルター用のインプットタグの変更を取得する関数
    setFilteringWord(e.target.value);                                      // fillteringWordを最新の値に更新
    const newFilterdTasks = tasks.filter(                                  //フィルターされた新しい配列を定義
      (task) =>
        task.name.includes(filteringWord) || task.person.includes(filteringWord)
    );
    setFilteredTasks(newFilterdTasks);                                    //新しい配列でFilteredTaskを更新
  };
うまくいったコード
  const [filteringWord, setFilteringWord] = useState("");
  const [filteredTasks, setFilteredTasks] = useState<Array<task>>(tasks);
  const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newFilteringWord = e.target.value;
    setFilteringWord(newFilteringWord);     // ここまでほぼ同じ


  useEffect(() => {  // useEffectを使用
    const newFilterdTasks = tasks.filter(   // filteredTasksの更新をここに記述
      (task) =>
        task.name.includes(filteringWord) || task.person.includes(filteringWord)
    );
    setFilteredTasks(newFilterdTasks);
  }, [filteringWord, tasks]);               // 発火のタイミングをfilterringWordの変更時とtasksの変更時に設定
 

詰まったポイント

  • フィルターの動作が入力直後ではなく、入力直後から1動作後(他の文字を1タイプした後)になる
  • 原因はuseStateのsetHogeをすると、非同期処理になるため
    • つまり、うまくいかないコードの下記の箇所でフィルタリングしているfilteringWordは、setFilteringWord(e.target.value)したfilteringWordではなく、その前のものになっている
    setFilteringWord(e.target.value);      // ここの処理が非同期
    const newFilterdTasks = tasks.filter(  
      (task) =>
        task.name.includes(filteringWord) || task.person.includes(filteringWord)  // ここで使用しているfilteringWordはsetFilteringWord(e.target.value)が反映される前
    

解消方法

  • useEffectを利用
  • useEffectとは
    • Reactの機能(hook)の一つ
    • 任意の変数が更新されたタイミングで関数を発火できる
    • 使い方は下記
    useEffect(() => {
     // ここに処理を記述する
    }, [ここに記述した変数が更新されたタイミングで処理を行う]);
    

おわりに

Reactを触るうえで特に詰まったのは以下の点でした。

  • TypeScriptの型定義、特に関数やPropsの型定義方法
  • 親コンポーネントと子コンポーネントの関数の受け渡し
  • useStateやuseEffectなどReact独自の関数(hooks)

本当に最低限のインプットはできたと思うので、今後はフロントReact-バックRailsでの簡易アプリ開発にも挑戦していきます。

2
0
3

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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?