Open10

React学習ログ

アライ リョータアライ リョータ

アロー関数

  • モダンJSで用いられる、引数と関数の本体をアロー=>で繋ぐ記法
基本の構文
const 関数名 = (引数,) => { 関数の本体 }
基本のアロー関数例
const circle = (radius) => {
  return (radius ** 2) * Math.PI;
}

アロー関数の省略記法

関数の本体が単一文の場合

  • ブロック{ … }を省略できる
  • 本体(式)の値がそのまま関数の戻り値となるため、returnも省略できる
    ※ 式(expression);変数に代入できる
{}とreturnを省略
const circle = (radius) => (radius ** 2) * Math.PI;

引数が1つの場合

  • 引数の()を省略できる
  • 引数がない場合は()を省略できない
引数の()を省略
const circle = radius => (radius ** 2) * Math.PI;
アライ リョータアライ リョータ

オブジェクトリテラル:プロパティ名の動的な生成

  • プロパティ名をブラケットで括ることで、式の値からプロパティ名を生成できる(算出プロパティ)
let i = 0;
const dog = {
  [`attr${++i}`]: 'Pochi',
  [`attr${++i}`]: 'shiba',
  [`attr${++i}`]: '3'
}
console.log(dog);
コンソール出力結果
{
    "attr1": "Pochi",
    "attr2": "shiba",
    "attr3": "3"
}
アライ リョータアライ リョータ

Optional Chaining演算子:?.

  • オブジェクトのプロパティやメソッドにアクセスする際、その前のプロパティやメソッドがnullundefinedの場合、エラーが発生する
const str = null;
console.log(str.substring(1));
// => Uncaught TypeError: Cannot read properties of null (reading 'substring')

👍 ?.を用いることで

  • null・undefinedだった場合のエラーを回避し、undefinedを返却するようになり、安全に(=プログラムがエラーなく実行される)プロパティやメソッドにアクセスできるようになる
?.を用いた例
const str = null;
console.log(str?.substring(1));
// => undefined
アライ リョータアライ リョータ

コンポーネントの再レンダリングのタイミング

  • Stateが更新された時
  • 渡されたPropsが変更された時
  • (子コンポーネントにおいて)親コンポーネントが再レンダリングされた時
アライ リョータアライ リョータ

常に最新のStateを参照する

  • ReactではStateを非同期で更新する=Stateが新しい値に更新されるのはイベントハンドラを終えた後
カウントアップが期待通りの挙動にならない例
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)

  const countUp = () => {
    setCount(count + 1) // カウントを1増やすコードが2つあるが、どちらも参照しているのは同じ値
    setCount(count + 1) // なので、イベントハンドラ終了時に増えるカウントは1のみ
  }

  return (
    <>
      <p>現在のカウント:{count}</p>
      <button onClick={countUp}>カウントアップ</button>
    </>
  )
}

export default App
  • 常に新しい値を参照するにはsetXxxxx関数に関数型の引数を渡す
構文
setXxxxxx( state => statements )

👉 引数stateに渡されるstate値は、その時々での最新の値であることを保証される

カウントアップが2ずつ増える例
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)

  const countUp = () => {
    setCount(prev => prev + 1)
    setCount(prev => prev + 1)
  }

  return (
    <>
      <p>現在のカウント:{count}</p>
      <button onClick={countUp}>カウントアップ</button>
    </>
  )
}

export default App
アライ リョータアライ リョータ

配列の更新

  • Stateは常にセッター(setXxxxx関数)経由で更新しなければならない(Reactがstate値の変更を認識でいないため)
  • Stateに格納されたオブジェクトを更新する時は新しいオブジェクトを作成した上で、それをsetXxxxx関数に渡すのがReactの作法(配列も同様)
操作 ○ 推奨 × 非推奨
追加 concat, [...list] push, unshift
更新 map splice, list[i] = ~
削除 filter, slice pop, shift, splice
ソート 事前に配列を複製 sort, reverse

非推奨 = 破壊的メソッド

配列への要素の追加
スプレッド演算子で元の配列を複製し、新規の要素を追加
import { useState } from "react"

let maxId = 0; // keyを一意にするために、便宜的に定義

export const Todo = () => {
  const [title, setTitle] = useState("");
  const [todo, setTodo] = useState([]);

  const hundleChangeTitle = e => {
    setTitle(e.target.value)
  }

  const hundleClick = () => {
    setTodo([
      ...todo, // 元の配列を複製
      {
        id: ++maxId,
        title,
        createdAt: new Date(),
        isDone: false
      }
    ]);
  };

  return (
    <div>
      <label>
        Todo:
        <input type="text" name="title" placeholder="タスクを入力" value={title} onChange={hundleChangeTitle} />
      </label>
      <button type="button" onClick={hundleClick}>追加</button>
      <hr />
      <ul>
        {todo.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  )
}
配列の更新(Todoの完了チェック)
完了ボタンをクリックすると、テキストに線が表示される
import { useState } from "react"

import './Todo.css'

let maxId = 0;

export const Todo = () => {
  const [title, setTitle] = useState("");
  const [todo, setTodo] = useState([]);

  const hundleChangeTitle = e => {
    setTitle(e.target.value)
  }

  const hundleClick = () => {
    setTodo([
      ...todo,
      {
        id: ++maxId,
        title,
        createdAt: new Date(),
        isDone: false
      }
    ]);
  };

  const hundleDone = e => {
    setTodo(todo.map(item => {
      if (item.id === Number(e.target.dataset.id)) {
        return {
          ...item,
          isDone: true
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <div>
      <label>
        Todo:
        <input type="text" name="title" placeholder="タスクを入力" value={title} onChange={hundleChangeTitle} />
      </label>
      <button onClick={hundleClick}>追加</button>
      <hr />
      <ul>
        {todo.map(item => (
          <li key={item.id}
            className={item.isDone ? 'done' : '' }>
            {item.title}
            <button onClick={hundleDone} data-id={item.id}>完了</button> // data-id(独自データ属性)
          </li>
        ))}
      </ul>
    </div>
  )
}

アライ リョータアライ リョータ
配列の削除(ToDoの破棄)
  • filterメソッドで「削除ボタンに紐づくタスクと一致しないタスクのみを抽出した配列」に更新することで、結果的に削除ボタンに紐づくタスクが破棄された状態となる
削除ボタンをクリックしたタスクのidと一致しないタスクでフィルタリングする
import { useState } from "react"

import './Todo.css'

let maxId = 0;

export const Todo = () => {
  const [title, setTitle] = useState("");
  const [todo, setTodo] = useState([]);

  const hundleChangeTitle = e => {
    setTitle(e.target.value)
  }

  const hundleClick = () => {
    setTodo([
      ...todo,
      {
        id: ++maxId,
        title,
        createdAt: new Date(),
        isDone: false
      }
    ]);
  };

  const hundleDone = e => {
    setTodo(todo.map(item => {
      if (item.id === Number(e.target.dataset.id)) {
        return {
          ...item,
          isDone: true
        };
      } else {
        return item;
      }
    }));
  }

  const hundleRemove = e => {
    setTodo(todo.filter(item => 
      item.id !== Number(e.target.dataset.id)
    ))
  }

  return (
    <div>
      <label>
        Todo:
        <input type="text" name="title" placeholder="タスクを入力" value={title} onChange={hundleChangeTitle} />
      </label>
      <button onClick={hundleClick}>追加</button>
      <hr />
      <ul>
        {todo.map(item => (
          <li key={item.id}
            className={item.isDone ? 'done' : '' }>
            {item.title}
            <button onClick={hundleDone} data-id={item.id}>完了</button>
            <button onClick={hundleRemove} data-id={item.id}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

配列の並べ替え(昇順・降順ソート)
sortは破壊的メソッドのため、事前に配列を複製する
import { useState } from "react"
import './Todo.css'

let maxId = 0;

export const Todo = () => {
  const [title, setTitle] = useState("");
  const [todo, setTodo] = useState([]);
  const [desc, setDesc] = useState(true);

  const hundleChangeTitle = e => {
    setTitle(e.target.value)
  }

  const hundleClick = () => {
    setTodo([
      ...todo,
      {
        id: ++maxId,
        title,
        createdAt: new Date(),
        isDone: false
      }
    ]);
  };

  const hundleDone = e => {
    setTodo(todo.map(item => {
      if (item.id === Number(e.target.dataset.id)) {
        return {
          ...item,
          isDone: true
        };
      } else {
        return item;
      }
    }));
  }

  const hundleRemove = e => {
    setTodo(todo.filter(item => 
      item.id !== Number(e.target.dataset.id)
    ))
  }

  const hundleSort = e => {
    const sorted = [...todo];
    sorted.sort((m, n) => {
      if (desc) {
        return n.createdAt.getTime() - m.createdAt.getTime();
      } else {
        return m.createdAt.getTime() - n.createdAt.getTime();
      }
    })
    setDesc(d => !d)
    setTodo(sorted)
  }

  return (
    <div>
      <label>
        Todo:
        <input type="text" name="title" placeholder="タスクを入力" value={title} onChange={hundleChangeTitle} />
      </label>
      <button onClick={hundleClick}>追加</button>
      <button onClick={hundleSort}>ソート {desc ? '☝️' : '👇'}</button>
      <hr />
      <ul>
        {todo.map(item => (
          <li key={item.id}
            className={item.isDone ? 'done' : '' }>
            {item.title}
            <button onClick={hundleDone} data-id={item.id}>完了</button>
            <button onClick={hundleRemove} data-id={item.id}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

アライ リョータアライ リョータ

React Hook Form

https://react-hook-form.com/

  • ライブラリをインストール
npm install react-hook-form
React Hook Formを使用した簡易的な入力フォームのバリデーション
import { useForm } from "react-hook-form";

function App() {
  const defaultValues = {
    name: 'Pochi',
    email: 'pochi@example.com',
    gender: 'male',
    memo: ''
  }

  const { register, handleSubmit, formState: { errors } } = useForm({
    defaultValues
  });

  const onsubmit = data => console.log(data);
  const onerror = err => console.log(err);

  return (
    <form onSubmit={handleSubmit(onsubmit, onerror)} noValidate>
      <div>
        <label htmlFor="name">名前:</label><br/>
        <input id="name" type="text"
          {...register('name', {
            required: '名前の入力は必須です。',
            maxLength: {
              value: 20,
              message: '名前は20文字以内にしてください。'
            }
          })} />
        <div>{errors.name?.message}</div>
      </div>
      <div>
        <label>性別:</label><br/>
        <label>
          <input type="radio" value="male"
            {...register('gender', {
              required: '性別は必須です。',
            })} />男性
        </label>
        <label>
          <input type="radio" value="female"
            {...register('gender', {
              required: '性別は必須です。',
            })} />女性
        </label>
        <div>{errors.gender?.message}</div>
      </div>
      <div>
        <label htmlFor="email">メールアドレス:</label><br/>
        <input id="email" type="email"
          {...register('email', {
            required: 'メールアドレスの入力は必須です。',
            pattern: {
              value: /([a-z\d+\-.]+)@([a-z\d-]+(?:\.[a-z]+)*)/i,
              message: 'メールアドレスの形式が不正です。'
            }
          })} />
        <div>{errors.email?.message}</div>
      </div>
      <div>
        <label htmlFor="memo">備考:</label><br/>
        <textarea id="memo"
          {...register('memo', {
            required: '備考の入力は必須です。',
            minLength: {
              value: 10,
              message: '備考は10文字以上にしてください。'
            }
          })} />
        <div>{errors.memo?.message}</div>
      </div>
      <div>
        <button type="submit">送信</button>
      </div>
    </form>
  );
}

export default App;
register関数
  • 第一引数:フォームの値を識別するための名前。フォームデータのキーとして使用される
  • 第二引数:オブジェクト。オプションとしてバリデーションルールを指定できる
基本的な使い方
import { useForm } from 'react-hook-form';

function App() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("example")} />
      <input {...register("exampleRequired", { required: true })} />
      <button type="submit">Submit</button>
    </form>
  );
}
オプションを指定する例
<input {...register("example", { required: true, maxLength: 80 })} />
  • パラメータを持たないもの(requiredなど)
...register('検証名', { required: 'エラーメッセージ' }
  • パラメータを持つもの(maxLength/patternなど)
...register('検証名', { required: { パラメータ名1:1, パラメータ名2:2,} }
バリデーションメッセージ(Optional Chainingを用いた例)
<div>{errors.name?.message}</div>

上記のコードで設定したバリデーション

フィールド名 検証ルール
name 必須, 最大文字数長(20)
gender 必須
email 必須, 正規表現(メールアドレス)
memo 必須, 最小文字数長(10)

hundleSubmit関数

構文
hundleSubmit(onsubmit, onerror)
// onsubmit:検証成功時の処理
// onerror:検証失敗時の処理
  • 引数onsubmit, onerrorが引数として受け取る情報
    ※上記のコード例では、コンソールに出力しているだけだが、本来はサーバーに送信する等してデータを扱う
関数 引数 概要
onsubmit data 入力値( フィールド名: 値 )
onsubmit e イベントオブジェクト
onerror error エラー情報
onerror e イベントオブジェクト
アライ リョータアライ リョータ

useEffect

副作用フック:コンポーネントを再描画/破棄を実行する

  • コンポーネントの状態(State, Props)が変化したタイミングで実行すべき処理を定義できる。
useEffectの構文
構文
useEffect(() => { statement, [deps] })
  • statement:再レンダリング時に実行したい処理(文)
  • deps:依存配列
useEffectの挙動を見てみる

// ※あくまでuseEffectの挙動確認のため、推奨される利用方法ではない点のみ留意

  • useEffectの依存配列にcountを指定したことで、同コンポーネント内の他のState(下記の例ではhoge)が更新されても、countは再レンダリングされない。
import { useEffect, useState } from "react"

export const StateEffect = () => {
  const [count, setCount] = useState(0);
  const [hoge, setHoge] = useState('hoge');

  useEffect(() => {
    console.log(`count is ${count}`)
  }, [count])

  const hundleClick = () => setCount(prev => prev + 1);

  return (
    <>
      <button onClick={() => setHoge(Date.now())}>{hoge}</button>
      <button onClick={hundleClick}>カウント</button>
    </>
  )
}
依存配列を省略(記述しない)した場合
  useEffect(() => {
    console.log(`count is ${count}`)
  })
// 通常のReactの再レンダリングと同様の挙動になる
依存配列を空配列にした場合
  useEffect(() => {
    console.log(`count is ${count}`)
  }, [])
// 初回のレンダリング時のみ処理が実行される