React HooksとDjango REST Frameworkを使ってTo Do Listを作ってみた(前編)〜フロントエンド編
最近気に入って使っているDashなんですが、React.jsというJavaScriptのライブラリをベースにしていると開発元のホームページに書いてあります。
また、Dashのグラフコンポーネントを新たに作るならReactを学ぶことが勧められています。
さらに、こちらのページによるとJavaScriptのフロントエンドフレームワークの中で最も世界的に人気があるようで、Googleトレンドでも同様の結果となっています。
これらにそそのかされて、この1ヶ月程、Reactを集中的に学んでみました(JavaScriptの再学習も併せて)。そこで、自分自身の理解を深めるために、ReactでTo Do Listアプリを作成してみましたので、紹介したいと思います。
なお、Djangoをバックエンドとして使いたいと思っていろいろなサイトを見てみましたが、Django REST Frameworkを使っているサイトが多いようなので、今回Django REST Frameworkを利用することにしました。
出来上がりはこんな感じ。
まずは、フロントエンドを、Reactを使って、To Do LIstアプリを作成(データベースとしてはjson-serverを利用)。その後、Django REST Frameworkを使ってバックエンドを作成し、一旦作成したReactと繋げていきます。
Reactのインストール(Mac)
1. node.jsをインストール
こちらのサイトからNode.js
のLTS版をインストールします。
nodejs.org
Node.js
はJavaScriptで作られているバックエンドのフレームワークです。今回はNode.jsをインストールすると、同時にインストールされるnpm(Node Package Manager: Node.jsのパッケージを管理するツール)を利用します。
2. 新たにReactアプリを作成
1により Node >= 8.10 及び npm >= 5.6 がインストールされているので、新たにReactプロジェクトを作成するには以下をターミナルから実行するだけです。mytodoのところは任意のアプリ名を入れます。
npx create-react-app mytodo cd mytodo npm start
ここでReactアプリが立ち上がるはずが、以下のような表示が出て先に進めなくなってしまうことが環境によっては良くあります。私の場合がそうです。
この場合は一旦ctr+c
にて中断して、ターミナルにて
unset HOST
を入力し、Enter
キーを押します(HOST情報の解除)。これで再度npm start
にてReactアプリ(初期画面)が立ち上がるようになります。
必要なパッケージのインストール
今回インストールするパッケージとその役割は以下の通り。
- json-server:Reactから利用できる簡易サーバー(モックアップサーバーというらしい)です。まずはフロントエンドで完結できるよう、このサーバーとjsonデータをやりとりできるようにします。
- axios:モックアップサーバーとの通信に利用する非同期通信ライブラリです。
- uuid:一意なIDを生成するライブラリで、各TaskのIDの作成に利用します。
インストールは、先ほどのmytodo直下で以下のコマンドを入力して行います。
npm install --save-dev json-server npm install axis npm install uuid
モックアップサーバーの設定
初期データの準備
mytodoフォルダ直下にdb.json
というJSONファイルを追加し、以下を記述します。
{ "tasks": [ { "id": "f543f73d-48d6-4b65-a07f-20cf68e11461", "title": "TO DO LISTをReactで作成する", "isCompleted": false }, { "id": "9e25ddb3-ac4a-4354-9320-d8211762d1fb", "title": "お米を買う", "isCompleted": false }, { "id": "6a18845a-c537-4263-8cc7-9e62b96d90ee", "title": "お酒を買う", "isCompleted": false } ] }
これがモックアップサーバー上のデータになります。idは取り敢えず何でも大丈夫です。新規で追加される場合には、uuidにより自動的にidが付加されます。
モックアップサーバーの起動準備
mytodoフォルダ直下にあるpackage.jsonの中にあるscriptsセクションに以下のとおり、json-serverに関する設定を追加します。
"scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", "server": "json-server --watch db.json --port 3001" // 追加 },
これにより、ターミナルからnpm run server
というコマンドを入力すれば、モックアップサーバーが立ち上がります。なお、npm start
で立ち上がるReactアプリのポートが3000のため、モックアップサーバーのポートを--port 3001
と3001に設定しています。--watch db.json
にてjson-serverが常にdb.json
の変更を監視できるように設定しています。
モックアップサーバーの起動確認
ターミナルにてnpm run server
と入力するとモックアップサーバーが立ち上がります。ここで、任意のブラウザにてhttp://localhost:3001/tasks
にアクセスすると画面にdb.jsonに登録したデータが表示されます。
To Do Listアプリの作成
こちらのサイトなどを参考にしながらTo Do Listアプリを作成しました。
ファイル構成
ファイル構成は以下の通りです。npx create-react-app
でインストールされるファイルで不要なものは削除しています。
public/ index.html style.css src/ index.js App.js api/ TaskApi.js components/ Task.js TaskForm.js TaskList.js TaskProvider.js node_modules/ db.json package.json package-lock.json
ファイルの内容
このアプリでは、React Hooksのうち、基本フックであるuseState
, useContext
及びuseEffect
を利用しています。
useState
は基本のHookになります。Reactのオフィシャルページによると以下のとおり。
useState は現在の state の値と、それを更新するための関数とをペアにして返します。この関数はイベントハンドラやその他の場所から呼び出すことができます。
要は、コンポーネントの中でデータを保持し、関数を使ってそのデータを加工できる仕組みをいいます。
また、useContext
は、オフィシャルページでは以下のとおり。
コンテクストオブジェクト(React.createContext からの戻り値)を受け取り、そのコンテクストの現在値を返します。コンテクストの現在値は、ツリー内でこのフックを呼んだコンポーネントの直近にある <MyContext.Provider> の value の値によって決定されます。
何を言っているか良くわかりませんねぇ。
いろいろ調べてみると、このフックを利用すれば、親コンポーネントから子コンポーネントにデータを渡す際に使うPropsを利用しなくても良くなります。加えて「親から子にPropsを渡し、さらにその子供にPropsを渡し、さらにその子供に...」といったPropsのバケツリレーを行う必要がなくなり、どのコンポーネントからでもデータにアクセスできるようにするためのフックです。
useEffect
は、オフィシャルページは分かりにくいので、以下のとおり整理しました。
コンポーネントに副作用のある命令型コード(side effect)を追加するフックをいう。 副作用のある命令型コード(side effect)とは次のような処理を実行する関数のこと。これらは関数コンポーネント本体で書くことはできない(バグや非整合性の原因)。
- DOMの変更
- APIとの通信
- 非同期処理
- タイマー
- console.log(ロギング)
代わりに、useEffect
を利用する。useEffect
に渡された関数は、レンダーの結果が画面に反映された後に動作する(デフォルト)。特定の値が変化した時のみ動作させることも可能。
最初にuseState
,useContext
, useEffect
の全てを利用している本アプリの肝であるTaskProvider.js
について説明します。
TaskProvider.js
// 1. 各ライブラリから必要なモジュールのインポート import React, { useState, useEffect, useContext, createContext } from 'react'; import { v4 } from 'uuid'; import TaskApi from '../api/TaskApi'; // 2. Contextオブジェクトとカスタムフック const TaskContext = createContext(); const useTasks = () => useContext(TaskContext); function TaskProvider({ children }) { // 3. useStateの設定 const [tasks, setTasks] = useState([]); // 4. 当初レンダリング時のデータ取得 useEffect(() => { TaskApi.getAll().then(tasks => { setTasks(tasks); }); }, []) // 5. タスクの追加 const addTask = item => { const newTask = { id: v4(), title: item, isCompleted: false } TaskApi.add(newTask).then(addedTask => ( setTasks([...tasks, addedTask]) )) } // 6. データステータスの変更 const toggleStatus = (id, status) => { const item = tasks.find(task => task.id === id) const updatedTask = {...item, isCompleted: status} TaskApi.update(id, updatedTask).then(newTask => { const newTasks = tasks.map(item => item.id === newTask.id ? newTask : item); setTasks(newTasks); }) } // 7. データの削除 const deleteTask = id => { TaskApi.delete(id).then(deletedId => { const newTasks = tasks.filter(item => item.id !== deletedId) setTasks(newTasks); }) } // 8. リターン値 return ( <TaskContext.Provider value={{ tasks, addTask, toggleStatus, deleteTask }}> { children } </TaskContext.Provider> ) } export default TaskProvider; export { useTasks }
1.各ライブラリから必要なモジュール(関数、オブジェクト等)のインポート
まず最初にReact及び2つのHooks(useStateとuseContext)をインポートします。createContext
はuseContext
を利用するために必要ですので、併せてインポートしておきます。また、新たにTaskを追加した際に、自動的に一意のidを追加するため、uuidライブラリからv4モジュールをインポートします。
2.Contextオブジェクトとカスタムフック
createContext
にてContextオブジェクトを生成し、TaskContext
とします。このTaskContext
を呼び出す(useContext
)を関数の形でカスタムフックuseTasks
として定義します。これにより、どのコンポーネントにおいても、tasksアレイや必要な関数(以降で定義)にアクセスできるようになります。
3.useStateの設定
useState Hookにより、taskアレイを初期化し、デフォルト値として空のアレイを設定します。このuseStateは2つのオブジェクトのアレイを返します。1つは状態を表す値(taskアレイ)とその値を変更するのに使う関数(setTasks)です。
当初レンダリング時に、モックアップサーバーからAPIによりデータを取得します。APIとの通信は副作用(side-effect)に該当しますので、useEffect
を利用する必要があります。第二引数に[]と指定していますので、当初レンダリング時にのみデータを取得します。
データを取得後、setTasks(tasks)
にて、tasksアレイに取得データを設定しています。
5.タスクの追加
ここでaddTask
関数を作成しており、他のコンポーネントでもタスクを追加することができます。タスクのタイトルを入力すると、v4
により自動的にユニークなid
と、完了フラグisCompleted
がfalse
に設定されます(newTask)。APIにより、json-serverにnewTaskとして登録した後、setTasks
により、...task
(スプレッド構文)で展開される既存taskアレイに、newTaskを追加してtasksに登録します。
6.データステータスの変更
ここでは、toggleStatus
関数を作成しています。チェックボックス のオンオフをstatus
として与えた時に、その対象となっているtaskのisComplete
の値(true/false)を変更するようにしています。json-severのデータを書き換えた後に、setTasks
によりtaskアレイも変更します。
7.データの削除
ここで、deleteTask
関数を作成しています。対象タスクのid
を与え、json-serverのデータを削除し、また、taskアレイも変更しています。
8.リターン値
2にて定義したTaskContext
を<TaskContext.Provider value={{}}>{children}</TaskContext.Provider>
の形でchildren
(子コンポーネント)を挟んでリターン値として返すことで、コンポーネント間でこのvalueを共有できます。
TaskApi.js
次はTaskApi.js
です。これはaxios
ライブラリを利用してjson-serverとデータのやりとりを行うスクリプトファイルです。
GitHub - axios/axios: Promise based HTTP client for the browser and node.js
axios
はエラー発生時のハンドリングのロジック等を追加できますが、ここでは、そうしたロジックを記述せずに単純にaxios.get
でデータの取得、axios.post
でデータの追加、axios.put
でデータの修正、axios.delete
でデータの削除を行っています。
import axios from 'axios'; const baseUrl = "http://localhost:3001/tasks"; async function getAll () { const response = await axios.get(baseUrl); return response.data; } async function add (newTask) { const response = await axios.post(baseUrl, newTask) return response.data; } async function update(id, updatedTask) { const response = await axios.put(`${baseUrl}/${id}`, updatedTask) return response.data; } async function _delete(id) { await axios.delete(`${baseUrl}/${id}`); return id; } export default { getAll, add, update, delete: _delete }
以降は、上位のファイルの内容から順に記載していきます。
index.js
このファイルは、index.htmlの<div id="root"></div>
タグに紐つけ、<App />
(Appコンポーネント)を表示させる役割を持っています。
<React.StrictMode>
は、公式HP↓にも記載されていますが、strictモードの設定で、開発環境のみ有効な機能です。例えば、React Hooksを安全でない使い方をした場合に、警告メッセージが出力されるようになります。
<TaskProvider>
は最初に説明したコンポーネントです。この間に挟まれた<App />
以降のコンポーネント間でTaskProvider.js
において設定されているvalue
であるtasks
アレイ, addTask
・toggleTask
・deleteTask
間をuseTasks
を通じて共有できるようにしています。
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import TaskProvider from './components/TaskProvider'; ReactDOM.render( <React.StrictMode> <TaskProvider> <App /> </TaskProvider> </React.StrictMode>, document.getElementById('root') );
App.js
このファイルは、タイトル(h1タグ)と2つのコンポーネント<TaskForm />
及び`
import React from 'react'; import TaskForm from './components/TaskForm'; import TaskList from './components/TaskList'; function App() { return ( <div className="container"> <h1 className="title">To Do List</h1> <TaskForm /> <TaskList /> </div> ); } export default App;
TaskList.js
ここでは、useTasks'によりtasksアレイのみ取り出して、
map`メソッドによりTaskコンポーネントを展開しています。
import React from 'react'; import Task from './Task'; import { useTasks } from './TaskProvider'; function TaskList() { const { tasks } = useTasks(); return ( <table> <tbody> { tasks.map((task, id) => <Task key={id} {...task} /> ) } </tbody> </table> ) } export default TaskList;
Task.js
Taskコンポーネントを定義しています。カスタムフックuseTasks
を通じて、toggleStatus
関数とdeleteTask
関数を取り出しています。それぞれチェックボックス のオンオフ、x
ボタンのクリックに対応させています。また、チェックボックス をオンにすると、title
に取消線(line-through)が表示されるようにしています。
import React from 'react'; import { useTasks } from './TaskProvider'; function Task({ id, title, isCompleted }) { const { toggleStatus, deleteTask } = useTasks(); const checkTask = event => toggleStatus(id, event.target.checked); return ( <tr> <td> <input type="checkbox" onChange={checkTask} checked={isCompleted} /> </td> <td className="align-left" style={{width: "100%"}}> <span style={{textDecoration: isCompleted && "line-through"}}> {title} </span> </td> <td> <button onClick={() => deleteTask(id)}>x</button> </td> </tr> ) } export default Task;
TaskForm.js
フォーム入力のためのコンポーネントです。useTasks
を通じてaddTask
関数を利用できるようにしており、+
ボタンを押すとその時点で入力していたタスクが追加されます。
import React, { useState } from 'react'; import { useTasks } from './TaskProvider'; function TaskForm() { const [task, setTask] = useState('') const { addTask } = useTasks(); const submit = event => { event.preventDefault(); addTask(task); setTask(''); } return ( <form onSubmit={submit}> <input type="text" value={task} placeholder="" onChange={event => setTask(event.target.value)} required /> <button>+</button> </form> ) } export default TaskForm
説明は以上です。
今回設定したcss
は以下の通りです。
/* style.css */ body { background-color: azure; min-height: 70vh; padding: 1rem; box-sizing: border-box; display: flex; justify-content: center; align-items: center; color:rgb(99, 99, 99); text-align: center; font-size: 100%; } .container { width: 100%; height: auto; min-height: 500px; max-width: 500px; min-width: 350px; background: #f1f5f8; background-image: radial-gradient(#bfc0c1 7.2%, transparent 0); background-size: 25px 25px; box-shadow: 4px 3px 7px 2px #00000040; padding: 1rem; box-sizing: border-box; } form input { box-sizing: border-box; background-color: transparent; padding: 0.5rem; font-size: 1rem; border: 1px solid rgb(185, 185, 185); box-shadow: 1px 1px 2px rgb(185, 185, 185) inset; width: 80%; margin-bottom: 20px; } form button { color: rgb(245, 245, 245); font-size: 1.2rem; width: 40px; height: 32px; border: none; background-color: rgb(0, 162, 255); border-radius: 5%; margin-left: 10px; box-shadow: 2px 2px 4px rgb(185, 185, 185); } table { border-spacing: 0; } table td { border-bottom: solid 1px rgb(185, 185, 185); background-color: white; padding: 10px; } td button { color: rgb(104, 104, 104); border: none; background-color: rgb(224, 224, 224); border-radius: 100%; } .title { font-family: 'Open Sans', sans-serif; } .align-left { text-align: left; }
動かしてみましょう
まず、ターミナルからnpm run server
にてjson-serverを稼働させます。
> mytodo2@0.1.0 server /Users/tak/Desktop/react/mytodo2 > json-server --watch db.json --port 3001 \{^_^}/ hi! Loading db.json Done Resources http://localhost:3001/tasks Home http://localhost:3001 Type s + enter at any time to create a snapshot of the database Watching...
うまく立ち上がりました。
それでは別のターミナルからnpm start
にてReactアプリを立ち上げます。
うまくいきました。
次回は、django REST frameworkを利用してバックエンドを作成し、今回作成したReactアプリとつなげたいと思います。