前回までの記事で、React.jsの超入門的な内容を学習してきました。
ある程度の基礎用語や構文などを学習した後は、チュートリアルを実践することで、その言語やフレームワークの全体像を包括的に掴みたいですよね。
React.jsのチュートリアルは、公式サイトの三目並べやMDNのToDoアプリといった素晴らしいものが既にありますので、未実施の方はそちらに取り組んでいただくのも良いと思います。
今回の記事は、「もう少し趣向を変えたアプリを作ってみたい」という方に向けて、オリジナルアプリをチュートリアル形式でお届けします。
オリジナルアプリの仕様
作成するのは映画やドラマの管理アプリで、「FlickTracker」と名付けました。
「Flick」は映画やドラマを指すスラングで、それらをトラッキングするアプリという意味です。
最初から多機能にするとハードルが上がり過ぎてしまうので、まずは最小限の機能で完成まで進めていきます。
最初のリリースバージョンでは、次の機能を持ちます。
- 視聴したい映画・ドラマの登録/更新
- 視聴済に変更する/未視聴に戻す
- 簡単なレビューの登録/更新
- 映画・ドラマの検索
- 検索条件:タイトル、ジャンル、未視聴OR視聴済
チュートリアルの準備
まずは、Windows用の環境構築記事の方法でプロジェクトを作成(プロジェクト名は「flicktracker」に変更)し、本家のチュートリアルを改変しつつ機能を追加していきます。
プロジェクトが作成できたらVS Codeで「flicktracker」フォルダを開き、「src」フォルダ内の以下のファイルを処理してください。
- App.jsxのコードを全て消去
- index.cssのコードを全て消去
- App.cssを削除
これで、チュートリアルを開始するための準備が整いました。
Reactコンポーネントの作成
まずは、基本的なReactコンポーネントから着手していきましょう。
作成するのは以下の2ファイルです。
- 映画・ドラマを登録、更新するためのフォーム
- 登録済の映画・ドラマを一覧表示するためのリスト
ファイルを作成する前に、コンポーネントを配置するためのフォルダを作成します。
作成手順は次のとおりです。
- 「src」ディレクトリを右クリックして「新しいフォルダー…」を選択
- フォルダ名に「components」と入力し、確定(エンター)
フォームの外観を作成する
映画・ドラマを登録、更新するためのフォームを作成します。
まずは外観だけを作り、動作は後で追加していきます。
「タイトル」と「ジャンル」を登録できるようにするので、それぞれtitle、genreというstateを持ちます。
以前の記事「React.jsのstateとuseState」で述べたとおり、コンポーネントでstateを使用するには、useStateというReactフックをインポートする必要がありましたね。
今回はReactも一緒にインポートします。
それでは、「components」フォルダ内に、以下のとおりMovieForm.jsxを作成してください。
import React, {useState} from 'react'
function MovieForm() {
const [title, setTitle] = useState('')
const [genre, setGenre] = useState('')
return (
<form>
<input type="text"
placeholder='タイトル'
value={title}
/>
<input type="text"
placeholder='ジャンル'
value={genre}
/>
<button>追加</button>
</form>
)
}
export default MovieForm
フォームの外観ができたので、App.jsxを以下の通り編集し、ブラウザに表示できるようにしましょう。
import React from 'react'
import MovieForm from './components/MovieForm'
function App() {
return (
<div>
<h1>FlickTracker</h1>
<MovieForm />
</div>
)
}
export default App
いったんブラウザで確認してみましょう。
- コマンドプロンプトで次のコマンドを実行
npm run dev
- ブラウザを開き、URL「localhost:5173」にアクセスする
以下のように表示されていれば成功です。

フォームの動作を作成する
次の2つの動作を追加していきます。
- inputボックスに入力されたらstateを更新する
- 「追加」ボタンがクリックされたらリストを更新する
1.の動作から追加しましょう。
MovieForm.jsxに、以下の黄色マーカーのコードを追加してください。
import React, {useState} from 'react'
function MovieForm() {
const [title, setTitle] = useState('')
const [genre, setGenre] = useState('')
return (
<form>
<input type="text"
placeholder='タイトル'
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<input type="text"
placeholder='ジャンル'
value={genre}
onChange={(e) => setGenre(e.target.value)}
/>
<button>追加</button>
</form>
)
}
export default MovieForm
上記の通り、「コンポーネントが変更された」というイベントを扱うには、onChangeというイベントハンドラーを使用します。
イベントハンドラーには発生したイベントそのものがオブジェクトとして引数に渡されます。それが丸カッコ内の引数「e」です。
では続いて2.の動作を作成していきます。
MovieForm.jsxに、以下の黄色マーカーのコードを追加してください。
import React, {useState} from 'react'
function MovieForm() {
const [title, setTitle] = useState('')
const [genre, setGenre] = useState('')
function handleSubmit(e) {
e.preventDefault()
addMovie({
title,
genre,
watched: false,
review: ''
})
setTitle('')
setGenre('')
}
return (
<form onSubmit={handleSubmit}>
<input type="text"
placeholder='タイトル'
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<input type="text"
placeholder='ジャンル'
value={genre}
onChange={(e) => setGenre(e.target.value)}
/>
<button>追加</button>
</form>
)
}
export default MovieForm
上記の通り、「送信された」というイベントを扱うには、onSubmitを使用します。
コンポーネントがsubmit(送信)イベントを感知すると、handleSubmit関数が呼び出されます。handleSubmit関数の処理を詳しくみていきましょう。
発生したイベントはsubmitであるため、デフォルトの処理のままではページ遷移の処理が動作してしまいます。
やりたいのはページ遷移ではないので、デフォルトの処理をキャンセルする必要があります。
それをやってくれるのが、handleSubmit関数の1行目にある「e.preventDefault()」です。
続いて、赤太字で書かれているaddMovieという関数ですが、これはまだ作成されていません。
このsubmitは、映画・ドラマを登録する処理なので、関数本体を作る前に「映画を追加する」という意味の名前を付けています。
また、addMovieは次のデータを登録するよう設計しています。
- title(タイトル)
- genre(ジャンル)
- watched(視聴済:初期値はfalse)
- review(レビュー:初期値は空文字)
さて、それでは未作成のaddMovie関数は、どこに作成するのでしょうか。
これはReact.jsでは非常に重要な問題なので、見出しを変えて説明していきたいと思います。
addMovie関数を作成する
映画・ドラマを追加するaddMovie関数は、MovieForm.jsx内で呼び出されており、他のファイルから呼び出される想定はありません。
そういう意味では、「MovieForm.jsxの中に作成してしまえば良いのではないか」と思ってしまいますが、それはベストプラクティスではありません。
結論から言うと、addMovie関数はApp.jsxに作成します。
その理由は主に次の3点です。
- 親子コンポーネント間のデータ共有のため
Appコンポーネントがアプリ全体の状態(今回の場合は映画のリスト)を管理する想定で、addMovie関数を通じてこの状態を更新します。addMovie関数をMovieFormコンポーネント内に直接定義してしまうと、MovieFormコンポーネント内でしか状態を管理できなくなり、他のコンポーネント(例:後に作成するMovieList)とデータを共有するのが難しくなります。
- 状態の一元管理のため
Appコンポーネント内にaddMovie関数を定義することで、状態管理が一元化されます。状態が分散すると管理が煩雑になるため、状態を一箇所に集約することでアプリケーションの構造がシンプルでわかりやすくなります。
- データの流れを明確化するため
- Reactのデータフローは基本的に「上から下(親から子)」へと流れます。つまり、親コンポーネントから子コンポーネントへデータや関数を渡す形になります。これにより、データの流れが明確になり、デバッグや機能追加が容易になります。
では、親コンポーネントに作成された関数を、子コンポーネントからどのように呼び出すのでしょうか。
答えは、関数がpropsとして渡されるのです。
具体的に、App.jsxがどのようなコードになるかを提示します。
太字、黄色マーカーのコードが追加されているので、同じように編集してください。
import React, {useState} from 'react'
import MovieForm from './components/MovieForm'
function App() {
const [movies, setMovies] = useState([])
function addMovie(movie) {
setMovies([...movies, movie])
}
return (
<div>
<h1>FlickTracker</h1>
<MovieForm addMovie={addMovie} />
</div>
)
}
export default App
リスト(一覧)コンポーネントを作成する
登録済の映画・ドラマを一覧表示するコンポーネントを作成します。
「components」フォルダ内に、以下のとおりMovieList.jsxを作成してください。
import React from 'react'
function MovieList({movies}) {
return (
<div>
<h2>登録済の作品一覧</h2>
<ul>
{
movies.map((movie, index) => (
<li key={index}>
タイトル:{movie.title}<br />
ジャンル:{movie.genre}<br />
視聴:{movie.watched ? "済" : "未"}<br />
レビュー:{movie.review || ""}
</li>
))
}
</ul>
</div>
)
}
export default MovieList
ul要素の中でmapメソッドを使用して、ループしながらli要素を作成しています。mapメソッドに引数として渡しているアロー関数で、liのjsxが波カッコではなく丸カッコで囲まれていることに注意してください。
波カッコを使用すると処理のブロックが始まり、丸カッコを使用するとjsxを関数の戻り値として返すことを意味します。
続いて、上記のMovieListを使用するため、App.jsxを以下のように修正してください。
太字黄色マーカーのコードを追加します。
import React, {useState} from 'react'
import MovieForm from './components/MovieForm'
import MovieList from './components/MovieList'
function App() {
const [movies, setMovies] = useState([])
function addMovie(movie) {
setMovies([...movies, movie])
}
return (
<div>
<h1>FlickTracker</h1>
<MovieForm addMovie={addMovie} />
<MovieList movies={movies}></MovieList>
</div>
)
}
export default App
ここまでのプログラムの表示と動作を確認してみましょう。
以下のように、登録した映画が一覧表示されれば成功です。

この後も何かひとつ機能を追加したら、動作と表示を都度確認することで、エラー解決の難易度を下げることができます。
更新機能を追加する
少しデザインを整えたい気持ちもあるかもしれませんが、機能がまだ不十分なのでそちらから実装していきましょう。
実装が必要な機能は以下の通りです。
- 「視聴」の「未」「済」を変更する
- 「レビュー」を登録する
まずは1.の機能から着手します。
視聴状態の更新機能を実装する
変更するのはMovieList.jsxとApp.jsxの2ファイルです。
太字黄色マーカーのコードを追加、修正してください。
import React from 'react'
function MovieList({movies, toggleWatched}) {
return (
<div>
<h2>登録済の作品一覧</h2>
<ul>
{
movies.map((movie, index) => (
<li key={index}>
タイトル:{movie.title}<br />
ジャンル:{movie.genre}<br />
視聴:
視聴:
<button onClick={() => toggleWatched(index)}>
{movie.watched ? "済" : "未"}
</button><br />
レビュー:{movie.review || ""}
</li>
))
}
</ul>
</div>
)
}
export default MovieList
App.jsxには、関数を追加する必要があります。
そして、追加した関数をMovieListのpropsとして渡すよう修正します。
import React, {useState} from 'react'
import MovieForm from './components/MovieForm'
import MovieList from './components/MovieList'
function App() {
const [movies, setMovies] = useState([])
function addMovie(movie) {
setMovies([...movies, movie])
}
function toggleWatched(index) {
const updatedMovies = movies.map((movie, i) => {
if (i == index) {
return {...movie, watched: !movie.watched}
}
return movie
})
setMovies(updatedMovies)
}
return (
<div>
<h1>FlickTracker</h1>
<MovieForm addMovie={addMovie} />
<MovieList movies={movies} toggleWatched={toggleWatched}></MovieList>
</div>
)
}
export default App
これで、視聴状態の未済が更新できるようになりました。
表示と動作の確認をしておきましょう。
レビュー機能を実装する
この機能は少々難易度が高いので、コードにコメントを付け、補足説明を加えています。
仕様は以下の通りです。
- レビュー欄に「編集」ボタンを設ける
- 「変数」ボタンをクリックするとモーダルが開く
- モーダルにはレビューを編集するためのテキストエリアと「更新」ボタン、「キャンセル」ボタンがある
- レビューを入力して「更新」ボタンをクリックすると、モーダルが閉じて、元画面のレビュー欄が更新される
- 「キャンセル」ボタンをクリックすると、モーダルが閉じる
作成、編集するファイルは以下の通りです。
- ReviewModal.jsx(新規作成)
- MovieList.jsx(編集)
- App.jsx(編集)
ReviewModal.jsxの作成
モーダルのコンポーネントを新規に作成します。
「components」フォルダ内に新規ファイル「ReviewModal.jsx」を作成いただき、以下のとおりコーディングしてください。
import React from 'react'
function ReviewModal({show, onClose, onSubmit, review, setReview}) {
if (!show) {
return null
}
// フォームが送信されたときにレビューを更新する関数を呼び出す。
function handleSubmit(e) {
e.preventDefault()
onSubmit()
}
return (
<div className="modal">
<div className="modal-content">
<h2>レビュー</h2>
<form onSubmit={handleSubmit}>
<textarea
value={review}
onChange={(e) => setReview(e.target.value)}
placeholder="レビューを入力"
/>
<br />
<button type="submit">更新</button>
<button type="button" onClick={onClose}>キャンセル</button>
</form>
</div>
</div>
)
}
このコンポーネントは、以下のような考え方で作成します。
- 目的と機能を明確にする
- まず、
ReviewModalの目的を考えます。このモーダルは映画のレビューを編集するためのものです。そのため、表示状態の管理、ユーザーによるレビュー入力、および送信とキャンセルの操作が必要になります。
- まず、
- 必要な情報と操作(受け取る
props)を洗い出すshow: モーダルの表示状態を管理するために必要です。モーダルが表示されるかどうかを決定します。onClose: モーダルを閉じるための関数です。ユーザーがキャンセルボタンを押したときに必要です。onSubmit: フォームの送信時に呼び出される関数です。レビューの更新を行います。review: 現在のレビューの内容を保持する状態です。テキストエリアに表示されます。setReview: レビューの内容を更新する関数です。ユーザーがテキストエリアに入力したときに呼び出されます。
- コンポーネントの設計
- これらの情報をもとに、
ReviewModalコンポーネントを設計します。propsとして受け取るべき情報と関数を明確にし、それを使ってコンポーネント内部のロジックを構築します。
- これらの情報をもとに、
MovieList.jsxの編集
続いて、MovieList.jsxを編集します。
例によって太字黄色マーカーの箇所を追加、修正しますが、相当のボリュームです。
考え方としては、ReviewModalコンポーネントを作成した際に洗い出したpropsを中心に作成していきます。
「編集」ボタン押下時に呼び出される関数(updateReview)は、toggleWatchedと同様の考え方により、親コンポーネントのApp.jsx側で定義し、propsとして受け取ります。
import React, {useState} from 'react'
import ReviewModal from './ReviewModal'
function MovieList({movies, toggleWatched, updateReview}) {
const [showModal, setShowModal] = useState(false)
const [currentIndex, setCurrentIndex] = useState(null)
const [currentReview, setCurrentReview] = useState('')
// モーダルを開く関数。現在のインデックスとレビューを設定する。
function openModal(index, review) {
setCurrentIndex(index)
setCurrentReview(review)
setShowModal(true)
}
// モーダルを閉じる関数。
function closeModal() {
setShowModal(false)
}
// フォームが送信されたときにレビューを更新する関数。
function handleSubmit() {
updateReview(currentIndex, currentReview)
setShowModal(false)
}
return (
<div>
<h2>登録済の作品一覧</h2>
<ul>
{
movies.map((movie, index) => (
<li key={index}>
タイトル:{movie.title}<br />
ジャンル:{movie.genre}<br />
視聴:
<button onClick={() => toggleWatched(index)}>
{movie.watched ? "済" : "未"}
</button><br />
レビュー:{movie.review || ""}
<button onClick={() => openModal(index, movie.review)}>編集</button>
</li>
))
}
</ul>
<ReviewModal
show={showModal}
onClose={closeModal}
onSubmit={handleSubmit}
review={currentReview}
setReview={setCurrentReview}
/>
</div>
)
}
export default MovieList
App.jsxの編集
最後にApp.jsxを編集します。
太字黄色マーカーのコードを追加してください。
import React, {useState} from 'react'
import MovieForm from './components/MovieForm'
import MovieList from './components/MovieList'
function App() {
const [movies, setMovies] = useState([])
function addMovie(movie) {
setMovies([...movies, movie])
}
function toggleWatched(index) {
const updatedMovies = movies.map((movie, i) => {
if (i == index) {
return {...movies, watched: !movie.watched}
}
return movies
})
setMovies(updatedMovies)
}
function updateReview(index, review) {
const updatedMovies = movies.map((movie, i) => {
if (i === index) {
return { ...movie, review }
}
return movie
})
setMovies(updatedMovies)
}
return (
<div>
<h1>FlickTracker</h1>
<MovieForm addMovie={addMovie} />
<MovieList
movies={movies}
toggleWatched={toggleWatched}
updateReview={updateReview}>
</MovieList>
</div>
)
}
export default App
ここでいったん表示と動作の確認をしておきましょう。
検索機能を追加する
実装する機能の最後は、検索です。
仕様は以下の通りです。
- 画面上部に検索条件を入力・選択できる要素と「検索」ボタンがある
- 検索条件は次の3つ
- タイトル(部分一致)
- ジャンル(部分一致)
- 視聴状態(単一選択)
- 3つの条件をアンド検索する
SearchForm.jsxの作成
検索用のコンポーネントを新規に作成します。
「components」フォルダ内に新規ファイル「SearchForm.jsx」を作成いただき、以下のとおりコーディングしてください。
import React, { useState } from 'react';
function SearchForm({ updateSearchQuery }) {
const [title, setTitle] = useState('')
const [genre, setGenre] = useState('')
const [watched, setWatched] = useState('')
function handleSubmit(e) {
e.preventDefault();
updateSearchQuery({ title, genre, watched });
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="タイトルで検索"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<input
type="text"
placeholder="ジャンルで検索"
value={genre}
onChange={(e) => setGenre(e.target.value)}
/>
<select value={watched} onChange={(e) => setWatched(e.target.value)}>
<option value="">視聴状態</option>
<option value="未">未</option>
<option value="済">済</option>
</select>
<button type="submit">検索</button>
</form>
);
}
export default SearchForm
SearchFormはユーザーが検索条件を入力するためのフォームです。タイトル、ジャンル、視聴状態を入力・選択でき、それに基づいて映画リストを絞り込むことができます。
const [title, setTitle] = useState('')
const [genre, setGenre] = useState('')
const [watched, setWatched] = useState('')
上記は、検索用コンポーネントで使用する各stateの初期化です。
次の関数「handleSubmit」で、ユーザーが入力した値はそれぞれの状態に格納され、フォームが送信されるときにupdateSearchQuery関数が呼び出され、AppコンポーネントのsearchQueryが更新されるようにしています。
function handleSubmit(e) {
e.preventDefault();
updateSearchQuery({ title, genre, watched })
}
App.jsの編集
太字黄色マーカーのコードを追加、編集してください。
import React, {useState} from 'react'
import MovieForm from './components/MovieForm'
import MovieList from './components/MovieList'
import SearchForm from './components/SearchForm'
function App() {
const [movies, setMovies] = useState([])
const [searchQuery, setSearchQuery] = useState({ title: '', genre: '', watched: '' })
function addMovie(movie) {
setMovies([...movies, movie])
}
function toggleWatched(index) {
const updatedMovies = movies.map((movie, i) => {
if (i == index) {
return {...movie, watched: !movie.watched}
}
return movie
})
setMovies(updatedMovies)
}
function updateReview(index, review) {
const updatedMovies = movies.map((movie, i) => {
if (i === index) {
return { ...movie, review }
}
return movie
})
setMovies(updatedMovies)
}
function updateSearchQuery(query) {
setSearchQuery(query);
}
const filteredMovies = movies.filter(movie => {
return (
(searchQuery.title === '' || movie.title.includes(searchQuery.title)) &&
(searchQuery.genre === '' || movie.genre.includes(searchQuery.genre)) &&
(searchQuery.watched === '' || movie.watched === (searchQuery.watched === '済'))
)
})
return (
<div>
<h1>FlickTracker</h1>
<MovieForm addMovie={addMovie} />
<SearchForm updateSearchQuery={updateSearchQuery} />
<MovieList
movies={filteredMovies}
toggleWatched={toggleWatched}
updateReview={updateReview}>
</MovieList>
</div>
)
}
export default App
Appコンポーネントに新たに検索条件を格納するためのsearchQueryという状態を追加しました。この状態はタイトル、ジャンル、視聴状態を含むオブジェクトです。また、新たにSearchFormコンポーネントをインポートして表示しています。
App.jsxの変更点は以下の通りです。
const [searchQuery, setSearchQuery] = useState({ title: '', genre: '', watched: '' })
上記は、検索条件の初期化です。searchQueryはタイトル、ジャンル、視聴状態を保持しており、次の関数「updateSearchQuery」で更新されます。
function updateSearchQuery(query) {
setSearchQuery(query);
検索条件に応じたフィルタリングは、次の処理で行っています。
「&&」を使用してアンド検索しているため、3つの条件すべてに合致するものだけがフィルタリングされます。
const filteredMovies = movies.filter(movie => {
return (
(searchQuery.title === '' || movie.title.includes(searchQuery.title)) &&
(searchQuery.genre === '' || movie.genre.includes(searchQuery.genre)) &&
(searchQuery.watched === '' || movie.watched === (searchQuery.watched === '済'))
)
})
これですべての機能の実装が完了しました。
いよいよデザイン(CSS)の作成にとりかかる段階になりましたが、その前に表示と動作の確認をしておきましょう。
CSSの作成とアプリへの適用
CSSの作成
「src」フォルダ内にindex.cssというファイルがあります。
「チュートリアルの準備」の段階で、コードはすべて消去してあるはずなので、ここに以下のCSSをコーディングしてください。
※ 本記事はReact.jsのチュートリアルであるため、CSSはコピー&ペーストしてしまってもOKです。
/* App.css */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
h1, h2 {
text-align: center;
}
form {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
}
input, select, button {
margin: 5px 0;
padding: 10px;
font-size: 1em;
}
button {
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
ul {
list-style-type: none;
padding: 0;
}
li {
background-color: #f9f9f9;
margin: 10px;
padding: 20px;
border-radius: 5px;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 5px;
width: 80%;
max-width: 500px;
}
CSSのインポート
App.jsxにインポートを追加します。
太字黄色マーカーの箇所を追加してください。
import React, { useState } from 'react'
import './index.css' // ここでCSSファイルをインポート
import MovieForm from './components/MovieForm'
import MovieList from './components/MovieList'
import SearchForm from './components/SearchForm'
function App() {
// 以下省略...
まとめ
今回のチュートリアルでは、React.jsを使用して映画・ドラマの登録ウェブアプリを作成しました。
登録、視聴状態の切り替え、レビュー機能を作り、最後に検索機能を追加し、段階的にアプリをブラッシュアップすることで、実際の開発工程をイメージできる構成にしたつもりです。
この開発を通じて、React.jsの状態管理やコンポーネントの再利用などの基本的なスキルを学ぶことができました。
今後もさらに機能を追加して、ユーザーフレンドリーなアプリケーションを作成していきましょう。
これで今回のチュートリアルは終了です。
長い記事を最後までお読みいただきありがとうございました。
質問やご意見、ご感想は、いつでもお気軽にコメント頂ければ幸いです。

コメント