サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

React + Redux入門 02: フィールドに入力したテキストを項目リストに加える


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.js

import { 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.js

import { 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が加わった

図001

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.js

import { 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.js

import 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で示されたコンポーネントの構成

図001

コード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入門


作成者: 野中文雄
作成日: 2019年8月6日


Copyright © 2001-2019 Fumio Nonaka.  All rights reserved.