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

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

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


スクリーンショット

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

コード

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

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

今回の例では, 1画面に, 新しいアイテムの追加, リスト表示を並べる。コンポーネントはそれぞれ TodoEdit, TodoList. TodoEdit がアイテムを追加して, TodoList コンポーネントに表示するので, 表示するデータは App コンポーネントに保存することにする。

JavaScript
[RAW]
  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 に状態を保存する関係で, ボタンが押されたなどに対応するアクションハンドラを渡している。

JavaScript
[RAW]
  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. }

コメントアウト部分。以前はアロー関数がなく, function 式しかなかった。その時代は, 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 の機能で, 型チェックと必須パラメタかどうかを指示できる。