React + Redux で Web API を呼び出す

React + Redux で SPA を作成するとき避けられないのが、 Web API の呼び出し。 というか、Web API を呼び出さないなんて、まず考えられない。

Web API の呼び出しでは、redux-thunk を使う。

github.com

Redux のミドルウェアで、 Action Creator でオブジェクトだけでなく関数も返せるようになる。 HTTP リクエストを送るやつじゃない。 こいつを上手く使うことで、 Redux のレールの上でなんとか Web API を呼び出せた。

まず API サーバーは express を使って作成。 単純化するために API は GET のみ。 テンプレートエンジンを使うまでもないので、 HTML は静的ファイルで配信する。

var express = require("express");

// POST したデータをパースするために body-parser が別途必要
var bodyParser = require("body-parser");

var app = express();

// req.body をパースしてオブジェクトにするミドルウェアを使う
app.use(bodyParser.json());

// 静的なファイルを配信するミドルウェアを使う
app.use(express.static("public"));

var players = [
  { number: 10, name: "Kagawa" },
  { number: 4, name: "Honda" },
  { number: 5, name: "Nagatomo" },
  { number: 9, name: "Okazaki" }
];

app.get("/api/players", function(req, res) {
  res.send(players);
});

var server = app.listen(3000, function() {
  var host = server.address().address;
  var port = server.address().port;

  console.log("Listening at http://%s:%s", host, port);
});

HTML はこれだけ。

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

クライアント側の JSX コード。 ポイントは関数を返す Action Creator と、 取得の開始・成功・失敗でアクションを分けているところ。 Action Cretor が返した関数は thunk ミドルウェアに処理される。 HTTP クライアントは SuperAgent を使ってみた。

var React = require("react");
var ReactDOM = require("react-dom");

var createStore = require("redux").createStore;
var applyMiddleware = require("redux").applyMiddleware; 
var combineReducers = require("redux").combineReducers;

var Provider = require("react-redux").Provider;
var connect = require("react-redux").connect;

var thunk = require("redux-thunk");

// 大人の事情で ES2015 が使えないので、
// map や Object.assign を lodash で代用
var _ = require("lodash");

// HTTP クライアントは SuperAgent を使う
var request = require("superagent");

// thunk ミドルウェアを使う
var createStoreWithMiddleware = applyMiddleware(thunk)(createStore);

// アクションを定義
var START_FETCH_PLAYERS = "START_FETCH_PLAYERS";
var SUCCESS_FETCH_PLAYERS = "SUCCESS_FETCH_PLAYERS";
var ERROR_FETCH_PLAYERS = "ERROR_FETCH_PLAYERS";

// プレーヤー一覧の取得開始を表すアクション
function startFetchPlayers() {
  return {
    type: START_FETCH_PLAYERS
  };
}

// プレーヤー一覧の取得成功を表すアクション
function successFetchPlayers(result) {
  return {
    type: SUCCESS_FETCH_PLAYERS,
    players: result
  };
}

// プレーヤー一覧の取得失敗を表すアクション
function errorFetchPlayers() {
  return {
    type: ERROR_FETCH_PLAYERS
  };
}

// Web API を呼び出す非同期アクション
function fetchPlayersAsync() {
  return function(dispatch) {
    dispatch(startFetchPlayers());

    request.get("/api/players")
      .end(function(err, res) {
        if (err) {
          dispatch(errorFetchPlayers());
        } else {
          dispatch(successFetchPlayers(res.body));
        }
      });
  };
}

// プレーヤーの状態を操作する Reducer
function playersReducer(state, action) {
  switch (action.type) {
    case SUCCESS_FETCH_PLAYERS:
      return action.players;
    case ERROR_FETCH_PLAYERS:
      return [];
    default:
      return state || [];
  }
}

// 進捗の状態を操作する Reducer
function progressReducer(state, action) {
  state = state || false;
  switch (action.type) {
    case START_FETCH_PLAYERS:
      return true;
    case SUCCESS_FETCH_PLAYERS:
      return false;
    case ERROR_FETCH_PLAYERS:
      return false;
    default:
      return state;
  }
}

// プレーヤーの行を表示するコンポーネント
var Player = React.createClass({
  render: function() {
    return (
      <tr>
        <td>{this.props.player.number}</td>
        <td>{this.props.player.name}</td>
      </tr>
    );
  }
});

// プレーヤー一覧を表示するコンポーネント
var Players = React.createClass({
  players: function() {
    return _.map(this.props.players, function(p) {
      return <Player player={p} />
    });
  },

  render: function() {
    return (
      <table>
        <thead>
          <tr>
            <th>Number</th>
            <th>Name</th>
          </tr>
        </thead>
        <tbody>
          {this.players()}
        </tbody>
      </table>
    );
  }
});

// 進捗を表示するコンポーネント
var Progress = React.createClass({
  render: function() {
    if (this.props.progress) {
      return (
        <div>取得中...</div>
      );
    } else {
      return (
        <div></div>
      );
    }
  }
});

var _MyApp = React.createClass({
  reload: function() {
    this.props.dispatch(fetchPlayersAsync());
  },

  render: function() {
    return (
      <div className="wrapper">
        <button onClick={this.reload}>Reload</button>
        <Progress progress={this.props.progress}/> 
        <Players players={this.props.players}/>
      </div>
    );
  }
});

// connect でラップし、ストアの状態を props に受け取れるようにする
var MyApp = connect(function(state) {
  return {
    players: state.players,
    progress: state.progress,
  };
})(_MyApp);

// ストアに渡す Reducer を作成
var rootReducer = combineReducers({
  players: playersReducer,
  progress: progressReducer
});

// ミドルウェアを組み込んだストアを作成
var store = createStoreWithMiddleware(rootReducer);

ReactDOM.render((
  <Provider store={store}>
    <MyApp />
  </Provider>
  ), document.getElementById("content")
);

Web API を呼び出して結果を表示するだけなのに、Redux のレール上でやろうとしたら、 思いのほかメンドかった。 この程度のサンプルだからそう感じただけで、 規模が大きいアプリになると恩恵を感じるようになるのかもな。 そうだといいな。

React で使う HTTP クライアント候補として SuperAgent を試してみた

React と Redux には HTTP クライアントは含まれていない。 別途ライブラリを使う必要がある。 Web API を呼び出すためだけに jQuery を使うのはバカげているから、 専用のライブラリがいいね。

Browserify でビルドするから、npm にあるライブラリを使えるな。 ってことで、有名どころの SuperAgent を試してみる。

github.com

とはいえ、SuperAgent を試そうにも肝心の API サーバーが無いと話にならない。

まずは express をインストール。

npm install --save express

単純な API サーバーをこしらえてみた。

// server.js
var express = require("express");

// POST したデータをパースするために body-parser が別途必要
var bodyParser = require("body-parser");

var app = express();

// req.body をパースしてオブジェクトにするミドルウェアを使う
app.use(bodyParser.json());

var players = [
  { number: 10, name: "Kagawa" },
  { number: 4, name: "Honda" },
  { number: 5, name: "Nagatomo" },
  { number: 9, name: "Okazaki" }
];

app.get("/", function(req, res) {
  res.send("Hello Express");
});

app.get("/players", function(req, res) {
  res.send(players);
});

app.post("/players", function(req, res) {
  players.push({ 
    number: req.body.number,
    name: req.body.name
  });
  res.sendStatus(200);
});

app.get("/players/:number", function(req, res, next) {
  var targets = players.filter(function(p) {
    return p.number.toString() === req.params.number;
  });
  if (0 < targets.length) {
    res.send(targets[0]);
  } else {
    next();
  }
});

var server = app.listen(3000, function() {
  var host = server.address().address;
  var port = server.address().port;

  console.log("Listening at http://%s:%s", host, port);
});

express 初めて使ったけど、RubySinatra みたいに分かりやすい。 ・・・と思っていたよ。POST に対応するまでは。 POST したデータをパースするのに body-paraser が別途必要ってどうなのさ。 Sinatra みたいに params から取得できればいいのに。

気を取り直して、SuperAgent を試すとしよう。

npm でインストール。

npm install --save superagent

SuperAgent を使って、Web API を呼び出すコードを書く。

// client.js
var request = require("superagent");

request.get("http://localhost:3000/players")
  .end(function(err, res) {
    if (err) {
      console.log(err);
    } else {
      console.log(res.body);
    }
  });

request.post("http://localhost:3000/players")
  .send({ number: 7, name: "Shibasaki" })
  .end(function(err, res) {
    if (err) {
      console.log(err);
    } else {
      console.log(res.status);
    }
  });

request.get("http://localhost:3000/players/7")
  .end(function(err, res) {
    if (err) {
      console.log(err);
    } else {
      console.log(res.body);
    }
  });

先ほど作った API サーバーを起動し、

$ node server.js
Listening at http://:::3000

Web API を呼び出せることを確認。

$ node client.js
[ { number: 10, name: 'Kagawa' },
  { number: 4, name: 'Honda' },
  { number: 5, name: 'Nagatomo' },
  { number: 9, name: 'Okazaki' } ]
200
{ number: 7, name: 'Shibasaki' }

SuperAgent シンプルで悪くない。

メソッドチェインの形で POST するデータや追加のヘッダーを指定するインタフェースはちょっと変わってるな。 すぐ慣れたけど。 いや、end に渡すコールバックの第一引数がエラーなのにはまだ違和感残ってる。レスポンスが先であるべきじゃないかな。 Promise じゃないのもね・・・。

ただ、周辺ライブラリも揃っているのも魅力的。 Promise も、superagent-promise や superagent-bluebird-promise あたりを組み合わせる手があるにはあるか。

BLUE JAM

アサデスでだいぶ前に紹介されて、 いつか行こうと思っていたパン屋『BLUE JAM』にようやく行ってみた。 場所は地下鉄七隈線次郎丸駅から南に歩いて約15分。 結構遠かった。

10時くらいに行ったら色々置いてあったので、 テレビで紹介されていたお目当てのクロワッサンと、 他に気になるやつを 3 種類購入。

クロワッサンはパリパリで、油っこくなく非常に軽い。 何個でもいけそう。毎朝食べても飽きが来なさそう。 さつまいもと挽肉のカレーパンは、さつまいもの甘さとカレーのスパイシーさが互いに引き立てあっていて美味。

きびクリームのパンはカドの無い素朴な甘さ。 生地がすごい弾力で噛み応えがあった。

数量限定のメロンパンもレベル高かった。 なんていうか、生地の密度が濃い。

駅から遠かったのと、値段の割りにサイズが小さかったけど、 味は申し分なかった。 人気なのも納得。 ただ車がないと買いに行くのはシンドイな。

関連ランキング:パン | 次郎丸駅橋本駅賀茂駅

React でも Bootstrap のコンポーネントを使う

React で SPA を実装する場合も、 デザインは Bootstrap のお世話になりたい。 クラスを指定するだけで使えるコンポーネントは普通に使えそうだけど、 Tooltip や Dropdown や Modal のような、 jQuery に依存したコンポーネントは React でそのまま使えないのが残念。

そこで Bootstrap のコンポーネントを React コンポーネントとして実装した、 React-Bootstrap を試してみた。

github.com

JavaScript の部分を jQuery から React に実装し直したシロモノなので、 別途 Bootstrap の CSS は必要。

Bootstrap を使って自分がよくやるレイアウトを React-Bootstrap で再現してみた。

var React = require("react");
var ReactDOM = require("react-dom");

// React-Bootstrap のコンポーネントを読み込む
var Navbar = require("react-bootstrap").Navbar;
var Nav = require("react-bootstrap").Nav;
var NavItem = require("react-bootstrap").NavItem;
var NavDropdown = require("react-bootstrap").NavDropdown;
var MenuItem = require("react-bootstrap").MenuItem;
var Grid = require("react-bootstrap").Grid;
var Row = require("react-bootstrap").Row;
var Col = require("react-bootstrap").Col;
var Table = require("react-bootstrap").Table;
var Tabs = require("react-bootstrap").Tabs;
var Tab = require("react-bootstrap").Tab;
var Button = require("react-bootstrap").Button;
var Modal = require("react-bootstrap").Modal;
var Input = require("react-bootstrap").Input;

// Navbar を使ってヘッダーを作成
var Header = React.createClass({
  render: function() {
    return (
      <Navbar>
        <Navbar.Header>
          <Navbar.Brand>
            <a href="#">React-Bootstrap Sample</a>
          </Navbar.Brand>
        </Navbar.Header>

        <Nav pullRight>
          <NavDropdown eventKey={1} title="Settings" >
            <MenuItem eventKey={1.1}>Profile</MenuItem>
            <MenuItem divider />
            <MenuItem eventKey={1.2}>Sign out</MenuItem>
          </NavDropdown>
        </Nav>
      </Navbar>
    );
  }
});

// Nav を使ってメニューを作成
var SideNav = React.createClass({
  render: function() {
    return (
      <Nav bsStyle="pills" activeKey={1} stacked>
        <NavItem eventKey={1} href="#">Home</NavItem>
        <NavItem eventKey={2} href="#">About</NavItem>
        <NavItem eventKey={3} href="#">Contact</NavItem>
      </Nav>
    );
  }
});

// 登録フォームを作成
var MemberForm = React.createClass({
  render: function() {
    return (
      <form>
        <Input type="text" label="Number" />
        <Input type="text" label="Name" placeholder="Name text" />
        <Input type="select" label="Position">
          <option value="FW">FW</option>
          <option value="MF">MF</option>
          <option value="DF">DF</option>
          <option value="GK">GK</option>
        </Input>
      </form>
    );
  }
});

// モーダルを表示するボタンを作成
var AddMember = React.createClass({
  getInitialState: function() {
    return {
      showModal: false
    }
  },
  
  close: function() {
    this.setState({ showModal: false });
  },

  open: function() {
    this.setState({ showModal: true });
  },

  render: function() {
    return (
      <div>
        <Button onClick={this.open}>Add</Button>

        <Modal show={this.state.showModal} onHide={this.close}>
          <Modal.Header closeButton>
            <Modal.Title>Add Member</Modal.Title>
          </Modal.Header>
          <Modal.Body>
            <MemberForm />
          </Modal.Body>
          <Modal.Footer>
            <Button bsStyle="primary" onClick={this.close}>Save</Button>
            <Button onClick={this.close}>Cancel</Button>
          </Modal.Footer>
        </Modal>
      </div>
    );
  }
});

// Table を使ってメンバー一覧を作成
var Members = React.createClass({
  render: function() {
    return (
      <Table striped>
        <thead>
          <tr>
            <th>Number</th>
            <th>Name</th>
            <th>Position</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>10</td>
            <td>Kagawa</td>
            <td>MF</td>
          </tr>
          <tr>
            <td>4</td>
            <td>Honda</td>
            <td>MF</td>
          </tr>
          <tr>
            <td>5</td>
            <td>Nagatomo</td>
            <td>DF</td>
          </tr>
          <tr>
            <td>9</td>
            <td>Okazaki</td>
            <td>FW</td>
          </tr>
        </tbody>
      </Table>
    );
  }
});

// Grid と Row と Col を使ってグリッドレイアウト
var MyApp = React.createClass({
  render: function() {
    return (
      <div class="wrap">
        <Header />

        <Grid>
          <Row>
            <Col md={3}>
              <SideNav />
            </Col>
            <Col md={9}>
              <AddMember />

              <Tabs defaultActiveKey={1}>
                <Tab eventKey={1} title="Start">
                  <Members />
                </Tab>
                <Tab eventKey={2} title="Sub"></Tab>
                <Tab eventKey={3} title="Other"></Tab>
              </Tabs>
            </Col>
          </Row>
        </Grid>
      </div>
    );
  }
});

ReactDOM.render(
  <MyApp />,
  document.getElementById("content")
);

Navbar が内部で container を持っているから、ヘッダーやフッター以外では使えないな。 Navbar は自分でコンポーネントを書くことにしよう。 Dropdown や Modal はそのまま使えそうだ。 React-Bootstrap 結構使えるかもしれないな。

React + Redux でもルーティング

react-router でルーティングを試してみたけど、 実際のアプリは React + Redux で開発する予定。 その際、react-router のみではなく redux-router も一緒に使う。

redux-router は react-router の Redux バインディングルーターの状態を Redux のストアで管理できるようになる。

github.com

redux-router は npm でインストール。 redux-router が依存している history も併せてインストールしておく。

npm install --save history redux-router

react-router のサンプルを Redux と redux-router を使って書き変えてみた。

var React = require("react");
var ReactDOM = require("react-dom");

var Link = require("react-router").Link;
var Route = require("react-router").Route;
var IndexRoute = require("react-router").IndexRoute;

var combineReducers = require("redux").combineReducers;
var createStore = require("redux").createStore;
var compose = require("redux").compose;

var Provider = require("react-redux").Provider;
var connect = require("react-redux").connect;

var reduxReactRouter = require("redux-router").reduxReactRouter;
var routerStateReducer = require("redux-router").routerStateReducer;
var createElement = require("redux-router").createElement;
var ReduxRouter = require("redux-router").ReduxRouter;

// HTML だけで完結したいので hash history を使う
var createHashHistory = require("history").createHashHistory;

// ユーザー一覧のユーザーを表示するコンポーネント
var User = React.createClass({
  render: function() {
    return (
        <tr>
          <td>
            <Link to={"/users/" + this.props.user.id} query={{name: this.props.user.name}}>
              {this.props.user.name}
            </Link>
          </td>
        </tr>
    );
  }
});

// ユーザー一覧を表示するコンポーネント
var Users = React.createClass({
  getUsers: function() {
    return this.props.users.map(function(u) {
      return <User user={u} />;
    });
  },

  render: function() {
    return (
      <div className="users">
        <h1>ユーザー一覧</h1>
        <table>
          <thead>
            <tr>
              <th>名前</th>
            </tr>
          </thead>
          <tbody>
            {this.getUsers()}
          </tbody>
        </table>
      </div>
    );
  }
});

// ユーザー詳細を表示するコンポーネント
var UserDetail = React.createClass({
  render: function() {
    // パスに埋め込まれたパラメータは params から取得できる
    var userId = this.props.params.id;

    // ユーザーを取得
    var user = this.props.users[userId - 1];

    // 名前はクエリパラメータで受け取ったものを表示してみる
    return (
      <div className="user-detail">
        <h1>ユーザー詳細</h1>
        <dl>
          <dt>ID</dt>
          <dd>{user.id}</dd>
          <dt>名前</dt>
          <dd>{this.props.location.query.name}</dd>
          <dt>年齢</dt>
          <dd>{user.age}</dd>
        </dl>
      </div>
    );
  }
});

var About = React.createClass({
  render: function() {
    return (
      <div className="about">
        <h1>このサンプルについて</h1>
        <p>react-router のサンプルです。</p>
      </div>
    );
  }
});

// コンテナコンポーネント
var _MyApp = React.createClass({
  render: function() {
    return (
      <div>
        <ul>
          <li><Link to="/users">ユーザー一覧</Link></li>
          <li><Link to="/about">このサンプルについて</Link></li>
        </ul>
        <div>
          {this.props.children && React.cloneElement(this.props.children, {
            users: this.props.users
          })}
        </div>
      </div>
    );
  }
});

// connect でラップして、
// ストアから取り出したユーザー一覧を MyApp の props に渡せるようにする
var MyApp = connect(function(state) {
  return {
    users: state.users
  };
})(_MyApp);

// ユーザー一覧を操作する Reducer
// といってもユーザー一覧の初期データを返すだけ
function usersReducer(state, action) {
  return state || [
    { id: 1, name: "香川", age: 26 },
    { id: 2, name: "本田", age: 29 },
    { id: 3, name: "長友", age: 29 },
    { id: 4, name: "岡崎", age: 29 },
    { id: 5, name: "内田", age: 28 },
    { id: 6, name: "清武", age: 25 }
  ];
}

// ストアに渡す Reducer を作成
var reducer = combineReducers({
  router: routerStateReducer,
  users: usersReducer
});

// 唯一のストアを作成
// 履歴を扱えるように reduxReactRouter ミドルウェアを適用
var store = compose(
  reduxReactRouter({ "createHistory": createHashHistory })
)(createStore)(reducer);

// ReduxRouter を使ってルーティングを定義
ReactDOM.render((
  <Provider store={store}>
    <ReduxRouter>
      <Route path="/" component={MyApp}>
        <IndexRoute component={Users} />
        <Route path="users" component={Users} />
        <Route path="users/:id" component={UserDetail} />
        <Route path="about" component={About}/>
      </Route>
    </ReduxRouter>
  </Provider>
  ),
  document.getElementById("content")
);

前回、遷移先のコンポーネントに props を渡す方法を習得したので、 状態を Redux のストアに一元管理できて良い感じ。

react-router で遷移先のコンポーネントに props を渡す

ネストしたルートに遷移するとき、 遷移先のルートに対応するコンポーネントが、親コンポーネントの props.children にセットされている。

ここで props.children をそのまま表示せずに、 React.cloneElement で props.children を複製すると、 その際に props を渡せる。

もっとスマートな方法が提供されていたらいいんだけど、 今のところこの方法しか無さそう。

何はともあれ、ルートコンポーネントのみに状態を持たせ、子コンポーネントはステートレスにできた。

var React = require("react");
var ReactDOM = require("react-dom");
var ReactRouter = require("react-router");
var Router = ReactRouter.Router;
var Link = ReactRouter.Link;
var Route = ReactRouter.Route;
var IndexRoute = ReactRouter.IndexRoute;

var User = React.createClass({
  // ID は URL に埋め込む。
  // クエリパラメータで名前を渡す。
  render: function() {
    return (
        <tr>
          <td>
            <Link to={"/users/" + this.props.user.id} query={{name: this.props.user.name}}>
              {this.props.user.name}
            </Link>
          </td>
        </tr>
    );
  }
});

var Users = React.createClass({
  getUsers: function() {
    return this.props.users.map(function(u) {
      return <User user={u} />;
    });
  },

  render: function() {
    return (
      <div className="users">
        <h1>ユーザー一覧</h1>
        <table>
          <thead>
            <tr>
              <th>名前</th>
            </tr>
          </thead>
          <tbody>
            {this.getUsers()}
          </tbody>
        </table>
      </div>
    );
  }
});

var UserDetail = React.createClass({
  render: function() {
    // パスに埋め込まれたパラメータは params から取得できる
    var userId = this.props.params.id;

    // ユーザーを取得
    var user = this.props.users[userId - 1];

    // 名前はクエリパラメータで受け取ったものを表示してみる
    return (
      <div className="user-detail">
        <h1>ユーザー詳細</h1>
        <dl>
          <dt>ID</dt>
          <dd>{user.id}</dd>
          <dt>名前</dt>
          <dd>{this.props.location.query.name}</dd>
          <dt>年齢</dt>
          <dd>{user.age}</dd>
        </dl>
      </div>
    );
  }
});

var About = React.createClass({
  render: function() {
    return (
      <div className="about">
        <h1>このサンプルについて</h1>
        <p>react-router のサンプルです。</p>
      </div>
    );
  }
});

var MyApp = React.createClass({
  getInitialState: function() {
    // ルートコンポーネントだけが状態を持つ
    return {
      users: [
        { id: 1, name: "香川", age: 26 },
        { id: 2, name: "本田", age: 29 },
        { id: 3, name: "長友", age: 29 },
        { id: 4, name: "岡崎", age: 29 },
        { id: 5, name: "内田", age: 28 },
        { id: 6, name: "清武", age: 25 }
      ]
    };
  },

  render: function() {
    // children の要素をクローンして描画する。
    // その際に props を渡す。
    return (
      <div>
        <ul>
          <li><Link to="/users">ユーザー一覧</Link></li>
          <li><Link to="/about">このサンプルについて</Link></li>
        </ul>
        <div>
          {this.props.children && React.cloneElement(this.props.children, {
            users: this.state.users
          })}
        </div>
      </div>
    );
  }
});

// Router や Route も React コンポーネント
ReactDOM.render((
  <Router>
    <Route path="/" component={MyApp}>
      <IndexRoute component={Users}/>
      <Route path="users" component={Users}/>
      <Route path="users/:id" component={UserDetail}/>
      <Route path="about" component={About}/>
    </Route>
  </Router>
  ),
  document.getElementById("content")
);

react-router を使って React でルーティング

SPA を作る上で実装したいのがルーティング。React を使ったアプリでのルーティング実装には react-router を使う。 今回からインストールは npm で。

npm install --save react-router

react-router を使って Master-Detail の簡単なサンプルを書いてみた。 react-router が提供する Router や Route は React コンポーネントなので、 DOM を組み立てる感覚でルーティングを定義できて面白い。

var React = require("react");
var ReactDOM = require("react-dom");
var ReactRouter = require("react-router");
var Router = ReactRouter.Router;
var Link = ReactRouter.Link;
var Route = ReactRouter.Route;
var IndexRoute = ReactRouter.IndexRoute;

// ユーザー情報を格納したストア
var initialUsers = [
  { id: 1, name: "香川", age: 26 },
  { id: 2, name: "本田", age: 29 },
  { id: 3, name: "長友", age: 29 },
  { id: 4, name: "岡崎", age: 29 },
  { id: 5, name: "内田", age: 28 },
  { id: 6, name: "清武", age: 25 }
];

var User = React.createClass({
  render: function() {
    return (
        <tr>
          <td>
            <Link to={"/users/" + this.props.user.id} query={{name: this.props.user.name}}>
              {this.props.user.name}
            </Link>
          </td>
        </tr>
    );
  }
});

var Users = React.createClass({
  getInitialState: function() {
    return {
      users: initialUsers
    };
  },

  getUsers: function() {
    return this.state.users.map(function(u) {
      return <User user={u} />;
    });
  },

  render: function() {
    return (
      <div className="users">
        <h1>ユーザー一覧</h1>
        <table>
          <thead>
            <tr>
              <th>名前</th>
            </tr>
          </thead>
          <tbody>
            {this.getUsers()}
          </tbody>
        </table>
      </div>
    );
  }
});

var UserDetail = React.createClass({
  render: function() {
    // パスに埋め込まれたパラメータは params から取得できる
    var userId = this.props.params.id;

    // ユーザーを取得
    var user = initialUsers[userId - 1];

    // 名前はクエリパラメータで受け取ったものを表示してみる
    return (
      <div className="user-detail">
        <h1>ユーザー詳細</h1>
        <dl>
          <dt>ID</dt>
          <dd>{user.id}</dd>
          <dt>名前</dt>
          <dd>{this.props.location.query.name}</dd>
          <dt>年齢</dt>
          <dd>{user.age}</dd>
        </dl>
      </div>
    );
  }
});

var About = React.createClass({
  render: function() {
    return (
      <div className="about">
        <h1>このサンプルについて</h1>
        <p>react-router のサンプルです。</p>
      </div>
    );
  }
});

var MyApp = React.createClass({
  render: function() {
    return (
      <div>
        <ul>
          <li><Link to="/users">ユーザー一覧</Link></li>
          <li><Link to="/about">このサンプルについて</Link></li>
        </ul>
        <div>
          {this.props.children}
        </div>
      </div>
    );
  }
});

// Router や Route も React コンポーネント
ReactDOM.render((
  <Router>
    <Route path="/" component={MyApp}>
      <IndexRoute component={Users}/>
      <Route path="users" component={Users}/>
      <Route path="users/:id" component={UserDetail}/>
      <Route path="about" component={About}/>
    </Route>
  </Router>
  ),
  document.getElementById("content")
);

ルーティングにマッチしたコンポーネントは props.children、 パスに埋め込まれたパラメータは props.params に格納される。

Link でクエリパラメータを渡せるけど、id やフラグといった、ちょっとした情報を渡す程度にとどめて置くのが無難。 すべての情報を渡すのは適してない。 そのため、id をもとにストアから詳細な情報を取り出す必要があった。

今回も「コンテナコンポーネントに状態をもたせて子コンポーネントはステートレスにする」設計をなんとか実装したかったが、 上手いやり方が思いつかず断念。react-router に精通すれば可能なんだろうか。 今後の課題だな。