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

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

More than 1 year has passed since last update.

やりたいこと

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

以前Vue版を書いたが、そのReact版。正直まとまり悪いです。すみません。あと、参考ですがRedux利用版も書いてみました。

Laravelのプリセットを(Vueから)Reactに切り替える

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

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ファイルを用意します。

php artisan make:migration create_todos_table

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

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してテーブルを生成します。

php artisan migrate

モデルの作成

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

php artisan make:model Todo

データの挿入

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

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)に飛ぶように設定しておきます。

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

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

welcome.blade.phpの編集

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

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

雛形の作成

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

app.jsの編集

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

resouces/js/app.js
require('./bootstrap');

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

TodoApp.js

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

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は削除しました。

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コマンドで確認しておきます。

php artisan serve

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

スクリーンショット 2018-10-15 21.13.30.png

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

npm run watch

タスク一覧機能の作成

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

ルートの追加

まず、ルートの作成。

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

コントローラーの追加

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

php artisan make:controller TodoController

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

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

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を更新し描画

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

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

タスク追加機能の実装

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

ルート

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

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

コントローラー

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

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

という感じです。。。

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'));


タスク削除機能の実装

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

ルート

ルートを追加。

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のタスクを削除し、削除した後の全件データを返しています。

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()を実装
  • deleteTaxk()をconstructorでbind

という感じです。

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を利用する(非同期処理)

そのうち書きます。

zaburo
こんにちは。自分用のメモをだらだら公開しています。
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
ユーザーは見つかりませんでした