React by Examples 第2回: React without Redux - 入力データを表示につなげる

ソースコードはこちら; netsphere / react-by-examples · GitLab

今回は、ユーザがWebブラウザ内でデータを追加できるようにする。まだ永続化はしない。


スクリーンショット

1行テキストの追加, [Done] ボタンで状態変更。表示モードの切り替え、を実装する。Example: Todo List | Redux ここの例を, Redux を使わないように書き換えたもの。

コンポーネントを跨ぐ状態管理。

コードを書く

まずは, エントリポイント (src/index.js ファイル). Redux を使う場合, <Provider> コンポーネントを呼び出す。我々のバージョンでは、特に前回と変わりない。

JavaScript
  1. ReactDOM.render(
  2. // Redux を使う場合, <Provider store={store}> で囲む
  3. // App コンポーネントを呼び出す.
  4. <App />,
  5. document.getElementById('root') );

今回の例では, 1画面に, 新しいアイテムの追加, リスト表示を並べる。コンポーネントはそれぞれ TodoEdit, TodoList と名付ける.

TodoEdit がアイテムを追加して, TodoList コンポーネントに表示するので, データがコンポーネント内で完結しない。表示するデータは App コンポーネントに保存することにする。

App クラスのコンストラクタで state を設定。

JavaScript
  1. class App extends React.Component
  2. {
  3. constructor(props) {
  4. super(props);
  5. this.state = {
  6. // view model
  7. todos: TodoModel.find_all(VisibilityFilters.SHOW_ALL),
  8. // config
  9. filter: VisibilityFilters.SHOW_ALL,
  10. };
  11. }

注意したいのは、state に保存するのは、あくまでも表示すべきデータだ、ということ。モデルデータのすべてではない。フロントエンドとバックエンドとの分離で、一部を切り取って取り出して表示するイメージ。

App#render() にて、必要なデータを各コンポーネントに渡す。App に状態を保存する関係で, ボタンが押されたなどに対応するアクションハンドラも App で定義する。それも渡してあげる。

JavaScript
  1. render() {
  2. return (<div className="App">
  3. <header>Todo List</header>
  4. <TodoEdit onAdd={this.onAdd} />
  5. <TodoList todos={this.state.todos} onFinish={this.onFinish}
  6. selectedFilter={this.state.filter}
  7. onChangeFilter={this.onChangeFilter} />
  8. </div> );
  9. }

入力フォーム

前回見たように、JSX の属性は、JSON オブジェクトのプロパティとして組み立てられる。これが, コンポーネントの constructor の props 引数として渡される。TodoEdit コンポーネントには onAdd 関数が渡ってくる。

JavaScript
[RAW]
  1. // See https://ja.reactjs.org/docs/forms.html
  2. class TodoEdit extends React.Component
  3. {
  4. // props: onAdd
  5. constructor(props) {
  6. // props は this.props としてアクセス可能.
  7. super(props);
  8. this.state = {
  9. submitEnabled: false,
  10. todoText: '' // 現に表示している内容
  11. };
  12. // 現代は, bind() よりも、アロー関数を使う.
  13. //this.handleSubmit = this.handleSubmit.bind(this);
  14. }

コメントアウト部分について。はるか昔の JavaScript にはアロー関数がなく, function 式しかなかった。this がダイナミックスコープなので, bind(this) によって this が指すオブジェクトを自クラスに固定しなければならなかった。アロー関数はレキシカルスコープなので、そのような手間は不要。

入力フォーム部分は、次のようにする。

JavaScript
[RAW]
  1. // submit() 内で this.state で値を取れるようにする.
  2. handleTodoTextChanged = (event) => {
  3. const value = event.target.value;
  4. this.setState( {
  5. todoText:value,
  6. submitEnabled: value.trim() !== '' } );
  7. }
  8. handleSubmit = (event) => {
  9. // サーバに送信しない。後の例で, 送信するようにする.
  10. event.preventDefault();
  11. const text = this.state.todoText.trim();
  12. if (!text)
  13. return;
  14. this.props.onAdd(text);
  15. this.setState( {todoText: ''} );
  16. }
  17. render() {
  18. return (<div>
  19. <form onSubmit={this.handleSubmit} >
  20. <input value={this.state.todoText} onChange={this.handleTodoTextChanged} />
  21. <button type="submit" disabled={this.state.submitEnabled ? '' : 'disabled'}>
  22. Add Todo</button>
  23. </form>
  24. </div> );
  25. }

まず、render() 内では, state の読み出ししかできない。input タグ値が変わる度に, ハンドラ handleTodoTextChanged を呼び出させる。いかにも重そうだが、クライアント側で完結するので、問題ない。

例として, リアルタイムで簡単な値の検査を行っている。メイルアドレスに限定、なども、この場所に記述すればよい。ここでは単に setState() で値を変更するだけで, render() 内で実際のボタンの有効/無効を設定する。

今回は, App コンポーネントにデータを格納しているので, handleSubmit ハンドラでは, App#onAdd() を呼び出すようにしている。

モデルの更新

App.js ファイルに戻る。レコードの作成に見立てた create() を呼び出す。

JavaScript
[RAW]
  1. // Redux の例では, actions/index.js 内で定義
  2. // state が App クラスにあるので、ここで定義する.
  3. onAdd = (text) => {
  4. TodoModel.create({
  5. text: text,
  6. completed: false
  7. });
  8. this.setState( {
  9. todos: TodoModel.find_all(this.state.filter),
  10. });
  11. }

モデルクラスで定義している。この例では、メモリ上に保持するだけで, まだ永続化は行っていない。

JavaScript
[RAW]
  1. static create(newItem) {
  2. const todoItem = {
  3. id: TodoModel.nextId,
  4. text: newItem.text.trim(),
  5. completed: newItem.completed };
  6. TodoModel.todos = [...TodoModel.todos, todoItem]
  7. ++TodoModel.nextId;
  8. }

... はスプレッド構文 Spread syntax で, 次のように書くと、要素を入れ替えたり追加したりできる。

[... iterableObj, '4', 'five', 6]

今回の例では, 単に Array#push() でもよいが。

React コンポーネントは入れ子にすることができる。行21で Footer コンポーネントを呼び出している。

JavaScript
[RAW]
  1. class TodoList extends React.Component
  2. {
  3. // props: todos, onFinish, selectedFilter, onChangeFilter
  4. constructor(props) {
  5. super(props);
  6. this.state = {}
  7. }
  8. // コンポーネントから別のコンポーネントを呼び出せる.
  9. render() {
  10. return (<div>
  11. {this.props.todos.map(todo => (
  12. <TodoListRow key={todo.id} {...todo}
  13. onClick={() => this.props.onFinish(todo.id)} />
  14. ))}
  15. <Footer selectedFilter={this.props.selectedFilter}
  16. onChangeFilter={this.props.onChangeFilter} />
  17. </div> );
  18. }
  19. }

TodoListRow(onClick: ...) 部分では、引数の数を調整している。Babel コンパイルで展開されたときに, JSONプロパティ値になる。関数式であればよいので、このようにアロー関数を書くことができる。

行18 で呼び出されている TodoListRow コンポーネントについて見てみよう。

単に表示するだけのコンポーネントについては, いちいちクラスを定義しなくても, 関数で済ますことができる。

JavaScript
[RAW]
  1. // コンポーネント名は大文字で始めなければならない。
  2. // JSX の属性値は, JSON プロパティに展開されるため, 式でなければならない。特定
  3. // の場合だけ要素を表示する場合は, 下のように条件式か、あるいは3項演算子を使う.
  4. const TodoListRow = ({ onClick, completed, text }) => (<div>
  5. <span style={{
  6. textDecoration: completed ? 'line-through' : 'none' }} >
  7. {text}
  8. </span>
  9. {!completed && <button type="button" onClick={onClick}>Done</button>}
  10. </div> );
  11. TodoListRow.propTypes = {
  12. onClick: PropTypes.func.isRequired,
  13. completed: PropTypes.bool.isRequired,
  14. text: PropTypes.string.isRequired
  15. }

PropTypes は、React の機能で, 型チェックと必須パラメタかどうかを指示できる。