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

react.jsとPHP/MySQLでサンプルを作って見たので、はまったところなどを書いていきます。

More than 3 years have passed since last update.

はじめに

私はただReactを趣味で勉強しているだけで、業務でやってるわけでもなんでもありません。今回作成したのも自分の勉強のためだけです。
勉強しながら、はまったら検索してとコピペをしながら作ったため、つぎはぎ状態で一応動いているという感じのアプリです。
エラー処理などもほとんどやっていません。(そこまで考える余裕がありませんでした、、、)
そのため、間違っていることや古い書き方も多々あると思います。なのでこれから勉強したい人はあまり参考にしないでください。
ただ、やっぱり正しい指摘などをしていただければ、自分の勉強にもなりますし、コメント欄でみんな得すると思いますので、投稿させていただきました。
いたらない面も多いと思いますが、よろしくお願い致します。m(_ _)m

作成したもの→名前、メールアドレスを登録するアプリ

機能→追加・登録情報をリスト表示・更新・削除

スクリーンショット 2017-06-04 9.56.26.png

バージョン

php 7.1.1
MySQL 5.6
react 15.5.4

ディレクトリ構成

./
 ├ dist
 │ └ bundle.js
 ├ node_modules
 ├ src
 │  ├ CustomerForm.js
 │  ├ CustomerTable.js
 │ └ index.js
 ├ ajax.php
 ├ index.html
 ├ package.json
 └ webpack.config.js

データベース

テーブル構造です。

CREATE TABLE `customer` (
  `id` int(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
  `name` varchar(30) NOT NULL,
  `email` varchar(30) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime DEFAULT NULL
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

コード

webpackにより環境を作成しているので、htmlではbundle.jsを読み込んでいます。

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>React</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="dist/bundle.js"></script>
  </body>
</html>

webpackの設定ファイルです。

webpack.config.js
module.exports = {
   entry: './src/index.js',
   output: {
      path: __dirname + '/dist',
      filename: 'bundle.js'
   },
   module: {
      loaders: [
         {
            test: /.js?$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
            query: {
               presets: ['es2015', 'react']
            }
         }
      ]
   }
}

phpは便宜上1ファイルにしたので、
ajaxの時は$_POST['type']を送って、それぞれの処理を分けています。

ajax.php
<?php
try{
   $pdo = new PDO('mysql:dbname=sample;host=localhost;charset=utf8','root','root',[
      PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
      PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
   ]);

   header('Content-Type: application/json; charset=utf-8');
   switch($_POST['type']){
      case 'list':
         // 登録した情報のリスト取得
         echo json_encode($pdo->query('SELECT * FROM customer')->fetchAll());
         exit;
      case 'add':
         // 追加処理
         $stmt = $pdo->prepare('INSERT INTO customer(name,email,created) VALUES(?,?,now())');
         $stmt->execute([$_POST['name'],$_POST['email']]);
         exit;
      case 'edit_target':
         // IDから更新したいデータを取得
         $stmt = $pdo->prepare('SELECT id,name,email FROM customer WHERE id = ?');
         $stmt->execute([$_POST['id']]);
         echo json_encode($stmt->fetch());
         exit;
      case 'edit':
         // 更新処理
         $stmt = $pdo->prepare('UPDATE customer SET name = ?,email = ?,modified = now() WHERE id = ?');
         $stmt->execute([$_POST['name'],$_POST['email'],$_POST['editid']]);
         exit;
      case 'delete':
         // 削除処理
         $stmt = $pdo->prepare('DELETE FROM customer WHERE id = ?');
         $stmt->execute([$_POST['id']]);
         exit;
   }
}catch(PDOException $e){
   exit($e->getMessage());
}

メインのjsファイルです。
ここからCustomerForm.jsとCustomerTable.jsを読みこんでいます。

index.js
import React from 'react';
import { render } from 'react-dom';
import request from 'superagent';

import CustomerForm from './CustomerForm';
import CustomerTable from './CustomerTable';

class Customer extends React.Component {
   constructor(props) {
      super(props)

      this.state = {
         datas: [],
         status: 'add',
         editData: ''
      };

      this.drawList = this.drawList.bind(this);
      this.henkouButton = this.henkouButton.bind(this);
   }

   componentDidMount(){
      this.drawList();
   }

   drawList(){
      request
         .post('./ajax.php')
         .type('form')
         .send({type: 'list'})
         .end((err, res) => {
            this.setState({
               datas: res.body,
               status: 'add',
            });
         });
   }

   henkouButton(e){
      request
         .post('./ajax.php')
         .type('form')
         .send({
            type: 'edit_target',
            id: e.target.dataset.id,
         })
         .end((err, res) => {
            this.setState({
               status:'edit',
               editData: res.body,
            });
         });
   }

   render(){
      const propsForm = {status: this.state.status, editData: this.state.editData, drawList: this.drawList}
      const propsTable = {datas: this.state.datas, henkouButton: this.henkouButton, drawList: this.drawList}

      return (
         <div>
            <CustomerForm {...propsForm}/>
            <CustomerTable {...propsTable}/>
         </div>
      );
   }
}

render(
   <Customer />,
   document.getElementById('root')
);

追加・更新用のフォーム処理のコンポーネントです。

CustomerForm.js
import React from 'react';
import request from 'superagent';
export default class CustomerForm extends React.Component{
   constructor(props) {
      super(props)

      this.state = {
         name: 'テスト太郎',
         email: 'sample@email.com',
      };

      this.handleClick = this.handleClick.bind(this);
      this.onChangeName = this.onChangeName.bind(this);
      this.onChangeEmail = this.onChangeEmail.bind(this);
   }

   componentWillReceiveProps(nextProps){
      if(nextProps.editData){
         this.setState({
            name: nextProps.editData.name,
            email: nextProps.editData.email,
         });
      }
   }

   handleClick(e){
      request
         .post('./ajax.php')
         .type('form')
         .send({
            type: this.props.status,
            name: this.state.name,
            email: this.state.email,
            editid: this.props.editData.id
         })
         .end((err, res) => {
            this.props.drawList();
         });
   }

   onChangeName(e){
      this.setState({ name: e.target.value });
   }

   onChangeEmail(e){
      this.setState({ email: e.target.value });
   }

   render(){
      return (
         <form>
            <fieldset>
               <legend>{this.props.status === 'add' ? '新規登録' : 'ID:' + this.props.editData.id}</legend>
               <div>
                  <label htmlFor="name">名前:</label>
                  <input
                     type="text"
                     id="name"
                     name="name"
                     required="true"
                     value={this.state.name || ''}
                     onChange={this.onChangeName} />
               </div>
               <div>
                  <label htmlFor="email">Email</label>
                  <input
                     type="email"
                     id="email"
                     name="email"
                     value={this.state.email || ''}
                     onChange={this.onChangeEmail} />
               </div>
               <input
                  type="button"
                  value='登録'
                  disabled={this.props.status === 'add' ? false : true}
                  onClick={this.handleClick} />
               &nbsp;
               <input
                  type="button"
                  value='更新'
                  disabled={this.props.status === 'edit' ? false : true}
                  onClick={this.handleClick} />
            </fieldset>
         </form>
      );
   }
}

登録した情報のリスト表示用のコンポーネントです。

CustomerTable.js
import React from 'react';
import request from 'superagent';
export default class CustomerTable extends React.Component{
   constructor(props) {
      super(props)

      this.deleteList = this.deleteList.bind(this);
   }

   deleteList(e){
      request
         .post('./ajax.php')
         .type('form')
         .send({
            'type': 'delete',
            'id': e.target.dataset.id
         })
         .end((err, res) => {
            this.props.drawList();
         });
   }

   formatDate(date){
      if(!date)return '';
      const dt = new Date(date);
      return `${dt.getFullYear()}${dt.getMonth()+1}${dt.getDate()}${dt.getHours()}${dt.getMinutes()}${dt.getSeconds()}秒`;
   }

   render(){
      const style = {
         table:{borderCollapse:'collapse',marginTop:'10px',width:'70%'},
         thead: {backgroundColor:'#adff2f'},
         tr: {borderBottom: '1px solid black'}
      };

      return(
         <table style={style.table}>
            <thead style={style.thead}>
               <tr>
                  <th>ID</th><th>名前</th><th>Email</th><th>登録日</th><th>変更日</th><th></th><th></th>
               </tr>
            </thead>
            <tbody>
               {this.props.datas.map((data) => {
                  return (
                     <tr key={data.id} style={style.tr}>
                        <td>{data.id}</td>
                        <td>{data.name}</td>
                        <td>{data.email}</td>
                        <td>{this.formatDate(data.created)}</td>
                        <td>{this.formatDate(data.modified)}</td>
                        <td><input type="button" value="変更" onClick={this.props.henkouButton} data-id={data.id}/></td>
                        <td><input type="button" value="削除" onClick={this.deleteList} data-id={data.id}/></td>
                     </tr>
                  );
               })}
            </tbody>
         </table>
      );
   }
}

登録情報をリスト表示

該当コード

index.js
class Customer extends React.Component {
   constructor(props) {
      super(props)
      this.state = {
         datas: [],
         // ...
      };
      this.drawList = this.drawList.bind(this);
      // ...
   }
   componentDidMount(){
      this.drawList();
   }
   drawList(){
      request
         .post('./ajax.php')
         .type('form')
         .send({type: 'list'})
         .end((err, res) => {
            this.setState({
               datas: res.body,
               // ...
            });
         });
   }
   // ...
   render(){
      // ...
      const propsTable = {datas: this.state.datas, /* 省略 */ drawList: this.drawList}
      return (
         <div>
            {/* 省略 */}
            <CustomerTable {...propsTable}/>
         </div>
      );
   }
}
CustomerTable.js
            <tbody>
               {this.props.datas.map((data) => {
                  return (
                     <tr key={data.id} style={style.tr}>
                        <td>{data.id}</td>
                        <td>{data.name}</td>
                        <td>{data.email}</td>
                        <td>{this.formatDate(data.created)}</td>
                        <td>{this.formatDate(data.modified)}</td>
                        <td><input type="button" value="変更" onClick={this.props.henkouButton} data-id={data.id}/></td>
                        <td><input type="button" value="削除" onClick={this.deleteList} data-id={data.id}/></td>
                     </tr>
                  );
               })}
            </tbody>

はまった点

・ajaxはどうすればいいのか
いろいろ検索してみるとsuperagentがよく使われているようなので、それを使用してみました。

$ npm install superagent
import request from 'superagent';

・リスト表示関数をどこに書けばいいのか
最初CustomerTable.jsで全ての処理を書こうと思ったのですが、それだと、別コンポーネントに書いているformから追加、更新などの時にリストを再描画させるにはどうしたらいいのかがわからず、親コンポーネントに処理を書くことに、
正攻法かはわからないですが、それでコンポーネント間で処理を行えたので、とりあえず親コンポーネントでリスト表示処理を書きました。

新規登録処理

該当コード

CustomerForm.js
export default class CustomerForm extends React.Component{
   constructor(props) {
      super(props)
      this.state = {
         name: 'テスト太郎',
         email: 'sample@email.com',
      };
      // ...
   }

   handleClick(e){
      request
         .post('./ajax.php')
         .type('form')
         .send({
            type: this.props.status,
            name: this.state.name,
            email: this.state.email,
            // ...
         })
         .end((err, res) => {
            this.props.drawList();
         });
   }

   onChangeName(e){
      this.setState({ name: e.target.value });
   }

   onChangeEmail(e){
      this.setState({ email: e.target.value });
   }

   render(){
      return (
         <form>
            <fieldset>
               <legend>{this.props.status === 'add' ? '新規登録' : 'ID:' + this.props.editData.id}</legend>
               <div>
                  <label htmlFor="name">名前:</label>
                  <input
                     type="text"
                     id="name"
                     name="name"
                     required="true"
                     value={this.state.name || ''}
                     onChange={this.onChangeName} />
               </div>
               <div>
                  <label htmlFor="email">Email</label>
                  <input
                     type="email"
                     id="email"
                     name="email"
                     value={this.state.email || ''}
                     onChange={this.onChangeEmail} />
               </div>
               <input
                  type="button"
                  value='登録'
                  disabled={this.props.status === 'add' ? false : true}
                  onClick={this.handleClick} />
               &nbsp;
               {/* 省略 */}
            </fieldset>
         </form>
      );
   }
}

はまった点

・登録が終わった後にリストの再描画
子コンポーネントから親コンポーネントのメソッドを使用するのですが、そこではまりました。
propsで関数を親コンポーネントから子コンポーネントに渡すとうまくいくようです。

index.js
<CustomerTable drawList={this.drawList}/>

ただこれだけですと、以下のようにエラーが出ましたので、.bind(this)をつけるとうまくいきました。
スクリーンショット 2017-06-04 10.48.45.png

index.js
   constructor(props) {
      super(props)
      this.drawList = this.drawList.bind(this);
   }

または

index.js
<CustomerTable drawList={this.drawList.bind(this)}/>

実行箇所

CustomerForm.js
      request
         .post('./ajax.php')
         .type('form')
         .send({
            type: this.props.status,
            name: this.state.name,
            email: this.state.email,
            // ...
         })
         .end((err, res) => {
            this.props.drawList(); //propsから渡された親コンポーネントのメソッドを使用
         });

TJJKJnaXBX.gif

削除ボタン

該当コード

CustomerTable.js
   deleteList(e){
      request
         .post('./ajax.php')
         .type('form')
         .send({
            'type': 'delete',
            'id': e.target.dataset.id
         })
         .end((err, res) => {
            this.props.drawList();
         });
   }

   render(){
       // ...
            <tbody>
               {this.props.datas.map((data) => {
                  return (
                     <tr key={data.id} style={style.tr}>
                        {/* 省略 */}
                        <td><input type="button" value="削除" onClick={this.deleteList} data-id={data.id}/></td>
                     </tr>
                  );
               })}
            </tbody>
      // ...
   }

はまった点

・clickイベントでidを取得するにはどうしたらいいのか
とりあえずタグにはdata-id={data.id}と一意のデータを入れてみましたが、その取得方法ではまりました。

e.targetでタグを取得できるので、そこからdataを取るという方法でうまくいきました。
ただ検索すると他にもいろいろ方法があるみたいです。
スクリーンショット 2017-06-04 11.35.51.png

スクリーンショット 2017-06-04 11.36.27.png

更新ボタン

該当コード

CustomerTable.js
      <tbody>
         {this.props.datas.map((data) => {
            return (
               <tr key={data.id} style={style.tr}>
                  {/* 省略 */}
                  <td><input type="button" value="変更" onClick={this.props.henkouButton} data-id={data.id}/></td>
                  {/* 省略 */}
               </tr>
            );
         })}
      </tbody>
index.js
   henkouButton(e){
      request
         .post('./ajax.php')
         .type('form')
         .send({
            type: 'edit_target',
            id: e.target.dataset.id,
         })
         .end((err, res) => {
            this.setState({
               status:'edit',
               editData: res.body,
            });
         });
   }
   render(){
      const propsForm = {status: this.state.status, editData: this.state.editData, /* 省略 */}
      const propsTable = {datas: this.state.datas, henkouButton: this.henkouButton,  /* 省略 */}
      return (
         <div>
            <CustomerForm {...propsForm}/>
            <CustomerTable {...propsTable}/>
         </div>
      );
CustomerForm.js
   componentWillReceiveProps(nextProps){
      if(nextProps.editData){
         this.setState({
            name: nextProps.editData.name,
            email: nextProps.editData.email,
         });
      }
   }
   render(){
      return (
         <form>
            <fieldset>
               <legend>{this.props.status === 'add' ? '新規登録' : 'ID:' + this.props.editData.id}</legend>
               <div>
                  <label htmlFor="name">名前:</label>
                  <input
                     type="text"
                     id="name"
                     name="name"
                     required="true"
                     value={this.state.name || ''}
                     onChange={this.onChangeName} />
               </div>
               <div>
                  <label htmlFor="email">Email</label>
                  <input
                     type="email"
                     id="email"
                     name="email"
                     value={this.state.email || ''}
                     onChange={this.onChangeEmail} />
               </div>
               {/* 省略 */}
               <input
                  type="button"
                  value='更新'
                  disabled={this.props.status === 'edit' ? false : true}
                  onClick={this.handleClick} />
            </fieldset>
         </form>
      );
   }

変更ボタンをクリックしたら、入力フォームが新規登録から、変更用のフォームに切り替わるという処理の流れにしました。

CustomerTabelの変更ボタンを押すことで、親コンポーネントのstateを変更し、それをCustomerFormのinputタグの値に入れるということをしているのでちょっとややこしくなってしまった。
SAvKV41tMh.gif

はまった点

・親コンポーネントのstateを変更したが、子コンポーネントに反映されない

初心者にやりがちなミスだと思いますが、、、最初componentDidMountの中でsetStateしようとしたのですが、それだと親コンポーネントのstate更新した際に処理が実行されませんでした。
更新時に実行され、かつsetStateできるのがcomponentWillReceivePropsだけなようでしたので、そう書き換えて解決できました。以下のページを参考にさせていただきました。
http://qiita.com/yukika/items/1859743921a10d7e3e6b

更新時
   // 更新時に実行されない
   componentDidMount(){
      this.setState({
         name: this.props.editData.name,
         email: this.props.editData.email,
      });
   }

   // 更新時に実行される
   componentWillReceiveProps(nextProps){
      this.setState({
         name: nextProps.editData.name,
         email: nextProps.editData.email,
      });
   }

・変更ボタンを押してもinputタグに反映されない

CustomerForm.js
   <input
      type="text"
      id="name"
      name="name"
      required="true"
      defaultValue={this.state.name} {/* これだと変更されませんでした */}
      onChange={this.onChangeName} />

最初defaultValueで初期値を設定していたのですが、defaultValueは初期ロード時にしか実行されないらしく、以下のサイトを参考にしvalue={this.state.name || ''}と書き換えることでうまくいきました。
https://stackoverflow.com/questions/30727837/react-change-input-defaultvalue-by-passing-props

最後まで読んでいただき、ありがとうございました。:persevere:

okumurakengo
人が作ってくれたご飯食べるときに何も言わずに食べるのは、ちょっとダメらしいという話を聞いたことがあるので、「あ、うめ、あ、うめ」って言いながら食ってたら、すごい変な人と思われてしまってしまった/初心者です、あまりわかっていません
https://bokete.jp/user/okumurakengo
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
ユーザーは見つかりませんでした