MeteorJSとReactを勉強してみる その7: コンポーネントのステートにUIの状態を保存してみよう【公式翻訳】
MeteorJSを勉強してみる第七弾です。
タイトルの意味がわかりにくいですね。 まあ実際に書いてみないと何を言っているか分からないと思います。
英語って難しい。
コンポーネントのステートに一時的なUIの状態を保存してみよう
このステップでは、クライアント側でのデータフィルタリング機能をアプリに追加します。 これによりユーザーは、未完了のタスクだけを表示させることができます。
Reactコンポーネントのステートを、クライアント側のみで利用される一時的な状態を保持するためにどのように使えるのかを見ていきましょう。
imports/ui/App.js
before
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Tasks.insert({ text, createdAt: new Date() }); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { return this.props.tasks.map((task) => ( <Task key={task._id} task={task} /> )); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch() }; })(App);
after
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Tasks.insert({ text, createdAt: new Date() }); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { return this.props.tasks.map((task) => ( <Task key={task._id} task={task} /> )); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch() }; })(App);
新しく追加した<input>
タグの中で、this.state.hideCompleted
を呼んでいるのがわかると思います。
Reactコンポーネントは「ステート」と呼ばれる特殊な属性を持っており、ここにカプセル化されたコンポーネントのデータを格納しておくことができます。
これを利用するためには、コンポーネントのコンストラクタの中でthis.state.hideCompleted
の値を初期化しなければなりません。
imports/ui/App.js
before
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Tasks.insert({ text, createdAt: new Date() }); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { return this.props.tasks.map((task) => ( <Task key={task._id} task={task} /> )); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch() }; })(App);
after
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted = false; }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Tasks.insert({ text, createdAt: new Date() }); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { return this.props.tasks.map((task) => ( <Task key={task._id} task={task} /> )); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch() }; })(App);
ここで初期化したthis.state
は、イベントハンドラ内でthis.setState
を呼ぶことで更新することができます。
これにより、ステートは非同期的に更新され、同時にコンポーネントの再レンダリングを引き起こします。
では、実際にステートの内容を更新する役目を持つtoggleHideCompleted()
を実装しましょう。
imports/ui/App.js
before
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted = false; }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Tasks.insert({ text, createdAt: new Date() }); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } render() { return ( <div className="container"> <header> <h1>Todo List</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch() }; })(App);
after
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted = false; }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Tasks.insert({ text, createdAt: new Date() }); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { return this.props.tasks.map((task) => ( <Task key={task._id} task={task} /> )); } toggleHideCompleted() { this.setState({ hideCompleted: !this.state.hideCompleted }); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch() }; })(App);
ここまでできたら、hideCompleted
がtrueの時にはそのタスクを表示しない、という処理をrenderTasks
関数に持たせる必要があります。
imports/ui/App.js
before
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted = false; }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Tasks.insert({ text, createdAt: new Date() }); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { return this.props.tasks.map((task) => ( <Task key={task._id} task={task} /> )); } toggleHideCompleted() { this.setState({ hideCompleted: !this.state.hideCompleted }); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch() }; })(App);
after
import ReactDOM from 'react-dom'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted = false; }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Tasks.insert({ text, createdAt: new Date() }); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { let filteredTasks = this.props.tasks; if (this.state.hideCompleted) { filteredTasks = filteredTasks.filter((task) => !task.checked); } return filteredTasks.map((task) => ( <Task key={task._id} task={task} /> )); } toggleHideCompleted() { this.setState({ hideCompleted: !this.state.hideCompleted }); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch() }; })(App);
ここまでできたら、保存してブラウザを確認してみましょう。 追加したチェックボックスにチェックを入れると、完了していないタスクのみが表示されるようになっているはずです。
未完了タスクの合計数を表示する
完了したタスクを弾くコードを書いてきましたが、同じようなやり方で「未完了タスクの数を表示する」こともできます。
これを実装するためには、データコンテナにタスクの数を入れ、render
メソッドに1行追加するだけでOKです。
既にクライアント側のコレクションにデータを持っているため、「タスク数」を追加するにあたってはサーバーにアクセスする必要は一切ありません。
imports/ui/App.js
before
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted: false, }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Tasks.insert({ text, createdAt: new Date() }); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { let filteredTasks = this.props.tasks; if (this.state.hideCompleted) { filteredTasks = filteredTasks.filter((task) => !task.checked); } return filteredTasks.map((task) => ( <Task key={task._id} task={task} /> )); } toggleHideCompleted() { this.setState({ hideCompleted: !this.state.hideCompleted }); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(), }; })(App);
after
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted: false, }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Tasks.insert({ text, createdAt: new Date() }); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { let filteredTasks = this.props.tasks; if (this.state.hideCompleted) { filteredTasks = filteredTasks.filter((task) => !task.checked); } return filteredTasks.map((task) => ( <Task key={task._id} task={task} /> )); } toggleHideCompleted() { this.setState({ hideCompleted: !this.state.hideCompleted }); } render() { return ( <div className="container"> <header> <h1>Todo List ({this.props.incompleteCount})</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(), incompleteCount: Tasks.find({ checked: { $ne: true } }).count() }; })(App);
ここまで来ると大分Reactの使い方に慣れてきた気がしますね。
render
メソッドには出力したいHTMLを書く- そのHTMLの中で動的に操作したい内容は変数化・メソッド化して外に出す
- リスト要素のように共通の要素は
render
内に直接書くのではなく別コンポーネントに分ける
僕も業務で使ってないので想像ですが、実際はrender()
の中身も別ファイルに分けて、メソッドのみのファイルとUIのファイルを分けてしまうとより良さそうですね。
Meteorの公式ドキュメントに最適な構成に関するものもあったので、こちらも近々読んで翻訳してみます。
第八弾は明日にでも。