k01ken’s b10g

He110 W0r1d!

Reactで詰まったポイントの解決メモ

開発環境は、Windows10 64bit(Pro) + react@16.8.6。


■ライフサイクルメソッドについて
・stateが変更されるたびに、componentWillUpdate,componentDidUpdateメソッドがそれぞれ呼び出される。


■componentWillMountを使用すると、querySelectorAllでは、まだ要素は読み込めない。componentDidMonutでようやく読み込める。
・イベントは、addEventListenerで追加するのではなくて、タグにonClickやonFocusなど書いて、その中にコールバック関数を指定する方法が一般的か。


■ルーティングで、ログインページとユーザーの個別ページの場合、LinkコンポーネントをSwitchコンポーネントで囲っておいて、ユーザーの個別ページを一番最後に設置しておかないと、BrowserRouterコンポーネントでは個別ページをログインページと認識して両方表示されてしまう。

<Switch>
  <Link to="/login" render={Login} />
  <Link to="/:username" render={UserPage} />
</Switch>

Switchコンポーネントだと、一番最初にマッチしたものだけを表示してくれる。


参考リンク

React入門



■LinkコンポーネントでonClickメソッド属性にメソッドを指定していることで無限ループしてしまう際の対処法

例えば、バックエンド側からデータを引っ張ってくるメソッドをreadPageとして、下記のように記述すると、

<Link onClick={this.readPage('/')} to="/" />

無限ループしてしまう。(無限に指定ページを読み込もうとする)
括弧をつけて、記述しているから起動している?
ただ、

<Link onClick={() => { this.readPage('/')} } to="/" />

という風にアロー関数で指定すると大丈夫。

メソッドを最初からアロー関数で定義していたらどうなのだろうか?



■子コンポーネントから親コンポーネントのstateを変更する

stateを変更するメソッドは、コンストラクタでバインドしておくこと。
そうしないと、変更できない。

コンポーネント

constructor(props){
  super(props);
  this.state = {
    isClicked: false
  };
  this.clickStateChanger = this.clickStateChanger.bind(this);
}

clickStateChanger(){
  let current_state = this.state.isClicked;
  this.setState({isClicked: !current_state});
  console.log("current_state: " + !current_state);
}

<Child clickStateChanger={this.clickStateChanger} />

コンポーネント

<button onClick={this.props.clickStateChanger} >click me</button>

参考リンク
[react]子のコンポーネントから親のstateを変更する方法 - Qiita
コンポーネントに関数を渡す – React



■親コンポーネントから子コンポーネントへメソッドを渡して使用する際の注意点
コンポーネントからメソッドを受け取って、子コンポーネントのbuttonのonClick属性などに設定して、呼び出すパターンです。

コンポーネント

handleAlert(){
  console.log("handleAlert");
}

<Child handleAlert={this.handleAlert} />
/*
ここで、
<Child handleAlert={() => { this.handleAlert} } />
のようにアロー関数で渡すとhandleAlertが動作しないので注意。
*/

コンポーネント

<button onClick={this.props.handleAlert}>click me</button>

これだと、問題なく呼び出せますが、例えば、子コンポーネント側で、メソッドを用意して、その中で、親コンポーネントのメソッドを呼び出したい場合は、子コンポーネント側を以下のようにすると、wrap()メソッド内の、thisが、親コンポーネントを指していなくて、TypeError: this is undefinedとなる。

コンポーネント

wrap(){
  this.props.handleAlert();
}

<button onClick={this.wrap}>click me</button>

そこで、wrap()メソッドをアロー関数として呼び出すと問題なく動作する。

コンポーネント

wrap = () => {
  this.props.handleAlert();
}

<button onClick={this.wrap}>click me</button>



■子コンポーネントへメソッドを渡す際に、引数を指定できるようにする

コンポーネント

import React from 'react';
import Child from './Child';

class Parent extends React.Component{
  testMethod(name){
    console.log("Hello, " + name + "!");
  }

  render(){
    return(
    <Child testMethod={(name) => this.testMethod(name)} />
    );
  }
}

コンポーネント

<button onClick={() => this.props.testMethod("k01ken")}>click me</button>

export default Child;

参考リンク
React子コンポーネントから親の引数付きメソッドを実行する - Qiita


■JSX文法内部で条件分岐させる
JSX内にて、stateの状態によって、CSSのclass属性を切り替えたい場合は、

<div className={this.state.isClicked === true ? "isClicked" : "notClicked"}></div>

のように書いていきます。

他にも、一部の文字列は共通だが、残りの文字列は状態に応じて切り替えたい場合は、状態に応じて切り替わる方を()で括っておくこと。

例えば、アイコン画像がアップロードされていれば、アイコン画像を、なければ、デフォルトのアイコン画像を表示させたい場合は、

<img src={"/img/user_icon/" + (this.state.icon_image !== null ? "useruploadicon.jpg" : "defaulticon.jpg")} />

としておく。

もっと細かくネストしたい場合は、三項演算子をネストします。

<div>
{
  this.state.isClicked === true ?
    this.state.isChecked === true ?
      クリックして、かつ、チェックしています
    :
      クリックしましたが、チェックはしてません
  :
    クリックしてません
}
</div>

参考リンク
React.jsの知っておいて損はないTips - Qiita


■class属性をタグ内に書く場合は、classNameと書くこと

<div class="test"></div>

じゃなくて、

<div className="test"></div>

という風にする。


■this.State内の配列の一部を変更する方法

constructor(props){
  super(props);
  this.state = {
    frults: ["apple", "orange", "grape"]
  };
}

fruits_change(){
  var copy_fruits_array = this.state.fruits.slice();
  copy_fruits_array[0] = 'banana';
  this.setState({fruits: copy_fruits_array });
} 

参考リンク
ReactのsetStateで配列の一部を変更する - Qiita


■this.state内にデータを入れると自動的にサニタイズしてくれる
例えば、チャット機能など作る際に、ユーザーが、

<script>alert("hello");</script>

などのプログラムを書くと、プログラムが実行してしまいhelloと書かれたalertが表示され、セキュリティ的に危険なので、普通だったら、HTMLサニタイズするプログラムを書かなくてはならないが、Reactの場合は、this.stateに入れた時点で自動的に行ってくれるようだ。

Reactで親コンポーネントから子コンポーネントへstateを渡す

コンポーネントのstateの状態に応じて、子コンポーネントの状態を切り替えたいと思います。
今回はサンプルプログラムとして、clickボタンを押すたびに、ONとOFFの表示が交互に切り替わるプログラムを書いてみたいと思います。

コンポーネントのstateの状態が変化するたびに、子コンポーネントの内容も切り替わっています。

コンポーネント

import React from 'react';
import {render} from 'react-dom';
import Child from './Child';

class Parent extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      flug: false;
    };
  }

  handleClick(){
    let state = this.state.flug;
    this.setState({flug: !state});
  }

  render(){
    return(
    <div>
      <Child isClicked={this.state.flug} />
      <button onClick={() => this.handleClick} >click</button>
    </div>
    );
  }
}

render(
<Parent />,
document.getElementById("root")
);


コンポーネント

import React from 'react';

class Child extends React.Component{
  constructor(props){
    super(props);
  }

  render(){
    if(this.props.isClicked === true){
       return(<div>ON</div>);
    }else{
      return(<div>OFF</div>);
    }
  }
}

export default Child;


参考リンク
Reactで親から子へstate/メソッドをpropsで渡す時の考え方 - shibe23のはてなブログ
[react]子のコンポーネントから親のstateを変更する方法 - Qiita
逆に子コンポーネントから親コンポーネントのstateを変更させる方法について書かれています。

JavaScriptで何番目のclass属性でイベントが発生したのかインデックスを取得する

開発環境は、Windows 10 Pro(64bit)。

例えば、いいねボタンなどの複数のclass属性(今回はlike_btn)がある中で実際にクリックしたボタンのみを、色づけしたい、あるいは色づけを解除したい場合、jQueryのindexメソッドのようなものを、生のJavaScriptで行うにはどうすればいいのか、調べたのでメモ。コールバック関数clickedとして以下のコードを書く。

clicked(e){
  let doc = document.getElementsByClassName('like_btn');
  doc = [].slice.call(doc);
  let idx = doc.indexOf(e.target);
  console.log(idx);
}

Reactの場合は、e.targetの部分を、e.currentTargetに変える。console.log(e)で中身を見ていて発見した。


getElementsByClassNameで、HTMLCollectionのオブジェクトを取得し、それを、sliceメソッドを用いて、配列としてコピーしたいが、できないので、callメソッドを用いて、裏技的にHTMLCollectionオブジェクトを配列に変換して、その配列にe.targetを引数に指定したindexOfメソッドを用いると取得できる。


参考リンク
Array.prototype.indexOf() - JavaScript | MDN

ReactでBootstrapを使う

開発環境は、Windows 10 Pro(64bit) + npm 6.4.1。

https://react-bootstrap.github.io/

npm install react-bootstrap bootstrap

htmlファイルのheadタグ部分に、

<link
  rel="stylesheet"
  href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
  integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
  crossorigin="anonymous"
/>

を追加する。CSSファイルを入れないと、デザインが反映されません。
自分独自のCSSファイルは、上記のCSSファイルの後に読み込むこと。

Reactのファイル内に

import Button from 'react-bootstrap/Button';

// renderのreturn内にて。
<ButtonToolbar>
  <Button variant="primary">Primary</Button>
</ButtonToolbar>

を追加する。

※注意点
作っている途中に、react-bootstrapを導入したら、リセットCSSが原因で、それまで書いていたデザインが崩れてしまった。できれば、最初から、導入すること。

CakePHP3でReactを利用する

開発環境は、Windows 10 Pro(64bit) + CakePHP 3.7.8。

qiita.com
ここの記事を参考にして、CakePHPのルートディレクトリにて、
必要なモジュールをインストールしたが、bundle.jsを作成する段階でエラーが発生した。そこで、エラーメッセージに書いてある通りに修正すると、何とか動作した。(babelify@8をインストールした後に、babel-coreをインストールすると、うまく動作した。)


・PagesController.php内に、root(),a(),b()アクションをそれぞれ作る。

・/src/Template/Pages内に、a.ctp,b.ctp,root.ctpを作成する。

・/config/routes.phpを開き、Router::scope('/',~の中に追加する。
$routes->connect('/'~の部分は、actionをrootに書き換える。
routes.php

<?php
    $routes->connect('/', ['controller' => 'Pages', 'action' => 'root']);

    $routes->connect('/a', ['controller' => 'Pages', 'action' => 'a']);

    $routes->connect('/b', ['controller' => 'Pages', 'action' => 'b']);
?>

・エントリーポイントになるjavascriptファイルを作成して、以下のコードを書く。
entry.js

import React from 'react';
import {render} from 'react-dom';
import {BrowserRouter, Link, Route} from 'react-router-dom';

function root(){
  return <h1>root</h1>;
}

function a(){
  return <h1>a</h1>;
}

function b(){
  return <h1>b</h1>;
}

class Main extends React.Component{
  render(){
          return(
           <div>
           <BrowserRouter>
          <ul>
            <li><Link to="/">root</Link></li>
            <li><Link to="/a">a</Link></li>
            <li><Link to="/b">b</Link></li>
          </ul>
          <Route exact path="/" component={root} />
          <Route path="/a" component={a} />
          <Route path="/b" component={b} />
        </BrowserRouter>
        </div>
        );
  }
}

render(<Main />, document.getElementById("root"));

default.ctp

<body>
<div id="root"></div>
<script type="text/javascript" src="/js/bundle.js?_=<?= time() ?>"></script>
</body>

・これによって初回アクセス時は普通にhttpでアクセスし、後からは、リンクをクリックすると、ページ遷移をすることなく、動的に、URLを変化させながら、コンテンツの中身も変わる。

コントローラ側からJSON形式で取得する

entry.js

import React from 'react';
import {render} from 'react-dom';
import {BrowserRouter, Link, Route} from 'react-router-dom';

class Main extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      result: ''
    }
  }

  componentDidMount(){
    let url = location.pathname;
    this.readPage(url);
  }

  readPage(url){
    let this_ = this;
     fetch(url,{
      method: 'GET',
      headers: new Headers({ 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest'})
    }).then( function(res){
      return res.json();
    }).then( function(data){
      let test = data.result.message;
      this_.setState({result: test});
    }); 
  }

  render(){
     return(
       <div>
         <BrowserRouter>
           <ul>
            <li><Link to="/" onClick={() => (this.readPage("/"))}>root</Link></li>
            <li><Link to="/a" onClick={() => (this.readPage("/a")}>a</Link></li>
            <li><Link to="/b" onClick={() => (this.readPage("/b")}>b</Link></li>
          </ul>
          <Route exact path="/" render={() => (<h1>{this.state.result}</h1>)} />
          <Route path="/a" render={() => (<h1>{this.state.result}</h1>)} />
          <Route path="/b" render={() => (<h1>{this.state.result}</h1>)} />
        </BrowserRouter>
      </div>
    );
  }
}

render(<Main />, document.getElementById("root"));

コンポーネント内でcomponentDidMountメソッドを利用することで、指定のURLにアクセスした段階で、現在のURLのデータを読み込ませて、表示させています。これをしないと、リンクをクリックしない限り、データを読み込もうとはしません。
ja.reactjs.org


PagesController.php

<?php
  public function initialize(){
    parent::initialize();
  }

  public function root(){
    if($this->request->is(["ajax","get"])){
      $data = ["message" => "root page"];
      $this->set(['result' => $data, '_serialize' => ['result']]);
      return;
    }
  }

  public function a(){
    if($this->request->is(["ajax","get"])){
      $data = ["message" => "a page"];
      $this->set(['result' => $data, '_serialize' => ['result']]);
      return;
    }
  }

  public function b(){
    if($this->request->is(["ajax","get"])){
      $data = ["message" => "b page"];
      $this->set(['result' => $data, '_serialize' => ['result']]);
      return;
    }
  }
?>


参考リンク
React Router v4 でルーティング先の component に Props を渡す方法 - ngzmのブログ
【ajaxを卒業したい】rails+jqueryをreactで書き換えてみた #1 - Qiita
React.jsのComponent Lifecycle - Qiita

ffmpegを利用する

開発環境は、Windows10 Pro(64bit)。

動画から一部のフレームのサムネイル画像を作成したくて、ffmpegを利用してみることにしました。

1.公式サイトへアクセスする
https://www.ffmpeg.org/

2.左サイドバーのDownloadと書かれたリンクをクリック
f:id:k01ken:20190528061412p:plain

3.Get the packagesと書かれた欄のWindowsのアイコンにマウスカーソルを合わせて、Windows Buildsのリンクをクリック
f:id:k01ken:20190528061625p:plain

4.Versionは現時点で4.1.3を選択し、ArchitectureはWindows 64-bitを選択し、Linkingはstaticを選択し、Download Buildボタンをクリックし、zipファイルをダウンロードする
f:id:k01ken:20190528061912p:plain

5.ダウンロードしたzipファイルを解凍し、ファイル内へアクセスし、bin,doc,presetsディレクトリが直下にあるディレクトリの名前をffmpegに変更して、Cドライブ直下へ移動させる。

6.ffmpegディレクトリ内のbinディレクトリへのパス(今回はc:\ffmpeg\bin)を、Windowsの開発環境PATH内に設定する
k01ken.hatenablog.com

7.コマンドプロンプトを起動し、

ffmpeg -version

と入力し、バージョン情報が表示されたら、正しくパスが通っている。

8.Cドライブ直下に、ffmpeg_testというディレクトリを作成し、その中に、サムネイルを作成したい動画(名前は一応、input.mp4)を入れる。
※Cドライブ直下に、動画を設置すると、管理者権限でコマンドプロンプトを起動しないと、動作が失敗するので注意。

9.コマンドプロンプトを起動し、カレントディレクトリを上記のffmpeg_testにして、以下のコマンドを入力。

ffmpeg -i input.mp4 -f image2 -ss 120 -vframes 1 -s 426x240 thumbnail.jpg

成功すれば、動画の120秒の位置のフレームのthumbnail.jpgが、幅426px、高さ240pxで、同じディレクトリ内に作成されているはずです。

PHP経由で実行してみる

PHPからffmpegを実行して動作させたいと思います。といっても、単に、PHPのexec関数を使うだけです。

<?php
$output = '';
$status = '';
$command = 'ffmpeg -i input.mp4 -f image2 -ss 120 -vframes 1 -s 426x240 thumbnail.jpg';
exec($command, $output, $status);

if($status === 0){
  echo '成功しました';
}else{
  echo '失敗しました';
}
?>