Help us understand the problem. What is going on with this article?

【初心者向け】非同期にCRUDするReactアプリケーションを作る

More than 1 year has passed since last update.

はじめに

Reactを触り始めたプログラミング初心者にとって、非同期にデータを扱うアプリケーションを作るのはなかなか難しいことです。
なぜなら、シンプルにReactでのサーバーとの非同期処理に焦点を合わせた情報が少ないからです。かわりに、WebpackとかExpressとかmongoDBとかFluxとかReduxとか、初心者が戦いを挑むにはちょっと厳しいモンスターたちが絡んだ記事が出てきます。

この記事では、使用するモジュールをcreate-react-appjson-serverの2つに絞ることで、ややこしいことに煩わされずReact+非同期通信(処理)の面白さを味わえるようになっています。

作るもの

goal.gif

今回作るのは泣く子も黙る千番煎じ、タスク管理アプリケーションです!(ToDoアプリと呼ぶと千どころか万番煎じになってしまうので、せめてタスクと呼ぶことにしました)

これはタスクの取得、追加、更新、削除ができるアプリケーションです。
データの取得、追加、更新、削除は基本の操作であり、英語での頭文字(Create/Read/Update/Delete)を取ってCRUDと呼びます。

CRUDができれば、ほとんどの場合これを組み合わせてアプリケーションを作ることができます。

対象読者

「Reactは少しだけ触ったけど、非同期通信がよくわからない」
「json-serverの基本的な使い方を知りたい」
「JavaScriptをマスターしたい!」
「とりあえず何か作ってみたい!」

というプログラミング初心者です。

この記事でやることはまあまあ高度なので、いちから説明していてはキリがありません。
そのため、Reactの基本やHTTPのような各所で前提となる知識については詳しくは説明していません。

かわりに、なるべく参考記事へのリンクやキーワード(太字で示しています)を載せました。
まったくわからない人向けの記事も、より深く知りたい人向けの記事もごちゃごちゃに載せてしまったので、いい感じに取捨選択して参照してください。

では、さっそく作っていきましょう!

create-react-app

create-react-appの導入&基本はこちらの記事が参考になります。
Facebook公式のcreate-react-appコマンドを使ってReact.jsアプリを爆速で作成する

インストールしたらreactアプリケーションを適当な場所に作成&作成したディレクトリに移動しましょう。

$ create-react-app crud-sample
$ cd crud-sample

一応、起動してみます。

$ npm start

ブラウザが立ち上がってReactのロゴがくるくる回るページが表示されたらOKです。

このときコマンドラインツール上では以下のように表示されているはずです。これが表示されている間は、ブラウザからhttp://localhost:3000にアクセスすることで自分のReactアプリケーションを触ることができます。

Compiled successfully!

省略

【注意!】
このコマンドラインツールのウィンドウは開きっぱなしにして、別のウィンドウでcrud-sampleのディレクトリを開いてください。ターミナルであれば、command + tで開くことができます。
以降のコマンドはそこに入力していくことにします。

なぜかReactをほとんど知らないのにこのページに辿り着いた人は、まずはReactがどんなものか掴んでおくと良いでしょう。
Reactは公式のチュートリアル(Tutorial: Intro to React)が充実していて勉強になります。
どっかのすごい人が日本語訳してくれたもの(Reactチュートリアル: Intro To React【日本語翻訳】)もあるので、ありがたく使わせてもらいましょう。

json-server

続いて、json-serverを用いて簡単にRESTfulなデータベースサーバーを作ってみましょう。

RESTfulというのはまた概念の話なので、とりあえずわからなくても大丈夫です。

公式リファレンス(typicode/json-server)が充実しているので、必要に応じて目を通しておくとよいでしょう。

インストール

まずは以下のコマンドで、プロジェクト内でjson-serverが使えるようにします。

$ npm install json-server

データベースをつくる

json-serverでは、jsonファイルをデータの保管場所として使います。
JSONってなんだ?という人はこちら(JSONとは)。

まずはjsonファイルを作りましょう。

$ touch db.json

次に、db.jsonの中身を書き換えます。
データベースを直接いじるイメージです。

今回はタスクを管理するアプリケーションを作るので、db.jsonにtasksキーを作り、それに対応する値として、個別のタスクのデータが入った配列をセットします。

db.json
{
  "tasks": [
    {
      "id": 1,
      "body": "進捗を生む"
    }, {
      "id": 2,
      "body": "とにかくやる"
    }
  ]
}

これからアプリケーションを通じてタスク配列に入っているデータを追加・更新・削除しますが、それらの操作は具体的にはこのdb.jsonを適切に書き換えることに相当します。

自分でjsonファイルを書き換えるのは面倒ですが、json-serverに適切にお願いすれば、いい感じにファイルを書き換えてくれるのです。

データベースについて

json-serverは、JSON形式のデータをそのまま保存することができます。
しかしこれは当たり前のことではありません。

RDB(リレーショナルデータベース)と呼ばれるデータベースは、エクセルやスプレッドシートのようにたくさんの表でデータを保存します。
RDBでデータの入出力をするSQLという言語の学習コストが高い&JSON形式で入出力できるのはJavaScriptと相性が良いので、今回はjson-serverを使うことにしました。

json-serverなどのRDB以外のデータベースはSQLを使わないので、NoSQLと呼ばれます。
NoSQLのデータベースとしては、よく使われるものではMongoDBが挙げられます。

サーバー立ち上げ

先ほどjson-serverをインストールしたので、json-serverというコマンドが使えるようになっています。

これはjson-serverを立ち上げて、データベースを操作できるようにするコマンドです。

json-serverを立ち上げるコマンドは開発時には頻繁に使うので、package.jsonでサーバーを立ち上げるコマンドに名前をつけておきましょう。
なんでもいいのですが、名前そのまま、json-serverとしましょうか。

package.json
  "scripts": {
    省略
    "json-server": "json-server --port 3001 --watch db.json"
  }

json-server --port 3001 --watch db.jsonというコマンドに、json-serverという別名をつけました。

package.json

package.jsonは、create-react-appによって作られたファイルです。
この中にはタスク管理アプリケーションに関する様々な情報が書かれています。
package.jsonの中身を理解する
npm package.json 取扱説明書

package.jsonのscriptsのところには、アプリケーションで使うコマンドに別名をつけることができます。

上でコマンドを書き加えたことにより、ターミナルやコマンドプロンプトで以下の別名コマンドが使えるようになりました。

$ npm run json-server

これを実行するとかわいい顔文字とともにサーバーが立ち上がります。

> crud@0.1.0 json-server /Users/HIKARU/Documents/crud-sample/crud-sample
> json-server --port 3001 --watch db.json


  \{^_^}/ 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...

そもそもサーバーって何?という人はググるか本を読んでください。

package.jsonは、より正確には、Node.js(JavaScriptの実行環境)で動くパッケージ(誰かが作ったコードのまとまり、これを管理するのがnpm)の設定ファイルみたいなものです。
create-react-appやjson-serverもパッケージのひとつです。

Node.jsを取り巻くいろいろについては以下の記事が参考になります。全くの初心者向けの記事ではないので、わからなくても問題ありません。
Node.js とは何か?
npm とは何か / Package と module の違い

とりあえず設定ファイルなんだなとわかっておけばOKです。

ポート番号

Reactのファイルを送信するアプリケーションサーバーが3000番のポートを使っているので、データベースサーバーはとりあえず適当に3001番のポートを使うことにします。ポートは--portで指定します。

ポートって何?という人は、ポート番号でググってください。
1つのポート番号には同時には1つのアプリケーションしか対応できないから、create-react-appとjson-serverという2つのアプリケーションは互いに異なるポート番号で起動しないといけないんだな、くらいの理解で大丈夫です。

データ取得

コマンドを実行して表示されたものを見てみます。
顔文字、ではなくResourcesのところを見ると、http://localhost:3001/tasksと書いています。
Resourcesってなに?っていう人はこの記事(リソース指向アーキテクチャ(ROA)とは何なのか)。概念的な話なので、とりあえずわからなくても問題ありません。

とにかく、ここに書かれたURLにアクセスすると、タスクの一覧がjson形式で取得できます。

スクリーンショット 2019-01-28 22.13.30.png

まず大事なのは使い方を知ることです。

これでDBに保存した値を手に入れることができました!

タスクを取得する

続いて、Reactアプリケーションの中からデータベースにリクエストを飛ばし、データを取得します。

タスクを表示する

App.jsには余計な情報がたくさんあるので、ごっそり書き換えて、タスクを表示するだけのページにしてしまいましょう。

src/App.js
import React, { Component } from 'react';
import './App.css';

class App extends Component {
  constructor() {
    super()
    this.state = {
      tasks: [
        {
          id: 1,
          body: "とりあえず表示してみる"
        },
        {
          id: 2,
          body: "私たち、いずれ書き換えられる運命"
        }
      ]
    }
  }

  render() {
    return (
      <div className="App">
        <div className="tasks">
        {
          this.state.tasks.map( task => {
              return <div className="task" key={ task.id }>{ task.body }</div>
          })
        }
        </div>
      </div>
    );
  }
}

export default App;

スクリーンショット 2019-01-31 1.01.19.png

ここではまだデータベースからタスクを取得する処理は書いていません。
ただstateに入っているタスクのデータを表示しただけです。

タスクを取得する

いよいよデータベースからタスクを取得してみましょう。

今回はページが読み込まれてAppコンポーネントがマウントされる直前に通信を開始し、データを受け取り次第setStateしてデータが切り替わるようにしてみます。

データを受け取ってsetStateする処理はfetchTasksとして切り出しておきます。
あとで別のところでも呼び出すからです。

src/App.js
class App extends Component {
  constructor() {
    // 省略
  }

  componentWillMount(){
    this.fetchTasks()
  }

  fetchTasks(){
    fetch("http://localhost:3001/tasks") // データを取得しに行く
    .then( response => response.json() ) // json型のレスポンスをオブジェクトに変換する
    .then( json => { // オブジェクトに変換したレスポンスを受け取り、
      this.setState({ tasks: json }) // Stateを更新する
    })
  }

  render() {
    // 省略
  }
}

このfetchTaskscomponentWillMountの中で実行することで、コンポーネントがマウントされる直前(Appがページに表示される直前)にタスクのデータを取りに行くことができます。

componentWillMountのような、コンポーネントの状態に応じて実行されるメソッドをライフサイクルメソッドといいます。
React component ライフサイクル図

fetchとthen

fetchTasksのところで、fetchとかthenとか書いています。これについてはざっくりとだけ説明しておきます。

fetchは、非同期通信を実際に行うメソッドです。
thenは、非同期通信で結果が返ってきたあとに、それを使って自分の好きな処理を行わせることができるメソッドです。

とりあえずは、2つめのthenまでほぼ定型文だと考えておけばよいです。
2つめのthenの中で、受け取ったデータをjsonに変換したものを引数に取る関数を書くことができます。
これを使ってstateを更新しているというわけです。

fetchPromiseという仕様を使って実現されていますが、Promiseをしっかり理解するのは骨が折れると思うので、とりあえずコピペででもなんとなく使い方がわかるようになっておきましょう。

Promiseに関するサイトとしては、JavaScript Promiseの本があります。ものすごく詳しく丁寧に説明してくれています。

タスクデータの取得を確認する

では、確認してみましょう。

…と思ってブラウザで開きっぱなしのページを見てみると、create-react-appのおかげで自動的にページが更新され、すでにデータベースの情報でタスクが上書きされているはずです。

json-server.gif

ページを更新すると、動画のように一瞬デフォルトのタスクが表示されたあと、データベースのタスクで上書きされます。

このタイムラグこそ非同期通信の証拠です!

すなわち、ページが表示されたあとで、こっそり通信が行われ、一部だけが書き換わったのです。

Reactのおかげで、「データを取得したら要素をここに追加して…」なんていちいち考えずに、手軽に非同期通信によるデータの更新とその表示が行えている、ということも忘れてはなりません。

ともかく、これでデータの取得ができました。

タスクを作成する

データの取得だけでは、アプリケーションとしてできることの幅はものすごく狭いです。
今回のようにタスク管理アプリケーションとして使うつもりなら、db.jsonを直接触らずともブラウザ上からタスクを登録できるようにしたいものです。

タスクの作成は、以下の3つの仕事に分けられるでしょう。

  1. ユーザーが入力したタスクの文字列を取得し、
  2. json-serverに送信してdb.jsonに追加し、
  3. 追加したタスクを含むタスク一覧をユーザーに表示する

順番にやっていきましょう。

入力フォームからデータを取得する

まず、適当に入力フォームを作ります。

スクリーンショット 2019-01-29 20.05.49.png

src/App.js
class App extends Component {
  constructor() {
    // 省略

    this.changeText = this.changeText.bind(this)
    this.submitTask = this.submitTask.bind(this)
  }

  // 省略

  changeText(e) {
    const inputText = e.target.value
    this.setState({ inputText: inputText })
    console.dir(inputText);
  }

  submitTask() {
    console.log( this.state.inputText );
  }

  render() {
    return (
      <div className="App">
        <div className="tasks">
        {
          this.state.tasks.map( task => {
              return <div className="task" key={ task.id }>{ task.body }</div>
          })
        }
        </div>
        <div id="task-form">
          <input type="text" id="task-input" onChange={ this.changeText }/>
          <button id="submit" onClick={ this.submitTask }>submit</button>
        </div>
      </div>
    );
  }
}

フォームに入力.gif

入力したりsubmitするたびにコンソールに文字が表示されるようになりました。
これで「入力した値を使ってなんかやる」は達成です。

コードの方は、いくつか一気に書いたので、ややわかりにくいかもしれません。

Reactとフォーム

input要素のonChange属性には、入力フォームの値が変化するたびに実行する関数を、button要素のonClick属性には、ボタンがクリックされたときに実行する関数を書いておきます。

Reactにおけるこのようなフォームの書き方がわからない人はControlled Componentについて調べてみてください。
Forms - React

ちょっと情報が古くて、いまのReactの書き方と異なる点もあるのですが、以下の記事が参考になりそうです。
React.jsでFormを扱う

thisとbind

他にも、constructorの中で〇〇.bind(this)というコードが追記されています。
ややこしいので、ざっくり説明します。

onClick属性などに指定されたハンドラとしてクラス内に定義した関数を実行するとき、その関数の中のthisはクラスのインスタンス自身ではなくundefinedになってしまいます。
詳しいことは説明しませんが、thisは実行される状況によって指し示すものがコロコロ変わる特殊な変数なのです。

このthisの挙動は、今回のように望ましくない場合があります。
今回は、関数の中のthisがいついかなる時もインスタンス自身を指し示すように固定(バインド)したい!ということで、それをやってくれるのがbindメソッドです。

よくわからない場合は、とりあえずそういうもんと思って書いてください。
thisのバインドについては、関数定義にアロー関数を使うとか色々な方法があるので、気になったら調べてみてください。この記事でも更新と削除のところで一例紹介します。

データを送信する & 画面を更新

タスクの追加.gif

先ほど入手した値を送信してみましょう。
submitTaskの中身を書き換えます。

src/App.js
class App extends Component {
  constructor() {
    // 省略

    this.fetchTasks = this.fetchTasks.bind(this)
  }

  // 省略

  submitTask() {
    fetch("http://localhost:3001/tasks", {
      method: "POST",
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ body: this.state.inputText })
    })
    .then( this.fetchTasks )
  }

  // 省略
}

fetchのあたりがごちゃごちゃしていますね。
HTTPとかのWebの基本がわからない人は、以下の2点を区別して考えると、ちょっとだけ頭に入りやすいかなと思います。

①HTTPという通信の取り決めでは、通信先のURI(URL)とともに、HTTPメソッドやヘッダーや一緒に送信するデータの内容(ボディ)を送る必要がある。
②fetchというメソッドでは、そんなごちゃごちゃしたものを2番目の引数に入れることになっている。

①については、HTTPリクエスト/レスポンスの構成要素を初心者にも分かるように解説してみたなど、HTTPについてよく調べてください。
最も重要なのは、送信先のURIとHTTPメソッドです。リソース指向アーキテクチャにおいては、これらは「何を(URI)、どうする(HTTPメソッド)」という、HTTPリクエストの内容を端的に表しています。

②については、ちょっと高度ですがFetch 概説などが参考になるでしょう。

今回は、データの送信が完了したら、もう一度全タスクデータを取ってくることにしています(this.fetchTasks())。

ここで、thenメソッドの中ではまたthisがAppコンポーネント自身を指してくれなくなってしまうので、constructorfetchTasks内で使われるthisが常にAppコンポーネントを指すよう固定しています。

ちょっと何言ってるかわからないかもしれませんが、thisははじめは誰しも訳わかんねえと思うものです。
そういうときは、「いつか分かる」と気楽に構えましょう。

ところで、POSTのレスポンスがそのまま全タスクのデータになっていてくれたら二回もfetchせずに済むんですけど…そんなことできるのでしょうか…?(Is it possible to 'get' data using POST with json-server?

ともかく、これにより自分が追加したデータを含むタスクデータを表示し、無事タスクの追加ができるようになりました。

タスクを更新する

まず、更新ボタンを作成します。
二度手間になるので削除ボタンも今つけておきましょう。

src/App.js
class App extends Component {
  // 省略

  render() {
    return (
      <div className="App">
        <div className="tasks">
        {
          this.state.tasks.map( task => {
              return (
                <div className="task" key={ task.id }>
                  { task.body }
                  <button className="put" onClick={ ()=>{ this.putTask(task.id) } }>put</button>
                  <button className="delete" onClick={ ()=>{ this.deleteTask(task.id) } }>delete</button>
                </div>
              )
          })
        }
        </div>
        <div id="task-form">
          <input type="text" onChange={ this.changeText }/>
          <button onClick={ this.submitTask }>submit</button>
        </div>
      </div>
    );
  }

  // 省略
}

ハンドラのところで、以下のようにアロー関数を使っています。

onClick={ ()=>{this.putTask(task.id)} }

その理由は、一つには、putTaskにはタスクのIDを引数に渡さなければならないからです。
もう一つは、先ほどちらっと話しましたが、putTaskの中のthisをAppコンポーネント自身にバインドするためです。
【JavaScript】アロー関数式を学ぶついでにthisも復習する話を読むとアロー関数とthisについてイメージが湧きやすいかもしれません。

入力フォームのところでonChange={ this.changeText }と書いていたところをonChange={ ()=>{ this.changeText() } }と書けば、constructorの中でバインドする必要はありません。

ではputTaskを書きます。
最終的にこういうのができるイメージです。

put_and_delete.gif

(予測変換の「消えた年金」とは一体…?)

src/App.js
class App extends Component {
  // 省略

  putTask(taskId) {
    fetch("http://localhost:3001/tasks/"+taskId, {
      method: "PUT",
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ body: "やったよ" })
    })
    .then( this.fetchTasks )
  }

  // 省略
}

fetchする先のURIがちょっとかわりました。更新や削除という特定のタスクに対するリクエストは、タスクのIDをつけたURIにする必要があります。

ほかはほとんどPOSTの場合と同じです。
HTTPメソッドがPUTになり、タスクの中身を「やったよ」で上書きするようにしています。

タスクを削除する

src/App.js
  deleteTask(taskId) {
    fetch("http://localhost:3001/tasks/"+taskId, {
      method: "DELETE"
    })
    .then( this.fetchTasks )
  }

最後の削除に関してはもはや言うことがありません!

これで一通りの機能の実装は終わりです!

おわりに

お疲れ様でした!
立派なタスク管理アプリケーションが出来上がりました。

でも…

putボタンを押したら、タスクの中身が「やったよ」に書き換えられるのは使い勝手が悪いですね。チェックをつけて、タスクが完了しているかどうかを切り替えられるようにしたほうが良いでしょう。
また、タスクそのものをクリックしたときに、タスクが入力フォームに変身し、そこでタスクの中身を自由に書き換えられるといった機能もあったらいいですね。
あとは、タスクを作成した日時、タスクの期日、タスクを完了した日時などもあると嬉しいです。
ほかにも、チェックボックスをつけて、完了したタスクと未完了のタスクの表示を切り替えられたり…。

なんて、書いてる僕もああしたいこうしたいが溢れてきます。

これを読んでReactで非同期通信デビューしたあなたなら、なおさらですよね…?

ぜひ試してみてください!

参考サイト一覧

この記事の中で挙げた参考サイトをトピック別にまとめました。

create-react-app / React

Facebook公式のcreate-react-appコマンドを使ってReact.jsアプリを爆速で作成する
Tutorial: Intro to React
Reactチュートリアル: Intro To React【日本語翻訳】
React component ライフサイクル図

Forms - React
React.jsでFormを扱う

json-server / JSON

typicode/json-server
JSONとは
Is it possible to 'get' data using POST with json-server?

Node.js / package.json

package.jsonの中身を理解する
npm package.json 取扱説明書
Node.js とは何か?
npm とは何か / Package と module の違い

JavaScript

JavaScript Promiseの本
Fetch 概説
【JavaScript】アロー関数式を学ぶついでにthisも復習する話

Web関係

リソース指向アーキテクチャ(ROA)とは何なのか
HTTPリクエスト/レスポンスの構成要素を初心者にも分かるように解説してみた

t12u
JavaScript / Vue.js / React / Node.js / Swift / Ruby / Ruby on Rails 大学生向けプログラミングコミュニティGeekSalonの監修をしています。 https://geek-salon.com/
https://codeninth.com/
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
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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
ユーザーは見つかりませんでした