やりたいこと
- 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というカラムを追加します。
<?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)に飛ぶように設定しておきます。
動作に必須ではありません。
Route::get('/{any}', function(){
return view('welcome');
})->where('any','.*');
welcome.blade.phpの編集
welcome.blace.phpを下記のように編集します。
利用するテンプレートレベルではReactもVueも違いはありません。
<!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>
この時点で確認するとこんな感じです(静的ですが最終イメージに近い)。
雛形の作成
まず、Reactでページを作成するための準備をしていきます。
app.jsの編集
Vue編ではapp.jsを直接編集しましたが、Reactではapp.jsでは、コンポーネントファイル(TodoApp.js)を読み込むだけにします。
require('./bootstrap');
+require('./components/TodoApp');
TodoApp.js
TodoApp.jsを編集します。まずは、<div>TodoAppを返すだけにします。
welcome.blade.phpのtodoAppという名称のIDに<TodoApp/>で定義したタグを挿入する設定です。
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は削除しました。
<!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です。
また、jsファイルへの変更をリアルタイムで確認するためにnpm run watchを実行しておきます。なお、この操作は別のターミナル画面で行います(php artisan serveとは別の画面)。
npm run watch
タスク一覧機能の作成
ではまず、タスクの一覧画面を作成します。
ダミーデータとして挿入されたtodoが表示されるようにします。
ルートの追加
まず、ルートの作成。
Route::group(['middleware' => 'api'], function(){
Route::get('get', 'TodoController@getTodos');
});
コントローラーの追加
続いてコントローラー。まず、Controllerファイルを生成します。
php artisan make:controller TodoController
todosテーブルの全データを返しています。
実際にはページネーションとかが必要と思いますが。
<?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を更新し描画
という感じです。
慣れていないと???という感じですが、なれるより他ありません。
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'));
この時点で確認するとテーブルのみがレンダリングされています。
タスク追加機能の実装
続いてタスクの追加機能を実装します。
ルート
ルートにaddを追加します。
Route::group(['middleware' => 'api'], function(){
Route::get('get', 'TodoController@getTodos');
+ Route::post('add', 'TodoController@addTodo');
});
コントローラー
追加機能を担う機能(addTodo)を実装します。
追加して、全件データを返します。
<?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
という感じです。。。
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'));
タスク削除機能の実装
では最後に削除機能を実装します。
ルート
ルートを追加。
Route::group(['middleware' => 'api'], function(){
Route::get('get', 'TodoController@getTodos');
Route::post('add', 'TodoController@addTodo');
+ Route::post('del', 'TodoController@deleteTodo');
});
コントローラー
削除機能を実装します。
IDを受け取り、対象IDのタスクを削除し、削除した後の全件データを返しています。
<?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
という感じです。
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を利用する(非同期処理)
そのうち書きます。