Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
71
Help us understand the problem. What is going on with this article?
@yonetty

TypeScriptでReact Hooksに入門するチュートリアル

はじめに

本投稿の背景と目的

React HooksはReactアプリケーションを開発する際のファーストチョイスになっていると言っても過言ではありません。
Reactの初学者がHooksを学ぶ際に、一通りの情報は公式ドキュメントにまとまっているのですが、従来の公式チュートリアルのHooks版があったらいいのにな〜、と思いました。

というわけで、React Hooksに入門するためのチュートリアルを提供することが本稿の目的です。

なお、このチュートリアルではフックの中でも最も基本的かつ重要な useStateフックとuseEffectフックを習得対象とします。まずはこの2つを覚えれば、ちょっとしたReactアプリケーションの開発を始めることが可能です。

(2020-10-11追記)
「次に学ぶこと」の章を追加しました。
(2021-1-27修正)
ステップ4で追加されたBookに設定するidの値に不備がありkey重複を引き起こしていたためコードを修正

対象とする読者

  • React Hooks以前のReactコンポーネント開発の基礎は理解しており、これからReact Hooksを学びたい方

Hooksを使わない、従来のクラスコンポーネント・関数コンポーネントによる開発方法を復習したい方は、公式チュートリアルや、私が以前に書いた記事などを参考にして頂ければと思います。

開発環境

本チュートリアルは、CodeSandboxの環境を使ってブラウザ上でステップ・バイ・ステップでアプリケーションを開発していく流れとなっています。
ブラウザ上ではなくローカル環境で進めたいという方は、 付録B ローカル開発環境のセットアップ を参考にして環境を構築ください。

動作確認済みの環境

アプリケーションの動作確認は、CodeSandboxの他、以下の環境で行いました。

  • Mac OS X 10.15.6
  • node v11.10.1
  • npm 6.14.5
  • yarn 1.19.1
  • react 16.13.1
  • typescript 3.7.2

これからつくるもの

このチュートリアルでは、これから読みたい・買いたい本をストックしておくちょっとしたWebアプリケーションを作成します。
書籍情報はGoogle Books APIsを利用してAjaxで取得します。

react_00.png

Qiitaに動画をアップロードできなかったので、動かしている様子は個人ブログへアップしました。

最終的な結果をここで確認することができます:最終結果
チュートリアルを進める前に実際に画面を操作して動作を把握しておくことをお勧めします。

ステップ1:書籍のリストを表示する

スターターコードを確認する

ブラウザからスターターコードを開いてください。以下のようにCodeSandboxの画面が表示されるはずです。
保存時に自動でフォークされて自分専用のサンドボックスとなりますので、このあと自由にファイルの追加や編集を行ってください。

react_01.png

左側のペインにはサンドボックスの情報やエクスプローラが表示されています。中央のペインでコード編集を行い、右側のペインでリアルタイムに表示を確認することができます。
スターターには以下のファイルを事前に準備済みです。

ファイル 説明
App.css アプリケーション共通で利用するスタイルを定義(ReactにおけるCSSの管理方法は本稿範囲外なので、全てのスタイルをここで定義している)
App.tsx アプリケーションのメインReactコンポーネント
BookDescription.ts APIで取得する書籍情報の型を定義
BookToRead.ts アプリケーションで保管する書籍情報の型を定義
index.css index.tsxで利用するスタイルを定義
index.tsx エントリポイントとなるReactコンポーネント
package.json npmの構成ファイル
tsconfig.json TypeScriptの構成ファイル

上記のうち、App.tsx以外のファイルはチュートリアルの中で変更することはありません。

ダミーデータをリスト表示する

App.tsxには予めダミーの書籍データを用意してあるので、まずはこれをリスト形式で表示するコードを書きましょう。

App.tsx
const dummyBooks: BookToRead[] = [
  {
    id: 1,
    title: "はじめてのReact",
    authors: "ダミー",
    memo: ""
  },
  {
    id: 2,
    title: "React Hooks入門",
    authors: "ダミー",
    memo: ""
  },
  {
    id: 3,
    title: "実践Reactアプリケーション開発",
    authors: "ダミー",
    memo: ""
  }
];

CodeSandBoxのエクスプローラから、srcフォルダの下に新規ファイルBookRow.tsxを作成してください。
まずはインポート。

BookRow.tsx
import React from "react";
import { BookToRead } from "./BookToRead";

次に、propsの型を定義します。

BookRow.tsx
type BookRowProps = {
  book: BookToRead;
  onMemoChange: (id: number, memo: string) => void;
  onDelete: (id: number) => void;
};

BookToRead型の書籍情報(book)のほか、メモ項目の変更イベントのコールバック、書籍削除イベントのコールバックを持たせておきます。
そして、関数コンポーネントの本体を定義してエクスポートします。

const BookRow = (props: BookRowProps) => {
  const { title, authors, memo } = props.book;

  const handleMemoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    props.onMemoChange(props.book.id, e.target.value);
  };

  const handleDeleteClick = () => {
    props.onDelete(props.book.id);
  };

  return (
    <div className="book-row">
      <div title={title} className="title">
        {title}
      </div>
      <div title={authors} className="authors">
        {authors}
      </div>
      <input
        type="text"
        className="memo"
        value={memo}
        onChange={handleMemoChange}
      />
      <div className="delete-row" onClick={handleDeleteClick}>
        削除
      </div>
    </div>
  );
};

export default BookRow;

このコンポーネントは表示とイベント伝搬を行うのみで状態管理は必要としないため、React Hooksの出番はなく、通常の関数コンポーネントとして実装できます。つまり、propsで受け取ったプロパティを用いてレンダリングを行い、子コンポーネントでonChangeonClickなどのイベントが発生した際はpropsのプロパティを通じて親コンポーネントにイベントを伝搬します。

では次にApp.tsxから今作ったコンポーネントを利用して表示を行いましょう。

インポート文を追加します。

App.tsx
import BookRow from "./BookRow";

ダミーデータを各要素をJSX要素に変換して変数に格納しましょう。今はまだBookRowコンポーネントから発火されるイベントは無視します。

App.tsx
const App = () => {
  const bookRows = dummyBooks.map((b) => {
    return (
      <BookRow
        book={b}
        key={b.id}
        onMemoChange={(id, memo) => {}}
        onDelete={(id) => {}}
      />
    );
  });

繰り返し出力するコンポーネントに対しては、key属性を付ける必要があることを思い出してください。
次に、コンポーネントの戻り値となるJSX要素内に展開されるように記述します(クラス名がmainsection要素配下)。

  return (
    <div className="App">
      <section className="nav">
        <h1>読みたい本リスト</h1>
        <div className="button-like">本を追加</div>
      </section>
      <section className="main">{bookRows}</section>
    </div>
  );

これで、以下のようにリスト表示されるようになったでしょう。

react_02.png

これでステップ1は終了です。この時点でのコードはこのようになっているはずです。

ステップ2:書籍の削除とメモ書きを実装する

このステップの内容は、ステップ1終了時の状態から続けて実装します。

useStateフックによる状態管理

書籍の削除やメモ書きの変更のイベントを拾い、画面に反映させるためには、書籍のリストをコンポーネントのステート変数として管理する必要があります。
従来のReactでは、そのためにはクラスコンポーネントを作成して、this.stateの中に状態を管理する必要がありました。React Hooksの導入以後は、関数コンポーネントにおいても状態管理の実現が可能となりました。

そのために用いるのがuseStateフックです。
実際にコードを書きながら、使い方を確認しましょう。

今回のステップでやりたいことは以下です。

  • dummyBooksの内容を初期状態とするステート変数を作成する
  • 書籍削除のイベントを拾って上記ステート変数を更新する
  • 同じくメモ書き変更のイベントを拾ってステート変数を更新する

順に実装していきましょう。

書籍のリストを状態管理する

まずはuseState関数をインポートします。

App.tsx
import React, { useState } from "react";

このuseState関数を利用して、ステート変数とその更新用関数を取得します。

App.tsx
const App = () => {
  const [books, setBooks] = useState(dummyBooks);

useState関数の引数には、そのステート変数の初期値を指定します。
上記のコード例だと、初めてApp関数コンポーネントが呼び出された際(初回のレンダリング時)にbooks変数に格納されているのはdummyBooksで定義したダミーの書籍データの配列となります。

ステート変数booksの内容を表示するようにコードを修正します(dummyBooks->books)。ブラウザに表示される結果が変わらないことを確認してください。

App.tsx
  const bookRows = books.map((b) => {
    return (
      <BookRow
        book={b}
        key={b.id}
        onMemoChange={(id, memo) => {}}
        onDelete={(id) => {}}
      />
    );
  });

削除イベントのハンドリング

削除イベントのハンドラ関数を定義しましょう。書籍のIDを受け取り、該当する書籍を配列から削除します。
クラスコンポーネントにおけるstateの更新と同様、ステート変数の配列を直接操作するのではなく、新しい配列を生成して更新用関数に渡す点に注意してください。

App.tsx
  const [books, setBooks] = useState(dummyBooks);

  const handleBookDelete = (id: number) => {
    const newBooks = books.filter((b) => b.id !== id);
    setBooks(newBooks);
  };

ここでは、filter関数を使って、IDが一致する書籍を除外した配列を生成し、setBooks関数を通じてステート変数の更新を行います。
JSXを修正し、BookRowコンポーネントのonDelete属性で先程のハンドラを呼び出すようにします。

App.tsx
  const bookRows = books.map((b) => {
    return (
      <BookRow
        book={b}
        key={b.id}
        onMemoChange={(id, memo) => {}}
        onDelete={(id) => handleBookDelete(id)}
      />
    );
  });

実際の画面から任意の行の削除ボタンをクリックし、行が消えるようになったことを確認してください。

メモ書き変更イベントのハンドリング

同様のイベントのハンドラ関数を定義します。

  const handleBookMemoChange = (id: number, memo: string) => {
    const newBooks = books.map((b) => {
      return b.id === id
        ? { ...b, memo: memo }
        : b;
    });
    setBooks(newBooks);
  }

booksに格納されている書籍データの配列のうち、IDが合致する要素はメモを更新した値を、それ以外の要素はそのままの値で新しい配列に格納します。

{ ...b, memo: memo }

上のコードは、bの各プロパティを展開し、memoプロパティだけを上書きした新しいオブジェクトを生成しています。

JSXを修正しイベントとハンドラの紐付けを行います。

App.tsx
  const bookRows = books.map((b) => {
    return (
      <BookRow
        book={b}
        key={b.id}
        onMemoChange={(id, memo) => handleBookMemoChange(id, memo)}
        onDelete={(id) => handleBookDelete(id)}
      />
    );
  });

書籍リストのメモ欄が編集できるようになったことを画面で確認しましょう。

これでステップ2は終了です。この時点でのコードはこのようになっているはずです。

ステップ3:書籍を検索して追加する

このステップの内容は、ステップ2終了時の状態から続けて実装します。

検索ダイアログ

書籍の検索はモーダルダイアログで実現するため、react-modalライブラリを利用します。
CodeSandboxには既に依存関係を登録済みです。ローカル開発環境用のスタータープロジェクトにも登録済みですが、もしcreate-react-appで一から環境を構築する場合は以下のようにライブラリを追加します。

$ yarn add react-modal
$ yarn add @types/react-modal

これから作成するのは以下のようなダイアログコンポーネントです。

react_03.png

各々の書籍情報をタイルっぽい形で表示するコンポーネントを先に作ります。

BookSearchItemコンポーネント

CodeSandboxのエクスプローラから、新しいファイルBookSearchItem.tsxを作成してください。

まずはインポートとpropsの型定義。

BookSearchItem.tsx
import React from "react";
import { BookDescription } from "./BookDescription";

type BookSearchItemProps = {
  description: BookDescription;
  onBookAdd: (book: BookDescription) => void;
};

BookDescription はAPIで取得した書籍情報のうち、タイトル、著者(群)、サムネイル画像(のURL)を保持する型です。それに加えて、サムネイル画像の右下にある「+」をクリックした際のイベントを拾うコールバック関数を含めたものが当コンポーネントのpropsとなります。

続いてコンポーネント本体となる関数を定義し、エクスポートします。

BookSearchItem.tsx
const BookSearchItem = (props: BookSearchItemProps) => {
  const { title, authors, thumbnail } = props.description;
  const handleAddBookClick = () => {
    props.onBookAdd(props.description);
  };
  return (
    <div className="book-search-item">
      <h2 title={title}>{title}</h2>
      <div className="authors" title={authors}>
        {authors}
      </div>
      {thumbnail ? <img src={thumbnail} alt="" /> : null}
      <div className="add-book" onClick={handleAddBookClick}>
        <span>+</span>
      </div>
    </div>
  );
};

export default BookSearchItem;

特に難しいところはないかと思うので説明は割愛します。

BookSearchDialogコンポーネント(基本部分)

検索ダイアログの基本部分から作っていきましょう。エクスプローラから新規ファイルBookSearchDialog.tsxを作成してください。

インポートとpropsの型定義です。コンポーネントのプロパティとしては、検索結果の表示最大件数(maxResults)と書籍追加イベントを拾うコールバック関数(onBookAdd)を持たせます。

BookSearchDialog.tsx
import React, { useState } from "react";
import { BookDescription } from "./BookDescription";
import BookSearchItem from "./BookSearchItem";

type BookSearchDialogProps = {
  maxResults: number;
  onBookAdd: (book: BookDescription) => void;
};

コンポーネント本体の関数を実装していきましょう。まずはuseState関数を使ってステート変数を定義します。

BookSearchDialog.tsx
const BookSearchDialog = (props: BookSearchDialogProps) => {
  const [books, setBooks] = useState([] as BookDescription[]);
  const [title, setTitle] = useState("");
  const [author, setAuthor] = useState("");

booksは書籍の検索結果を表す配列。初期値は空の配列です。
title authorは検索条件のタイトルおよび著者名。どちらも初期値は空文字列です。

次にイベントハンドラのコールバック関数。
タイトル、著者名のinput要素のonChangeイベントを拾い、それぞれのステート変数を更新します。

BookSearchDialog.tsx
  const handleTitleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setTitle(e.target.value);
  };

  const handleAuthorInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setAuthor(e.target.value);
  };

実際にこれらのイベントが発火してsetXXXの関数呼び出しを通じてステート変数が変化すると、Reactはそれを検知してコンポーネントの再レンダリングを行います。
再レンダリングを行うということは即ちBookSearchDialog関数が呼び出されるということです。

例えばタイトルに「A」と入力した場合、setTitle(e.target.value) によって、ステート変数の値がAに更新されます。その後再レンダリングのためにBookSearchDialog関数が呼び出されると(初回レンダリング時と同様に)以下のコード行が実行されますが、このときuseStateが返すtitleの値はAになっています。

const [title, setTitle] = useState("");

このあたりのステート変数の更新と再レンダリングの仕組みは押さえておく必要があります。(なお、親コンポーネントから渡されるpropsの値が変更された場合も、子コンポーネントは再レンダリングされます)

さらに、検索ボタンのクリックイベントをハンドリングするコールバックも定義しましょう。(実際の検索処理は後で実装します)

BookSearchDialog.tsx
  const handleSearchClick = () => {
    if (!title && !author) {
      alert("条件を入力してください");
      return;
    }
    // 検索実行
  };

書籍追加イベントに対するコールバックも実装しておきます。これは子のBookSearchItemで発火したイベントを親コンポーネントへ伝搬するだけです。

BookSearchDialog.tsx
  const handleBookAdd = (book: BookDescription) => {
    props.onBookAdd(book);
  };

最後にレンダリング処理です。
検索結果はBookSearchItemコンポーネントを配列の要素数だけ繰り返し出力します。各イベントとハンドラの紐付けを行いましょう。

 const bookItems = books.map((b, idx) => {
    return (
      <BookSearchItem
        description={b}
        onBookAdd={(b) => handleBookAdd(b)}
        key={idx}
      />
    );
  });

  return (
    <div className="dialog">
      <div className="operation">
        <div className="conditions">
          <input
            type="text"
            onChange={handleTitleInputChange}
            placeholder="タイトルで検索"
          />
          <input
            type="text"
            onChange={handleAuthorInputChange}
            placeholder="著者名で検索"
          />
        </div>
        <div className="button-like" onClick={handleSearchClick}>
          検索
        </div>
      </div>
      <div className="search-results">{bookItems}</div>
    </div>
  );

エクスポートも忘れないように。

BookSearchDialog.tsx
export default BookSearchDialog;

ちょっと実装内容が多かったので、現時点のSearchBookDialog.tsxのコード全量を載せておきます。

SearchBookDialog.tsx(全量)
import React, { useState } from "react";
import { BookDescription } from "./BookDescription";
import BookSearchItem from "./BookSearchItem";

type BookSearchDialogProps = {
  maxResults: number;
  onBookAdd: (book: BookDescription) => void;
};

const BookSearchDialog = (props: BookSearchDialogProps) => {
  const [books, setBooks] = useState([] as BookDescription[]);
  const [title, setTitle] = useState("");
  const [author, setAuthor] = useState("");

  const handleTitleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setTitle(e.target.value);
  };

  const handleAuthorInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setAuthor(e.target.value);
  };

  const handleSearchClick = () => {
    if (!title && !author) {
      alert("条件を入力してください");
      return;
    }
    // 検索実行
  };

  const handleBookAdd = (book: BookDescription) => {
    props.onBookAdd(book);
  };

  const bookItems = books.map((b, idx) => {
    return (
      <BookSearchItem
        description={b}
        onBookAdd={(b) => handleBookAdd(b)}
        key={idx}
      />
    );
  });

  return (
    <div className="dialog">
      <div className="operation">
        <div className="conditions">
          <input
            type="text"
            onChange={handleTitleInputChange}
            placeholder="タイトルで検索"
          />
          <input
            type="text"
            onChange={handleAuthorInputChange}
            placeholder="著者名で検索"
          />
        </div>
        <div className="button-like" onClick={handleSearchClick}>
          検索
        </div>
      </div>
      <div className="search-results">{bookItems}</div>
    </div>
  );
};

export default BookSearchDialog;

App.tsxからのダイアログ表示

次に、「本を追加」ボタンクリックで検索ダイアログをモーダル表示するようにApp.tsxに手を加えます。

まずはインポートの追加と、react-modalを利用するための準備を行います。

App.tsx
import Modal from "react-modal";
import BookSearchDialog from "./BookSearchDialog";

Modal.setAppElement("#root");

const customStyles = {
  overlay: {
    backgroundColor: "rgba(0, 0, 0, 0.8)"
  },
  content: {
    top: "50%",
    left: "50%",
    right: "auto",
    bottom: "auto",
    marginRight: "-50%",
    padding: 0,
    transform: "translate(-50%, -50%)"
  }
};

Modal.setAppElementの呼び出しにより、モーダル表示時にオーバーレイで覆うDOM領域を指定します。
customStylesはモーダルダイアログおよびオーバーレイの外観のスタイル設定となります。ここでは上記の通りコード入力してください。

次にステート変数を追加します。

App.tsx
const App = () => {
  const [books, setBooks] = useState(dummyBooks);
  const [modalIsOpen, setModalIsOpen] = useState(false);

「モーダルダイアログが開いているかどうか」という画面モードをステート変数として持たせて切り替えを行います。最初は閉じていてほしいので、初期値をfalseにしています。

イベントハンドラを定義しましょう。

App.tsx
  const handleAddClick = () => {
    setModalIsOpen(true);
  };

  const handleModalClose = () => {
    setModalIsOpen(false);
  };

handleAddClickは「本を追加」ボタンのクリックに対するものなので、モーダルダイアログを開くためにsetModalIsOpentrueを指定します。
handleModalCloseはモーダルダイアログが開かれた状態で、ダイアログの領域外をクリックした際に呼び出されるものです。(falseを指定します)

JSXにコードを追加します。

App.tsx
  return (
    <div className="App">
      <section className="nav">
        <h1>読みたい本リスト</h1>
        <div className="button-like" onClick={handleAddClick}>
          本を追加
        </div>
      </section>
      <section className="main">{bookRows}</section>
      <Modal
        isOpen={modalIsOpen}
        onRequestClose={handleModalClose}
        style={customStyles}
      >
        <BookSearchDialog maxResults={20} onBookAdd={(b) => {}} />
      </Modal>
    </div>
  );

修正箇所は以下の2つです。

  • 「本を追加」のdivonClick属性を付与し、handleAddClickコールバック関数を紐付け
  • Modalコンポーネントを配置し、その子コンポーネントとして先ほど作成したBookSearchDialogコンポーネントを指定

これで、検索ダイアログがモーダル表示できるようになったはずです。(検索処理は未実装なので動作しません)
ダイアログを閉じたい場合は、ダイアログの外をクリックしてください。

検索処理を実装する

検索ダイアログで入力した条件で、Google Books APIsを呼び出して結果を表示できるようにしましょう。

まずは、API呼び出しで利用する関数をBookSearchDialog.tsxのインポート文の後ろあたりに作成します。実際のアプリケーションではAPI呼び出し処理は別ファイルにした方がよさそうですが、サンプルなのでBookSearchDialog.tsxに入れることにします。

BookSearchDialog.tsx
function buildSearchUrl(title: string, author: string, maxResults: number): string {
  let url = "https://www.googleapis.com/books/v1/volumes?q=";
  const conditions: string[] = []
  if (title) {
    conditions.push(`intitle:${title}`);
  }
  if (author) {
    conditions.push(`inauthor:${author}`);
  }
  return url + conditions.join('+') + `&maxResults=${maxResults}`;
}

function extractBooks(json: any): BookDescription[] {
  const items: any[] = json.items;
  return items.map((item: any) => {
    const volumeInfo: any = item.volumeInfo;
    return {
      title: volumeInfo.title,
      authors: volumeInfo.authors ? volumeInfo.authors.join(', ') : "",
      thumbnail: volumeInfo.imageLinks ? volumeInfo.imageLinks.smallThumbnail : "",
    }
  });
}

buildSearchUrlはAPIのURLを組み立てる関数、extractBooksはAPIの呼び出し結果(JSON)からコンポーネントが欲しい形でデータを抽出する関数です。上記をコピー&ペーストしてください。

次にAPI呼び出しを実装していきます。
タイミングとしては検索ボタンクリックの際なので、以下のイベントハンドラの「検索実行」の箇所に書けばよいでしょうか?

BookSearchDialog.tsx
  const handleSearchClick = () => {
    if (!title && !author) {
      alert("条件を入力してください");
      return;
    }
    // 検索実行
  };

React HooksにおいてはサーバとのAPI通信やLocalStorageへのアクセス等の、コンポーネント内に閉じない処理は副作用(あるいは作用)と呼ばれます。
副作用を実装する仕組みとして、useEffectフックが提供されていますので、その方法をサンプルを通して確認していきましょう。

イベントハンドラ内ではAPIによる検索処理を行わず、モードを変更するのみにします。

BookSearchDialog.tsx
  const handleSearchClick = () => {
    if (!title && !author) {
      alert("条件を入力してください");
      return;
    }
    setIsSearching(true);
  };

isSearchingは現在(のレンダリング処理時点で)検索処理実行中であることを表すboolean型のステート変数で、もちろんuseState関数を使って定義します。

BookSearchDialog.tsx
  const [isSearching, setIsSearching] = useState(false);

useEffectをインポートしましょう。

BookSearchDialog.tsx
import React, { useState, useEffect } from "react";

useStateを使ったステート変数定義の後ろに、useEffectを使った副作用の実装を記述します。

BookSearchDialog.tsx
  useEffect(() => {
    if (isSearching) {
      const url = buildSearchUrl(title, author, props.maxResults);
      fetch(url)
        .then((res) => {
          return res.json();
        })
        .then((json) => {
          return extractBooks(json);
        })
        .then((books) => {
          setBooks(books);
        })
        .catch((err) => {
          console.error(err);
        });
    }
    setIsSearching(false);
  }, [isSearching]);

useEffectの第1引数には、副作用を記述した関数を渡します。
ここでは、isSearchingtrueの場合に以下の副作用を実行します。(isSearchingfalseの場合にもこの関数が呼び出されるので、条件判断は必須です)

  • fetch関数によるAPIコール(Ajax)
  • 結果のJSONから書籍のデータを抽出
  • setBooks関数によるステート変数booksの更新

useEffectの第2引数には、副作用が依存するステート変数およびpropsのプロパティを列挙した配列を渡します。
Reactは、この配列に含まれるステート変数またはプロパティのいずれかの変更を検知した場合にのみ、副作用の関数呼び出しを行います。(省略した場合は、レンダリングの都度毎回呼び出されることになります)

今回の副作用のコードは、検索ボタンがクリックした時に実行されればよいので、isSearchingのみを配列に入れています。(title author props.maxResultsも参照してるぞとeslintに怒られますが、無視してください。気になる場合はこれらを追加しても動作に影響はないはずです)

これで、APIを使って検索を行い結果表示もできるようになったはずなので、動作確認をしてみてください。

書籍を選んでメイン画面のリストへ追加する

書籍の追加(「+」ボタンクリック)時のイベントはBookSearchItem=>BookSearchDialog=>Appと順に伝搬していくように実装済みなので、Appコンポーネントにイベントハンドラを実装します。

その前にAppコンポーネントに置いてあったダミーの書籍情報は不要になったので削除し(const dummyBooks ...を消す)、ステート変数の初期値も空配列にしておきましょう。

App.tsx
  const [books, setBooks] = useState([] as BookToRead[]);

イベント引数でBookDescriptionを受け取るのでインポート文を追加します。

App.tsx
import {BookDescription} from "./BookDescription";

イベントハンドラとなるコールバック関数は以下のようになります。

App.tsx
  const handleBookAdd = (book: BookDescription) => {
    const newBook: BookToRead = { ...book, id: Date.now(), memo: "" };
    const newBooks = [...books, newBook];
    setBooks(newBooks);
    setModalIsOpen(false);
  }

const newBooks = [...books, newBook]で現在の書籍リストの末尾に、検索ダイアログで選択した書籍情報(から作ったBookToReadオブジェクト)を追加し、setBooks関数でステート変数を更新します。
また、追加後はモーダルダイアログを閉じるために、同様にsetModalIsOpen関数でステート変数を更新します。

JSXを修正し、イベントとイベントハンドラを紐付けます。

<BookSearchDialog maxResults={20} onBookAdd={(b) => handleBookAdd(b)} />

長くなりましたが、これでステップ3は終了です。この時点でのコードはこのようになっているはずです。

ステップ4:書籍を検索して追加する

このステップの内容は、ステップ3終了時の状態から続けて実装します。

ここまででアプリケーションの動作としてはほぼ完成していますが、ブラウザをリロードしても書籍リストが消えてしまわないように、LocalStorageにデータを保存するように実装しましょう。

LocalStorageへのアクセスキーを定数定義します。

App.tsx
const APP_KEY = "react-hooks-tutorial"

const App = () => {

既に述べたように、LocalStorageの読み書きようなコンポーネント内で閉じない処理は副作用としてuseEffect関数を用いて実装するのでした。
useEffectのインポートを追加しておきましょう。

import React, { useState, useEffect } from "react";

まずは書き込み処理です。useStateによるステート変数取得処理の後ろあたりに以下のコードを記述します。

App.tsx
  useEffect(() => {
    localStorage.setItem(APP_KEY, JSON.stringify(books));
  }, [books]);

useEffectの第1引数に渡す関数には、books配列を文字列化した値をLocalStorageに書き込む処理を記述します。
第2引数にはbooks配列を指定することで、booksの内容が更新される都度、この副作用関数が実行されるようになります。

次は読み込み処理です。(注意)先ほどのuseEffectよりも前に記述してください

App.tsx
  useEffect(() => {
    const storedBooks = localStorage.getItem(APP_KEY);
    if (storedBooks) {
      setBooks(JSON.parse(storedBooks));
    }
  }, []);

この副作用は初回のレンダリング時に一度だけ実行すればよいので、このようなケースでは第2引数に空配列[]をしてください。

書籍の追加やメモの更新を行った後、ブラウザをリロードして書籍リストが保持されていることを確認してください。

これでチュートリアルのすべてのステップが完了しました。お疲れさまでした!
最終結果はこちらです。

次に学ぶこと

本チュートリアルで取り上げたuseStateフックとuseEffectフック以外にも、様々なフックが用意されています(useRefuseContextuseReducer、...etc)。独自のフック(カスタムフック)を定義して利用することも可能です。

本チュートリアルのステップ5以降をこちらの記事で公開しており、その他のフックの利用方法を順次アップデートしていっています。

付録

付録A ソースコード一式

GitHubに上げています。

付録B ローカル開発環境のセットアップ

スターター用のプロジェクトをGitHubに用意しましたので使ってください。
前提条件:以下がインストール済みであること

  • node
  • npm
  • (yarn)
  • git
$ git clone https://github.com/yonetty/react-hooks-tutorial-starter.git
$ cd react-hooks-tutorial-starter
$ yarn install
$ yarn start

もちろんnpmでやってもらっても構いません。(npm install npm start に読み替えてください)

ブラウザでスターターの画面が表示されればOKです。

react_04.png

71
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
yonetty
某SIerでアーキテクトとしてエンタープライズ向けシステム・製品の開発に携わっています。 Twitter: @tyonekubo

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
71
Help us understand the problem. What is going on with this article?