はじめてのReact.js

過去にブクマしてたFlux関連の記事を一気読みする機会があったので、その流れで今までノータッチ状態だったReact.jsもさわってみました。 りぃさんのCodeGridの記事(これから始めるReact.js 全6回 | CodeGrid)が一段落ついたので、これを見つつアレンジしつつやってみます。

環境構築

今回作ったやつ

環境構築周りの説明が省かれててリポジトリ見るとシェルベースでコンパイルする感じになってたので、おれおれテンプレされがちであろうgulpベースで整えてみる。

package.json

{
  "name": "react_study",
  "version": "1.0.0",
  "devDependencies": {
    "babel-preset-es2015": "6.9.0",
    "babel-preset-react": "6.5.0",
    "babelify": "7.3.0",
    "browser-sync": "2.13.0",
    "browserify": "13.0.1",
    "gulp": "3.9.1",
    "react": "15.1.0",
    "react-dom": "15.1.0",
    "run-sequence": "1.2.1",
    "vinyl-source-stream": "1.1.0"
  }
}

ES2015 と Browserifyを採用。

.babelrc

{
  "presets": [
    "es2015",
    "react"
  ]
}

gulpfile.js

const gulp = require('gulp');
const browserify = require('browserify');
const babelify = require('babelify');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const browserSync = require('browser-sync');
const runSequence = require('run-sequence');

const proj = ['01', '02', '03', '04', '05', '06', '07'];

const defineProjTask = (name, task) => {
  const tasks = [];
  const defineTask = (dir) => {
    const taskName = name + ':'+dir;
    tasks.push(taskName);
    gulp.task(taskName, task(dir));
  };
  proj.forEach(defineTask);
  gulp.task(name, tasks);
};

defineProjTask('es:convert', (dir) => {
  return (cb) => {
    return browserify(dir + '/main.js', { debug: true })
    // .babelrcを書かない場合は以下のようにする
      // .transform(babelify,{presets: ["react","es2015"]})
      .transform(babelify)
      .bundle()
      .on("error", function (err) { console.log("Error : " + err.message); })
      .pipe(source('bundle.js'))
      .pipe(buffer())
      .pipe(gulp.dest(dir));
  };
});

defineProjTask('watch', (dir) => {
  return (cb) => {
    gulp.watch([dir + '/**/*.jsx', dir + '/main.js', dir + '/*/*.js'], () => {
      runSequence('es:convert:' + dir, browserSync.reload);
    })
  };
});

gulp.task(
  'serve', () => {
    browserSync({
      server: {
        baseDir: '.'
      }
    });
  }
);

gulp.task('default', () => {
  runSequence(
    'es:convert',
    'serve',
    'watch'
  );
});

サブディレクトリ単位でbrowserifyする感じで。これでgulpと打てば、コンパイルとソースの変更監視と画面が起動する。

一番簡単なサンプル

/01/index.html

<!DOCTYPE html>
<html>
<head lang="ja">
<meta charset="UTF-8">
</head>
<body>
<div id="container"></div>
<script src="bundle.js"></script>
</body>
</html>

/01/main.js

import React from "react";
import ReactDOM from "react-dom";
import Message from "./message.jsx";
import App from "./app.jsx";

ReactDOM.render(
  <App />,
  document.getElementById('container')
);

/01/app.jsx

import React from "react";
import ReactDOM from "react-dom";
import Message from "./message.jsx";

export default class App extends React.Component{
  render (){
    return (
      <div>
        <h1>簡単なサンプル</h1>
        <Message />
      </div>
    );
  }
}

継承の記述がJavaっぽくて良い感じ。ちなみにimport文で{}つければ以下のようも書ける。

import {Component} from "react";
export default class App extends Component{
  // ...
}

/01/message.jsx

import React from "react";

export default class Message extends React.Component {
  render() {
    return <div>おす、おらごくう</div>;
  }
}

これで以下のようになる。

react01

propsとstate

  • props
    • コンポーネント外から渡された、読込専用の値
  • state
    • コンポーネント内で保持できる、変化する値

React.jsでは「各コンポーネントは極力stateを保持せずに、できる限りpropsでデータを受け取るべき」というポリシーがある。

propsで表示

/02/app.jsx

import React from "react";
import Message from "./message.jsx";

export default class App extends React.Component{
  render (){
    return (
      <div>
        <h1>propsでメッセージを表示する</h1>
        <Message text="おまえはもう死んでいる"/>
      </div>
    );
  }
}

/02/message.jsx

import React from "react";

export default class Message extends React.Component {
  render() {
    const { text } = this.props;
    return <div>{text}</div>;
  }
}

値の受け取り方が独特。こうなる。

react02

stateで表示

/03/app.jsx

import React from "react";
import Message from "./message.jsx";

export default class App extends React.Component{

  constructor() {
    super();

    this.state = {
      val: 'やっほー'
    };
    this.changeMessage = this.changeMessage.bind(this);
  }

  changeMessage(ev) {
    this.setState({ val: ev.target.value });
  }

  render (){
    let { val } = this.state;
    return (
      <div>
        <h1>stateでメッセージを表示する</h1>
        <input type="text" onChange={this.changeMessage} value={val}/>
        <Message text={val}/>
      </div>
    );
  }
}

super()でスーパークラスのコンストラクタを実行しとかないとエラーがでるっぽい。 setState()でstateの変更を行う。 こうなる。

react03

onClickとかのイベントについては以下参照。

JSX記法

これ覚えるのが一番めんどくさそう。

JSX記法のつまづきポイント | CodeGrid

とりあえず {} で囲むとJSが書けるらしい

<div id="answer">
  { (bool) ? 'YES' : 'NO' }
</div>

複雑なやつは外側で書く。

render() {
  let el;
  if (bool) {
    el = <div>YES</div>;
  } else {
    el = <span>NO</span>;
  }
  return (
    <div>
      {el}
    </div>
  );
}

変換されると以下のようになる。

React.createElement(
  'div',
  { id: 'answer' },
  (bool) ? 'YES' : 'NO'
)

2語以上の属性値名はキャメルケースで書く

  • tabindex → tabIndex
  • maxlength → maxLength

あと、ループ処理とkey propsとか...CodeGridみる

TODOアプリ

04/app.jsx

import React from "react";
import AddTodo from "./add_todo.jsx";
import TodoList from "./todo_list.jsx";

export default class App extends React.Component{

  constructor() {
    super();

    this.state = {
      todo: '働く',
      todoList: ['寝る','起きる','休む','食べる']
    };
    this._onChange = this._onChange.bind(this);
    this._onAdd = this._onAdd.bind(this);
    this._onClose = this._onClose.bind(this);
  }

  _onChange(ev) {
    this.setState({ todo: ev.target.value });
  }

  _onAdd(ev) {
    ev.preventDefault();
    const todo = this.state.todo;
    if( !todo ) return;
    const todoList = this.state.todoList.slice();
    todoList.push(todo);
    this.setState({ todoList: todoList, todo:''});
  }

  _onClose(ev, idx){
    ev.preventDefault();
    const todoList = this.state.todoList.filter((item, _idx)=>{
      return _idx !== idx;
    });
    this.setState({ todoList: todoList});
  }

  render (){
    let {todo, todoList} = this.state;
    return (
      <div>
        <h1>React TODO</h1>
        <AddTodo onChange={this._onChange} onAdd={this._onAdd} value={todo}/>
        <TodoList list={todoList} onClose={this._onClose}/>
      </div>
    );
  }
}

あまりコンポーネントで持たないほうが望ましいというstateはルートコンポーネントで管理。 stateを操作する各種イベントのコールバックもルートで管理。 一見 onChangeが不要そうだけど、これないとinput要素がreadOnlyになってしまう。

04/add_todo.jsx

import React from "react";

export default class AddTodo extends React.Component {
  render() {
    const {
      onChange,
      onAdd,
      value
    } = this.props;
    return (
      <form onSubmit={onAdd}>
        <input type="text" onChange={onChange} value={value}/>
        <input type="submit" value="add"/>
      </form>
    );
  }
}

AddTodo.propTypes = {
  onChange: React.PropTypes.func.isRequired,
  onAdd: React.PropTypes.func.isRequired,
  value: React.PropTypes.string.isRequired
};

TODO追加用の入力フィールドを定義したコンポーネント。 AddTodo.propTypesはpropsの型チェックを実行時に行ってくれるもの。詳細は以下。

04/todo_list.jsx

import React from "react";

export default class TodoList extends React.Component {
  render() {
    const {
      list,
      onClose
    } = this.props;
    return (
      <ul>
        { list.map((d, idx) => {
          return <li key={idx}><input type="checkbox" onClick={(ev)=>{onClose(ev, idx)}}/>{d}</li>
        }) }
      </ul>
    );
  }
}

TodoList.propTypes = {
  list: React.PropTypes.arrayOf(React.PropTypes.string),
  onClose: React.PropTypes.func.isRequired
};

TODO一覧。

react04

次はりぃさんの新連載、Flux編を見ながらやってみたい。