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


やりたいこと


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

そのうち書きます。