はじめに
本投稿の背景と目的
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で取得します。
Qiitaに動画をアップロードできなかったので、動かしている様子は個人ブログへアップしました。
最終的な結果をここで確認することができます:最終結果
チュートリアルを進める前に実際に画面を操作して動作を把握しておくことをお勧めします。
ステップ1:書籍のリストを表示する
スターターコードを確認する
ブラウザからスターターコードを開いてください。以下のようにCodeSandboxの画面が表示されるはずです。
保存時に自動でフォークされて自分専用のサンドボックスとなりますので、このあと自由にファイルの追加や編集を行ってください。
左側のペインにはサンドボックスの情報やエクスプローラが表示されています。中央のペインでコード編集を行い、右側のペインでリアルタイムに表示を確認することができます。
スターターには以下のファイルを事前に準備済みです。
ファイル | 説明 |
---|---|
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
には予めダミーの書籍データを用意してあるので、まずはこれをリスト形式で表示するコードを書きましょう。
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
を作成してください。
まずはインポート。
import React from "react";
import { BookToRead } from "./BookToRead";
次に、propsの型を定義します。
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
で受け取ったプロパティを用いてレンダリングを行い、子コンポーネントでonChange
やonClick
などのイベントが発生した際はprops
のプロパティを通じて親コンポーネントにイベントを伝搬します。
では次にApp.tsx
から今作ったコンポーネントを利用して表示を行いましょう。
インポート文を追加します。
import BookRow from "./BookRow";
ダミーデータを各要素をJSX要素に変換して変数に格納しましょう。今はまだBookRow
コンポーネントから発火されるイベントは無視します。
const App = () => {
const bookRows = dummyBooks.map((b) => {
return (
<BookRow
book={b}
key={b.id}
onMemoChange={(id, memo) => {}}
onDelete={(id) => {}}
/>
);
});
繰り返し出力するコンポーネントに対しては、key
属性を付ける必要があることを思い出してください。
次に、コンポーネントの戻り値となるJSX要素内に展開されるように記述します(クラス名がmain
のsection
要素配下)。
return (
<div className="App">
<section className="nav">
<h1>読みたい本リスト</h1>
<div className="button-like">本を追加</div>
</section>
<section className="main">{bookRows}</section>
</div>
);
これで、以下のようにリスト表示されるようになったでしょう。
これでステップ1は終了です。この時点でのコードはこのようになっているはずです。
ステップ2:書籍の削除とメモ書きを実装する
このステップの内容は、ステップ1終了時の状態から続けて実装します。
useStateフックによる状態管理
書籍の削除やメモ書きの変更のイベントを拾い、画面に反映させるためには、書籍のリストをコンポーネントのステート変数として管理する必要があります。
従来のReactでは、そのためにはクラスコンポーネントを作成して、this.state
の中に状態を管理する必要がありました。React Hooks
の導入以後は、関数コンポーネントにおいても状態管理の実現が可能となりました。
そのために用いるのがuseState
フックです。
実際にコードを書きながら、使い方を確認しましょう。
今回のステップでやりたいことは以下です。
-
dummyBooks
の内容を初期状態とするステート変数を作成する - 書籍削除のイベントを拾って上記ステート変数を更新する
- 同じくメモ書き変更のイベントを拾ってステート変数を更新する
順に実装していきましょう。
書籍のリストを状態管理する
まずはuseState
関数をインポートします。
import React, { useState } from "react";
このuseState
関数を利用して、ステート変数とその更新用関数を取得します。
const App = () => {
const [books, setBooks] = useState(dummyBooks);
useState
関数の引数には、そのステート変数の初期値を指定します。
上記のコード例だと、初めてApp
関数コンポーネントが呼び出された際(初回のレンダリング時)にbooks
変数に格納されているのはdummyBooks
で定義したダミーの書籍データの配列となります。
ステート変数books
の内容を表示するようにコードを修正します(dummyBooks
->books
)。ブラウザに表示される結果が変わらないことを確認してください。
const bookRows = books.map((b) => {
return (
<BookRow
book={b}
key={b.id}
onMemoChange={(id, memo) => {}}
onDelete={(id) => {}}
/>
);
});
削除イベントのハンドリング
削除イベントのハンドラ関数を定義しましょう。書籍のIDを受け取り、該当する書籍を配列から削除します。
クラスコンポーネントにおけるstate
の更新と同様、ステート変数の配列を直接操作するのではなく、新しい配列を生成して更新用関数に渡す点に注意してください。
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
属性で先程のハンドラを呼び出すようにします。
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を修正しイベントとハンドラの紐付けを行います。
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
これから作成するのは以下のようなダイアログコンポーネントです。
各々の書籍情報をタイルっぽい形で表示するコンポーネントを先に作ります。
BookSearchItemコンポーネント
CodeSandboxのエクスプローラから、新しいファイルBookSearchItem.tsx
を作成してください。
まずはインポートとpropsの型定義。
import React from "react";
import { BookDescription } from "./BookDescription";
type BookSearchItemProps = {
description: BookDescription;
onBookAdd: (book: BookDescription) => void;
};
BookDescription
はAPIで取得した書籍情報のうち、タイトル、著者(群)、サムネイル画像(のURL)を保持する型です。それに加えて、サムネイル画像の右下にある「+」をクリックした際のイベントを拾うコールバック関数を含めたものが当コンポーネントのprops
となります。
続いてコンポーネント本体となる関数を定義し、エクスポートします。
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
)を持たせます。
import React, { useState } from "react";
import { BookDescription } from "./BookDescription";
import BookSearchItem from "./BookSearchItem";
type BookSearchDialogProps = {
maxResults: number;
onBookAdd: (book: BookDescription) => void;
};
コンポーネント本体の関数を実装していきましょう。まずはuseState
関数を使ってステート変数を定義します。
const BookSearchDialog = (props: BookSearchDialogProps) => {
const [books, setBooks] = useState([] as BookDescription[]);
const [title, setTitle] = useState("");
const [author, setAuthor] = useState("");
books
は書籍の検索結果を表す配列。初期値は空の配列です。
title
author
は検索条件のタイトルおよび著者名。どちらも初期値は空文字列です。
次にイベントハンドラのコールバック関数。
タイトル、著者名のinput
要素のonChange
イベントを拾い、それぞれのステート変数を更新します。
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の値が変更された場合も、子コンポーネントは再レンダリングされます)
さらに、検索ボタンのクリックイベントをハンドリングするコールバックも定義しましょう。(実際の検索処理は後で実装します)
const handleSearchClick = () => {
if (!title && !author) {
alert("条件を入力してください");
return;
}
// 検索実行
};
書籍追加イベントに対するコールバックも実装しておきます。これは子のBookSearchItem
で発火したイベントを親コンポーネントへ伝搬するだけです。
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>
);
エクスポートも忘れないように。
export default BookSearchDialog;
ちょっと実装内容が多かったので、現時点の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
を利用するための準備を行います。
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
はモーダルダイアログおよびオーバーレイの外観のスタイル設定となります。ここでは上記の通りコード入力してください。
次にステート変数を追加します。
const App = () => {
const [books, setBooks] = useState(dummyBooks);
const [modalIsOpen, setModalIsOpen] = useState(false);
「モーダルダイアログが開いているかどうか」という画面モードをステート変数として持たせて切り替えを行います。最初は閉じていてほしいので、初期値をfalse
にしています。
イベントハンドラを定義しましょう。
const handleAddClick = () => {
setModalIsOpen(true);
};
const handleModalClose = () => {
setModalIsOpen(false);
};
handleAddClick
は「本を追加」ボタンのクリックに対するものなので、モーダルダイアログを開くためにsetModalIsOpen
にtrue
を指定します。
handleModalClose
はモーダルダイアログが開かれた状態で、ダイアログの領域外をクリックした際に呼び出されるものです。(false
を指定します)
JSXにコードを追加します。
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つです。
- 「本を追加」の
div
にonClick
属性を付与し、handleAddClick
コールバック関数を紐付け -
Modal
コンポーネントを配置し、その子コンポーネントとして先ほど作成したBookSearchDialog
コンポーネントを指定
これで、検索ダイアログがモーダル表示できるようになったはずです。(検索処理は未実装なので動作しません)
ダイアログを閉じたい場合は、ダイアログの外をクリックしてください。
検索処理を実装する
検索ダイアログで入力した条件で、Google Books APIsを呼び出して結果を表示できるようにしましょう。
まずは、API呼び出しで利用する関数をBookSearchDialog.tsx
のインポート文の後ろあたりに作成します。実際のアプリケーションではAPI呼び出し処理は別ファイルにした方がよさそうですが、サンプルなので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呼び出しを実装していきます。
タイミングとしては検索ボタンクリックの際なので、以下のイベントハンドラの「検索実行」の箇所に書けばよいでしょうか?
const handleSearchClick = () => {
if (!title && !author) {
alert("条件を入力してください");
return;
}
// 検索実行
};
React HooksにおいてはサーバとのAPI通信やLocalStorageへのアクセス等の、コンポーネント内に閉じない処理は副作用(あるいは作用)と呼ばれます。
副作用を実装する仕組みとして、useEffect
フックが提供されていますので、その方法をサンプルを通して確認していきましょう。
イベントハンドラ内ではAPIによる検索処理を行わず、モードを変更するのみにします。
const handleSearchClick = () => {
if (!title && !author) {
alert("条件を入力してください");
return;
}
setIsSearching(true);
};
isSearching
は現在(のレンダリング処理時点で)検索処理実行中であることを表すboolean
型のステート変数で、もちろんuseState
関数を使って定義します。
const [isSearching, setIsSearching] = useState(false);
useEffect
をインポートしましょう。
import React, { useState, useEffect } from "react";
useState
を使ったステート変数定義の後ろに、useEffect
を使った副作用の実装を記述します。
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引数には、副作用を記述した関数を渡します。
ここでは、isSearching
がtrue
の場合に以下の副作用を実行します。(isSearching
がfalse
の場合にもこの関数が呼び出されるので、条件判断は必須です)
-
fetch
関数によるAPIコール(Ajax) - 結果のJSONから書籍のデータを抽出
-
setBooks
関数によるステート変数books
の更新
useEffect
の第2引数には、副作用が依存するステート変数およびprops
のプロパティを列挙した配列を渡します。
Reactは、この配列に含まれるステート変数またはプロパティのいずれかの変更を検知した場合にのみ、副作用の関数呼び出しを行います。(省略した場合は、レンダリングの都度毎回呼び出されることになります)
今回の副作用のコードは、検索ボタンがクリックした時に実行されればよいので、isSearching
のみを配列に入れています。(title
author
props.maxResults
も参照してるぞとeslintに怒られますが、無視してください。気になる場合はこれらを追加しても動作に影響はないはずです)
これで、APIを使って検索を行い結果表示もできるようになったはずなので、動作確認をしてみてください。
書籍を選んでメイン画面のリストへ追加する
書籍の追加(「+」ボタンクリック)時のイベントはBookSearchItem
=>BookSearchDialog
=>App
と順に伝搬していくように実装済みなので、App
コンポーネントにイベントハンドラを実装します。
その前にApp
コンポーネントに置いてあったダミーの書籍情報は不要になったので削除し(const dummyBooks ...
を消す)、ステート変数の初期値も空配列にしておきましょう。
const [books, setBooks] = useState([] as BookToRead[]);
イベント引数でBookDescription
を受け取るのでインポート文を追加します。
import {BookDescription} from "./BookDescription";
イベントハンドラとなるコールバック関数は以下のようになります。
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へのアクセスキーを定数定義します。
const APP_KEY = "react-hooks-tutorial"
const App = () => {
既に述べたように、LocalStorageの読み書きようなコンポーネント内で閉じない処理は副作用としてuseEffect
関数を用いて実装するのでした。
useEffect
のインポートを追加しておきましょう。
import React, { useState, useEffect } from "react";
まずは書き込み処理です。useState
によるステート変数取得処理の後ろあたりに以下のコードを記述します。
useEffect(() => {
localStorage.setItem(APP_KEY, JSON.stringify(books));
}, [books]);
useEffect
の第1引数に渡す関数には、books
配列を文字列化した値をLocalStorageに書き込む処理を記述します。
第2引数にはbooks
配列を指定することで、books
の内容が更新される都度、この副作用関数が実行されるようになります。
次は読み込み処理です。(注意)先ほどのuseEffectよりも前に記述してください
useEffect(() => {
const storedBooks = localStorage.getItem(APP_KEY);
if (storedBooks) {
setBooks(JSON.parse(storedBooks));
}
}, []);
この副作用は初回のレンダリング時に一度だけ実行すればよいので、このようなケースでは第2引数に空配列[]
をしてください。
書籍の追加やメモの更新を行った後、ブラウザをリロードして書籍リストが保持されていることを確認してください。
これでチュートリアルのすべてのステップが完了しました。お疲れさまでした!
最終結果はこちらです。
次に学ぶこと
本チュートリアルで取り上げたuseState
フックとuseEffect
フック以外にも、様々なフックが用意されています(useRef
、useContext
、useReducer
、...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です。