HTML5テクニカルノート
React + Redux入門 02: フィールドに入力したテキストを項目リストに加える
- ID: FN1908002
- Technique: HTML5 / ECMAScript 2015
- Library: React 16.8.6
「React + Redux入門 01: テキスト入力のフォームをつくる」でつくったのは、テキスト入力フィールドを加えたフォームでした。今回は、ReduxのStoreに接続して、ボタンクリックにより、入力フィールドのテキストがリスト項目としてページに差し込めるようにします。
01 ActionとReducer
Reduxでは、アプリケーションの状態を変えるためには必ずActionが発行されます。ActionはシンプルなJavaScriptのオブジェクトです。type
プロパティで、何が起こったのかを示します(値は通常文字列)。あとのプロパティはActionにともなう情報で、どのような値を定めても構いません(「Actions」参照)。
{ type: 'ADD_TODO', id: 0, text: '項目テキスト' }
Actionは何が起こったのかを知らせるだけで、状態は変えません。状態をどうするのかを定める関数がReducerです。ただし、状態に直接手を加えることはなく、新たな状態をActionにして返します。
Reducerはいくつもつくられるのが普通です。それらはすべてまとめて、状態を管理するオブジェクトStoreに渡さなければなりません。そのために用いられるのが、combineReducers()
メソッドです(「Using combineReducers」参照)。
Reducerをひとつにまとめるモジュールsrc/reducers/index.js
は、つぎのコードのように定めます。combineReducers()
メソッドの引数はオブジェクトで、そのプロパティとして与えるのがReducerです。メソッドに複数のReducerが納められたオブジェクトを渡せば、ひとつにまとめたReducerが返されます。今はまだ、Reducerの関数はダミーです。
src/reducers/index.jsimport { combineReducers } from 'redux'; export default combineReducers({ todos: () => [] // ダミーReducer });
02 Storeをつくる
StoreオブジェクトをつくるのがcreateStore()
メソッドです。引数にReducerを渡します。Reduxでアプリケーションに与えられるStoreオブジェクトはひとつだけです。
Storeオブジェクトはモジュールsrc/index.js
でつくります。書き加えるのはつぎのコードです。createStore()
メソッドでつくったStoreオブジェクト(store
)は、
<Provider />
のstore
属性にを与えてください。こうしてラップしたアプリケーションのノードすべてから、Storeオブジェクトが使えるようになるのです(「Provider」参照)。
src/index.jsimport { createStore } from 'redux'; import { Provider } from 'react-redux'; import rootReducer from './reducers'; const store = createStore(rootReducer); render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
まだ、Actionが発行されないので、ページの表示は変わりません。けれど、React Developer Toolsで見れば、アプリケーションは<Provider />
で包まれ、プロパティ(Props)と状態(State)にはstore
が加わっていることを確かめられるでしょう(図001)。あとはActionを発行し、Reducerから新しい状態が返れば、React Reduxアプリケーションは動き出すのです。
図001■<Provider />でラップされてstoreが加わった
03 Action Creatorを定める
項目追加のActionをつくります。Actionをつくって返す関数がAction Creatorです(「Action Creators」参照)。type
プロパティの値は、文字列'ADD_TODO'
としましょう。新たにつくるモジュールsrc/actions/index.js
の中身は、つぎのコード001のとおりです。関数は、引数に受け取った項目テキスト(text
)にid
も加えたActionオブジェクトを返します。
コード001■src/actions/index.js
let nextTodoId = 0;
export const addTodo = (text) => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
});
なお、戻り値のActionオブジェクトの3つ目のプロパティ{ text
}に、コロン(:
)と値が記されていません。これは、ECMAScript 2015の省略表記で、{ text: text }
と同じです。
04 connect()メソッドでコンポーネントとStoreをつなぐ
ReactコンポーネントをReduxのStoreとつなぐのが、connect()
関数です。接続されたコンポーネントは、StoreにActionを送ったり、Storeからデータを得ることができるようになります。戻り値は、Storeに接続されたコンポーネントをつくる関数です。引数にコンポーネントを渡します。ただし、コンポーネントのクラスそのものは変わりません。単にラップされるだけです。
関数コンポーネントは、データが納められたオブジェクトを引数に受け取ります。そして、connect()
を引数なしに呼び出して、ラップしたコンポーネントのデータに含まれるのがdispatch()
関数です(「Default: dispatch as a Prop」および「mapDispatchToProps?: Object | (dispatch, ownProps?) => Object」参照)。
src/components/AddTodo.js
モジュールのコードを書き替えて、つぎのようにオブジェクトの分割代入によりdispatch()
関数を取り出してください(「JavaScript: オブジェクトの分割代入とスプレッド構文を使ってみる」参照)。onSubmit
イベントで引数にActionオブジェクトを渡して呼び出せば、接続されたStoreに送られます。
src/components/AddTodo.jsimport { connect } from 'react-redux'; import { addTodo } from '../actions'; // const AddTodo = () => { const AddTodo = ({ dispatch }) => { return ( <div> <form onSubmit={(event) => { // console.log(text, text.length); dispatch(addTodo(text)); }}> </form> </div> ) }; // export default AddTodo; export default connect()(AddTodo);
05 Reducerを定める
Actionは何が起こったのかを知らせるだけで、状態は変えません。新たな状態をつくって返す関数がReducerです。新たにsrc/reducers/todos.js
に定めるReducer(todos
)の第1引数は現在の状態(state
)で、第2引数がAction(action
)です。
ReducerはActionのtype
によって処理を分けます。モジュールsrc/reducers/todos.js
に定めるのはADD_TODO
の場合です(コード002)。スプレッド構文...
により引数のstate
を展開したうえで、action
から得たデータを加えています(「スプレッド構文」参照)。この戻り値が、Storeの新たな状態となるのです。
コード002■src/reducers/todos.js
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
];
default:
return state;
}
};
export default todos;
src/reducers/index.js
に加えていたダミーのReducerは消して、つぎのコード003のように書き替えてください。
コード003■src/reducers/index.js
import { combineReducers } from 'redux';
import todos from './todos';
export default combineReducers({
todos
});
06 リスト表示のためのコンポーネントをつくる
項目がリスト表示できるように、新たなコンポーネントをつくって、アプリケーションのモジュールsrc/components/App.js
に加えましょう。名前はTodoList
です。モジュールはこのあと定めます。
src/components/App.jsimport TodoList from './TodoList'; const App = () => ( <div> <AddTodo /> <TodoList /> </div> )
新たなモジュールsrc/components/TodoList.js
の定めは、以下のコード004のとおりです。connect()
関数の第1引数に渡された関数(mapStateToProps()
)は、接続したStoreが変わると呼び出されます(「Connect: Extracting Data with mapStateToProps」参照)。返されるオブジェクトのプロパティ(todos
)はラップされたコンポーネントのプロパティに加えられ、関数コンポーネントが引数に受け取るのです。
mapStateToProps()
が新たな状態から取り出してコンポーネントに渡したプロパティ(todos
)は、前掲src/reducers/todos.js
が項目を加えたリストデータです(コード002)。それらの各項目をArray.prototype.map()
メソッドでテンプレートに加えて返しています。
なお、要素の配列を波かっこ{}
にくくってテンプレートに加えるのは、複数要素を差し込む構文です(「複数のコンポーネントをレンダリングする」参照)。また、子コンポーネント(Todo
)は、このあと定めます。タグに加えた波かっこ{}
にスプレッド構文...
で展開したオブジェクト(todo
)のプロパティはコンポーネントに与えられます(「属性の展開」参照)。
コード004■src/components/TodoList.js
import React from 'react';
import { connect } from 'react-redux';
import Todo from './Todo';
const TodoList = ({ todos }) => (
<ul className="todo-list">
{todos.map((todo) =>
<Todo
key={todo.id}
{...todo}
/>
)}
</ul>
);
const mapStateToProps = (state) => ({
todos: state.todos
});
export default connect(
mapStateToProps
)(TodoList);
子コンポーネントのモジュールsrc/components/Todo.js
は、つぎのコード005のように定めてください。前掲コード004で与えられたプロパティから、項目テキスト(text
)を取り出して要素(<span>
)に加えています。
コード005■src/components/Todo.js
import React from 'react';
const Todo = ({ text }) => (
<li>
<span>
{text}
</span>
</li>
);
export default Todo;
これで、フィールドに入力したテキストが、ボタンクリックでリスト項目として加わるようになりました(図002)。以下のコード006から008に、手直しした3つのモジュールの記述全体をまとめて掲げます。また、サンプル001はCodeSandboxに公開した今回の作例です。
図002■React Developer Toolsで示されたコンポーネントの構成
コード006■src/index.js
import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from './components/App';
import rootReducer from './reducers';
const store = createStore(rootReducer);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
コード007■src/components/App.js
import React from 'react';
import AddTodo from './AddTodo';
import TodoList from './TodoList';
import '../App.css';
const App = () => (
<div>
<AddTodo />
<TodoList />
</div>
);
export default App;
コード008■src/components/AddTodo.js
import React from 'react';
import { connect } from 'react-redux';
import { addTodo } from '../actions';
const AddTodo = ({ dispatch }) => {
let input;
return (
<div>
<form onSubmit={(event) => {
event.preventDefault();
const text = input.value.trim();
input.value = '';
if (!text) {
return;
}
dispatch(addTodo(text));
}}>
<input ref={(element) => input = element} />
<button type="submit">
Add Todo
</button>
</form>
</div>
);
};
export default connect()(AddTodo);
サンプル001■react-redux-todos-02
React + Redux入門
- React + Redux入門 01: テキスト入力のフォームをつくる
- React + Redux入門 02: フィールドに入力したテキストを項目リストに加える
- React + Redux入門 03: 項目の処理済みと未処理でスタイルを変える
- React + Redux入門 04: リスト項目のフィルタを加える
- React + Redux入門 05: PropTypesで型を確かめる
- React + Redux入門 06: コンポーネントをプレゼンテーションとコンテナに分ける
- React + Redux入門 07: React ReduxのフックuseDispatch()とuseSelector()を使う
- React + Redux入門 08: Redux公式サイトのTodoリストの作例をフックuseDispatch()とuseSelector()で書き替える
作成者: 野中文雄
作成日: 2019年8月6日
Copyright © 2001-2019 Fumio Nonaka. All rights reserved.