akatak’s blog

プログラム初心者の50代ビジネスマンがセカンドキャリアを目指して働きながらPythonを中心に独学しています。自らの覚え書きや成果物、感じたことなどを脈絡もなく書き連ねるブログです。

React HooksとDjango REST Frameworkを使ってTo Do Listを作ってみた(前編)〜フロントエンド編

最近気に入って使っているDashなんですが、React.jsというJavaScriptのライブラリをベースにしていると開発元のホームページに書いてあります。

また、Dashのグラフコンポーネントを新たに作るならReactを学ぶことが勧められています。

さらに、こちらのページによるとJavaScriptのフロントエンドフレームワークの中で最も世界的に人気があるようで、Googleトレンドでも同様の結果となっています。

2019.stateofjs.com

これらにそそのかされて、この1ヶ月程、Reactを集中的に学んでみました(JavaScriptの再学習も併せて)。そこで、自分自身の理解を深めるために、ReactでTo Do Listアプリを作成してみましたので、紹介したいと思います。

なお、Djangoをバックエンドとして使いたいと思っていろいろなサイトを見てみましたが、Django REST Frameworkを使っているサイトが多いようなので、今回Django REST Frameworkを利用することにしました。

出来上がりはこんな感じ。


React-to-do-sample

まずは、フロントエンドを、Reactを使って、To Do LIstアプリを作成(データベースとしてはjson-serverを利用)。その後、Django REST Frameworkを使ってバックエンドを作成し、一旦作成したReactと繋げていきます。

Reactのインストール(Mac

1. node.jsをインストール

  こちらのサイトからNode.jsのLTS版をインストールします。 nodejs.org

Node.jsJavaScriptで作られているバックエンドのフレームワークです。今回は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アプリが立ち上がるはずが、以下のような表示が出て先に進めなくなってしまうことが環境によっては良くあります。私の場合がそうです。

f:id:akatak:20200830143410p:plain

この場合は一旦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アプリを作成しました。

dev.to

ファイル構成

ファイル構成は以下の通りです。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)をインポートします。createContextuseContextを利用するために必要ですので、併せてインポートしておきます。また、新たにTaskを追加した際に、自動的に一意のidを追加するため、uuidライブラリからv4モジュールをインポートします。

2.Contextオブジェクトとカスタムフック

createContextにてContextオブジェクトを生成し、TaskContextとします。このTaskContextを呼び出す(useContext)を関数の形でカスタムフックuseTasksとして定義します。これにより、どのコンポーネントにおいても、tasksアレイや必要な関数(以降で定義)にアクセスできるようになります。

3.useStateの設定

useState Hookにより、taskアレイを初期化し、デフォルト値として空のアレイを設定します。このuseStateは2つのオブジェクトのアレイを返します。1つは状態を表す値(taskアレイ)とその値を変更するのに使う関数(setTasks)です。

4.当初レンダリング時のAPIデータ取得

当初レンダリング時に、モックアップサーバーからAPIによりデータを取得します。APIとの通信は副作用(side-effect)に該当しますので、useEffectを利用する必要があります。第二引数に[]と指定していますので、当初レンダリング時にのみデータを取得します。

データを取得後、setTasks(tasks)にて、tasksアレイに取得データを設定しています。

5.タスクの追加

ここでaddTask関数を作成しており、他のコンポーネントでもタスクを追加することができます。タスクのタイトルを入力すると、v4により自動的にユニークなidと、完了フラグisCompletedfalseに設定されます(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アレイ,addTasktoggleTaskdeleteTask間を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アプリを立ち上げます。

f:id:akatak:20200831101350p:plain

うまくいきました。

次回は、django REST frameworkを利用してバックエンドを作成し、今回作成したReactアプリとつなげたいと思います。