Laravel5.7でReactを利用した開発をしてみる


## やりたいこと

* React + Laravelで簡単なTodoアプリを作ってみる

[以前Vue版を書いた](https://qiita.com/zaburo/items/603f422ed78c15c47a2a)が、そのReact版。正直まとまり悪いです。すみません。あと、参考ですが[Redux利用版](https://qiita.com/zaburo/items/d88e1dc7883f4f7f59b9)も書いてみました。

## Laravelのプリセットを（Vueから）Reactに切り替える

Laravelは標準でVueの利用環境が組み込まれていますが、5.5からはReactにも対応しています。
今回はReactを利用したいのでプリセットをVueからReactに切り替えます。

```bash
composer create-project laravel/laravel laravel-react
cd laravel-react

php artisan preset react

npm install && npm run dev
```

切り替えが完了したら、一度npm installおよびnpm run devで正しくビルドできるか確認しておきます。

## データの準備

まずはDBやデータまわりを準備します。

### テーブルの作成

todo管理用の簡単なテーブルを用意するためにmigrationファイルを用意します。

```bash
php artisan make:migration create_todos_table
```

今回はtodoのタイトルだけ保持したいので、titleというカラムを追加します。

```diff:database/migrations/xxxxx_create_todos_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTodosTable extends Migration
{
    public function up()
    {
        Schema::create('todos', function (Blueprint $table) {
            $table->increments('id');
+           $table->string('title');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('todos');
    }
}
```

編集が終わったらmigrateしてテーブルを生成します。

```bash
php artisan migrate
```

### モデルの作成

eloquentを利用したのでモデルを生成しておきます。

```bash
php artisan make:model Todo
```

### データの挿入

次にダミーデータを挿入しておきます。下記では2つのデータを挿入しています。
いろいろな挿入方法がありますが、ここではtinkerを利用してみます。

```bash
php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.7 — cli) by Justin Hileman
>>>
>>> $todo1 = new App\Todo;
=> App\Todo {#2899}
>>> $todo1->title = '買い物に行く';
=> "買い物に行く"
>>> $todo1->save();
=> true
>>>
>>> $todo2 = new App\Todo;
=> App\Todo {#2905}
>>> $todo2->title = '散歩に行く';
=> "散歩に行く"
>>> $todo2->save();
=> true
>>>
```

## 全体のルーティング（全てのリクエストをwelcome.blade.phpに飛ばす）

ReactやVueを利用したSPAではルーティングはjs側で行うので、何をリクエストされてもSPAのTOPページ（ここではwelcome.blade.php）に飛ぶように設定しておきます。

>動作に必須ではありません。

```php:routes/web.php
Route::get('/{any}', function(){
    return view('welcome');
})->where('any','.*');
```

## welcome.blade.phpの編集

welcome.blace.phpを下記のように編集します。
利用するテンプレートレベルではReactもVueも違いはありません。

```diff:resources/views/welcome.blade.php
<!doctype html>
<html lang="ja">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
+   <meta name="csrf-token" content="{{ csrf_token() }}">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- CSS -->
+   <link rel="stylesheet" href="{{ asset('css/app.css') }}">

    <title>react test</title>
</head>

<body>
    <div id="app">
        <div class="container">
            <h3 class="mt-5">Todo 管理システム</h3>

            <!-- form -->
            <div class="form-group mt-4">
                <label for="todo">新規Todo</label>
                <input type="text" class="form-control" id="todo">
            </div>
            <button type="submit" class="btn btn-primary">登録</button>

            <!-- table -->
            <table class="table mt-5">
                <thead>
                    <th>ID</th><th>タスク</th><th>完了</th>
                </thead>
                <tbody>
                    <tr>
                        <td>1</td>
                        <td>タスクがでる</td>
                        <td><button class="btn btn-secondary">完了</button></td>
                    </tr>
                    <tr>
                        <td>2</td>
                        <td>タスクがでる</td>
                        <td><button class="btn btn-secondary">完了</button></td></tr>
                </tbody>
            </table>
        </div>
    </div>

<!-- avaScript -->
+<script src="{{ asset('js/app.js')}}"></script>
</body>
</html>
```

この時点で確認するとこんな感じです（静的ですが最終イメージに近い）。

![スクリーンショット 2018-10-16 6.11.43.png](https://qiita-image-store.s3.amazonaws.com/0/55188/65153dc1-1d05-9bfa-eb65-43cff7839cbb.png)



## 雛形の作成

まず、Reactでページを作成するための準備をしていきます。

### app.jsの編集

Vue編ではapp.jsを直接編集しましたが、Reactではapp.jsでは、コンポーネントファイル(TodoApp.js）を読み込むだけにします。

```diff:resouces/js/app.js

require('./bootstrap');

+require('./components/TodoApp');

```

### TodoApp.js

TodoApp.jsを編集します。まずは、\<div>TodoApp</div>を返すだけにします。
welcome.blade.phpのtodoAppという名称のIDに\<TodoApp/>で定義したタグを挿入する設定です。

```js:resources/js/components/TodoApp.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

export default class TodoApp extends Component {
    render() {
        return (
            <div>TodoApp</div>
        );
    }
}

ReactDOM.render(<TodoApp />, document.getElementById('todoApp'));
```

### welcome.blade.php

welcome.blade.php側に挿入ポイントを作成します。ダミーで技術していたHTMLは削除しました。

```html:resouces/views/welcome.blade.php
<!doctype html>
<html lang="ja">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- CSS -->
    <link rel="stylesheet" href="{{ asset('css/app.css') }}">

    <title>react test</title>
</head>

<body>
    <div id="app">
        <div class="container">
            <h3 class="mt-5">Todo 管理システム</h3>
          
            <!-- ここを置き換えていく -->
            <div id="todoApp"></div>
              
        </div>
    </div>

<!-- avaScript -->
<script src="{{ asset('js/app.js')}}"></script>
</body>
</html>
```

正しく見えるかserveコマンドで確認しておきます。

```bash
php artisan serve
```

TodoAppとだけ表示されていればOKです。

![スクリーンショット 2018-10-15 21.13.30.png](https://qiita-image-store.s3.amazonaws.com/0/55188/76c20e49-4013-ac11-611b-c12fa4bdc6a4.png)


また、jsファイルへの変更をリアルタイムで確認するためにnpm run watchを実行しておきます。なお、この操作は別のターミナル画面で行います（php artisan serveとは別の画面）。

```bash
npm run watch
```


## タスク一覧機能の作成

ではまず、タスクの一覧画面を作成します。
ダミーデータとして挿入されたtodoが表示されるようにします。

### ルートの追加

まず、ルートの作成。

```php:routes/api.php
Route::group(['middleware' => 'api'], function(){
    Route::get('get', 'TodoController@getTodos');
});
```

### コントローラーの追加

続いてコントローラー。まず、Controllerファイルを生成します。


```bash
php artisan make:controller TodoController
```

todosテーブルの全データを返しています。

>実際にはページネーションとかが必要と思いますが。

```php:app/Http/Controllers/TodoController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Todo;

class TodoController extends Controller
{
    //getTodos
    public function getTodos()
    {
        $todos = Todo::all();
        return $todos;
    }
}
```

### TodoApp.jsの編集

まず、テーブルにtodo一覧を表示させてみます（追加用のフォームは後回し）。

実装の順番としては、

* まず、tableを実装する。
* テーブルの骨子やthead部分はそのまま（JSX）タグで記述
* 連続表示が必要な行部分は\<RenderRows>として外で描画
* todoの一覧はstateのtodosにオブジェクト配列として保持する
* componentDidMount()でapiに問い合わせし、setState()でtodosを更新し描画

という感じです。
慣れていないと？？？という感じですが、なれるより他ありません。

```js:resources/js/components/TodoApp.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

//RenderRowsの機能実装
function RenderRows(props){
    //mapでループしている（for相当）
    return props.todos.map(todo => {
        return (
            <tr key={todo.id}>
                <td>{todo.id}</td>
                <td>{todo.title}</td>
                <td><button className="btn btn-secondary">完了</button></td>
            </tr>
        );
    });
}

export default class TodoApp extends Component {

    //コンストラクタ内でstateにtodosを宣言
    constructor(){
        super();
        this.state = {
            todos: []
        }
    }

    //コンポーネントがマウントされた時点で初期描画用のtodosをAPIから取得
    componentDidMount(){
        axios
            .get('/api/get')
            .then((res) => {
                //todosを更新（描画がかかる）
                this.setState({
                    todos: res.data
                });
            })
            .catch(error => {
                console.log(error)
            })
    }

    //テーブルの骨組みを描画し、行の描画はRenderRowsに任せる（その際、todosを渡す）
    render() {
        return (
            <React.Fragment>
                <table className="table mt-5">
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>タスク</th>
                            <th>完了</th>
                        </tr>
                    </thead>
                    <tbody>
                        {/* 行の描画 */}
                        <RenderRows
                            todos={this.state.todos}
                        />
                    </tbody>
                </table>
            </React.Fragment>
        );
    }
}

ReactDOM.render(<TodoApp />, document.getElementById('todoApp'));
```

この時点で確認するとテーブルのみがレンダリングされています。

![スクリーンショット 2018-10-16 6.05.44.png](https://qiita-image-store.s3.amazonaws.com/0/55188/c51342af-9f28-8add-0d4b-c5b3eac8d651.png)


## タスク追加機能の実装

続いてタスクの追加機能を実装します。

### ルート

ルートにaddを追加します。

```diff:routes/api.php
Route::group(['middleware' => 'api'], function(){
    Route::get('get', 'TodoController@getTodos');
+   Route::post('add', 'TodoController@addTodo');
});

```

### コントローラー

追加機能を担う機能（addTodo）を実装します。
追加して、全件データを返します。

```diff:app/Http/Controllers/TodoController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Todo;

class TodoController extends Controller
{
    //getTodos
    public function getTodos()
    {
        $todos = Todo::all();
        return $todos;
    }

    //add todo
+   public function addTodo(Request $request)
+   {
+       $todo = new Todo;
+       $todo->title = $request->title;
+       $todo->save();

+       $todos = Todo::all();
+       return $todos;
+   }
}
```

### TodoApp.js

実装の流れは

* タグにinputやbuttonを追加
* 入力値を保持するためのtodoをstateに追加
* input値を正しく処理するためにinputChange()を実装
* add処理を行うためのaddTodo()を実装
* 上記メソッドをconstructorでbind

という感じです。。。

```diff:resources/js/components/TodoApp.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

//RenderRowsの機能実装
function RenderRows(props){
    //mapでループしている（for相当）
    return props.todos.map(todo => {
        return (
            <tr key={todo.id}>
                <td>{todo.id}</td>
                <td>{todo.title}</td>
                <td><button className="btn btn-secondary">完了</button></td>
            </tr>
        );
    });
}

export default class TodoApp extends Component {

    //コンストラクタ内でstateにtodosを宣言
    constructor(){
        super();
        this.state = {
            todos: [],
+           todo: ''
        };
+       this.inputChange = this.inputChange.bind(this);
+       this.addTodo = this.addTodo.bind(this);
    }

    //コンポーネントがマウントされた時点で初期描画用のtodosをAPIから取得
    componentDidMount(){
        axios
            .get('/api/get')
            .then((res) => {
                //todosを更新（描画がかかる）
                this.setState({
                    todos: res.data
                });
            })
            .catch(error => {
                console.log(error)
            })
    }

+   //入力がされたら（都度）
+   inputChange(event){
+       switch(event.target.name){
+           case 'todo':
+               this.setState({
+                   todo: event.target.value
+               });
+               break;
+           default:
+               break;
+       }
+   }

+   //登録ボタンがクリックされたら
+   addTodo(){

+       //空だと弾く
+       if(this.state.todo == ''){
+           return;
+       }

+       //入力値を投げる
+       axios
+           .post('/api/add', {
+               title: this.state.todo
+           })
+           .then((res) => {
+               //戻り値をtodosにセット
+               this.setState({
+                   todos: res.data,
+                   todo: ''
+               });
+           })
+           .catch(error => {
+               console.log(error);
+           });

+   }

    //テーブルの骨組みを描画し、行の描画はRenderRowsに任せる（その際、todosを渡す）
    render() {
        return (
            <React.Fragment>
                
+               {/* add from */}
+               <div className="form-group mt-4">
+                   <label htmlFor="todo">新規Todo</label>
+                   <input type="text" className="form-control" name="todo" value={this.state.todo} onChange={this.inputChange}/>
+               </div>
+               <button className="btn btn-primary" onClick={this.addTodo}>登録</button>

                {/* table */}
                <table className="table mt-5">
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>タスク</th>
                            <th>完了</th>
                        </tr>
                    </thead>
                    <tbody>
                        {/* 行の描画 */}
                        <RenderRows
                            todos={this.state.todos}
                        />
                    </tbody>
                </table>
            </React.Fragment>
        );
    }
}

ReactDOM.render(<TodoApp />, document.getElementById('todoApp'));


```

## タスク削除機能の実装

では最後に削除機能を実装します。

### ルート

ルートを追加。

```diff:routes/api.php
Route::group(['middleware' => 'api'], function(){
    Route::get('get', 'TodoController@getTodos');
    Route::post('add', 'TodoController@addTodo');
+   Route::post('del', 'TodoController@deleteTodo');
});

```

### コントローラー

削除機能を実装します。
IDを受け取り、対象IDのタスクを削除し、削除した後の全件データを返しています。

```diff:app/Http/Controllers/TodoController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Todo;

class TodoController extends Controller
{
    //getTodos
    public function getTodos()
    {
        $todos = Todo::all();
        return $todos;
    }

    //add todo
    public function addTodo(Request $request)
    {
        $todo = new Todo;
        $todo->title = $request->title;
        $todo->save();

        $todos = Todo::all();
        return $todos;
    }

+   //delete
+   public function deleteTodo(Request $request)
+   {
+       $todo = Todo::find($request->id);
+       $todo->delete();

+       $todos = Todo::all();
+       return $todos;
+   }
}
```

### TodoApp.js

行っている処理は

* 完了ボタンにdeleteTask()を割り当て
* deleteTask()を実装
* deleteTask()をconstructorでbind

という感じです。

```diff:resources/js/components/TodoApp.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

//RenderRowsの機能実装
function RenderRows(props){
    //mapでループしている（for相当）
    return props.todos.map(todo => {
        return (
            <tr key={todo.id}>
                <td>{todo.id}</td>
                <td>{todo.title}</td>
+               <td><button className="btn btn-secondary" onClick={() => props.deleteTask(todo)}>完了</button></td>
            </tr>
        );
    });
}

export default class TodoApp extends Component {

    //コンストラクタ内でstateにtodosを宣言
    constructor(){
        super();
        this.state = {
            todos: [],
            todo: ''
        };
        this.inputChange = this.inputChange.bind(this);
        this.addTodo = this.addTodo.bind(this);
+       this.deleteTask = this.deleteTask.bind(this);
    }

    //コンポーネントがマウントされた時点で初期描画用のtodosをAPIから取得
    componentDidMount(){
        axios
            .get('/api/get')
            .then((res) => {
                //todosを更新（描画がかかる）
                this.setState({
                    todos: res.data
                });
            })
            .catch(error => {
                console.log(error)
            })
    }

    //入力がされたら（都度）
    inputChange(event){
        switch(event.target.name){
            case 'todo':
                this.setState({
                    todo: event.target.value
                });
                break;
            default:
                break;
        }
    }

    //登録ボタンがクリックされたら
    addTodo(){

        //空だと弾く
        if(this.state.todo == ''){
            return;
        }

        //入力値を投げる
        axios
            .post('/api/add', {
                title: this.state.todo
            })
            .then((res) => {
                //戻り値をtodosにセット
                this.setState({
                    todos: res.data,
                    todo: ''
                });
            })
            .catch(error => {
                console.log(error);
            });

    }

+   //完了ボタンがクリックされたら
+   deleteTask(todo){
+       axios
+           .post('/api/del', {
+               id: todo.id
+           })
+           .then((res) => {
+               this.setState({
+                   todos: res.data
+               });
+           })
+           .catch(error => {
+               console.log(error);
+           });
+   }

    //テーブルの骨組みを描画し、行の描画はRenderRowsに任せる（その際、todosを渡す）
    render() {
        return (
            <React.Fragment>
                
                {/* add from */}
                <div className="form-group mt-4">
                    <label htmlFor="todo">新規Todo</label>
                    <input type="text" className="form-control" name="todo" value={this.state.todo} onChange={this.inputChange}/>
                </div>
                <button className="btn btn-primary" onClick={this.addTodo}>登録</button>

                {/* table */}
                <table className="table mt-5">
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>タスク</th>
                            <th>完了</th>
                        </tr>
                    </thead>
                    <tbody>
                        {/* 行の描画 */}
                        <RenderRows
                            todos={this.state.todos}
+                           deleteTask={this.deleteTask}
                        />
                    </tbody>
                </table>
            </React.Fragment>
        );
    }
}

ReactDOM.render(<TodoApp />, document.getElementById('todoApp'));
```

以上になります。

## 今後

* Reduxを利用する（state管理）
* ThunkかSagaを利用する（非同期処理）

そのうち書きます。