サイトトップ

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

HTML5テクニカルノート

React + Redux入門 06: コンポーネントをプレゼンテーションとコンテナに分ける


ReactとReduxでアプリケーションをつくるとき、コンポーネントはプレゼンテーションとコンテナに分けることが推奨されています(「Presentational and Container Components」参照)。アプリケーションの組み立てが明らかになり、コンポーネントも使い回しやすいからです。

プレゼンテーションコンポーネントは、テンプレートとスタイルを軸に見た目を定めます。ReduxのStoreと接続して、データを得たり状態を変えたりするのが、コンテナコンポーネントです(表001)。コンテナはReact Reduxのconnect()関数を呼び出してつくるのが、多くの場合適しています。

表001■プレゼンテーションコンポーネントとコンテナコンポーネント

違い プレゼンテーション コンテナ
定めるもの 見え方(テンプレートやスタイル) 動き方(データの取得や状態の更新)
Reduxへの認識 なし あり
データの読み込み プロパティ(props)から Reduxの状態(state)を購読
データの変更 プロパティからコールバック ReduxのActionを配信
生成 コードを記述 通常はReact Reduxが行う

01 コンポーネントLinkからコンテナにFilterLinkを分ける

モジュールsrc/components/Link.jsの中でプレゼンテーションの役割を担うのは、テンプレートを返す関数コンポーネント(Link)です。connect()関数の呼び出しと、その引数に渡す関数(mapStateToProps()mapDispatchToProps())はコンテナに移します。

src/components/Link.js

// import { connect } from 'react-redux';
// import { setVisibilityFilter } from '../actions';

const Link = ({ active, children, onClick }) => (  // 関数コンポーネントを残す
	// ...[中略]...
);

/* const mapStateToProps = (state, ownProps) => ({
	active: ownProps.filter === state.visibilityFilter
});

const mapDispatchToProps = (dispatch, ownProps) => ({
	onClick: () => dispatch(setVisibilityFilter(ownProps.filter))
}); */

/* export default connect(
	mapStateToProps,
	mapDispatchToProps
)(Link); */
export default Link;  // 変更

コンテナはモジュールsrc/containers/FilterLink.jsとし、前掲Linkコンポーネントから抜き出した記述を、つぎのコード001のように書き込みます。connect()から返された関数に渡すプレゼンテーションコンポーネント(Link)を、忘れずにインポートしてください。

コード001■src/containers/FilterLink.js


import { connect } from 'react-redux';
import { setVisibilityFilter } from '../actions';
import Link from '../components/Link';  // 追加

const mapStateToProps = (state, ownProps) => ({
	active: ownProps.filter === state.visibilityFilter
});

const mapDispatchToProps = (dispatch, ownProps) => ({
	onClick: () => dispatch(setVisibilityFilter(ownProps.filter))
});

export default connect(
	mapStateToProps,
	mapDispatchToProps
)(Link);

モジュールsrc/components/Footer.jsは、つぎのようにコンテナをインポートし、テンプレートも書き替えます。これでひとつ、コンポーネントからコンテナが切り分けられました。

src/components/Footer.js

// import Link from './Link';
import FilterLink from '../containers/FilterLink';

const Footer = () => (
	<div>

		{/* <Link filter={VisibilityFilters.SHOW_ALL}> */}
		<FilterLink filter={VisibilityFilters.SHOW_ALL}>
			All
		{/* </Link> */}
		</FilterLink>
		{/* <Link filter={VisibilityFilters.SHOW_ACTIVE}> */}
		<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>
			Active
		{/* </Link> */}
		</FilterLink>
		{/* <Link filter={VisibilityFilters.SHOW_COMPLETED}> */}
		<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>
			Completed
		{/* </Link> */}
		</FilterLink>
	</div>
);

書き直したふたつのコンポーネントモジュールの記述全体は、以下のコード002と003にまとめました。

コード002■src/components/Link.js


import React from 'react';
import PropTypes from 'prop-types';

const Link = ({ active, children, onClick }) => (
	<button
		onClick={onClick}
		disabled={active}
		style={{
			marginLeft: '4px',
		}}
	>
		{children}
	</button>
);

Link.propTypes = {
    active: PropTypes.bool.isRequired,
    children: PropTypes.node.isRequired,
    onClick: PropTypes.func.isRequired
};

export default Link;

コード003■src/components/Footer.js


import React from 'react';
import FilterLink from '../containers/FilterLink';
import { VisibilityFilters } from '../actions';

const Footer = () => (
	<div>
		<span>Show: </span>
		<FilterLink filter={VisibilityFilters.SHOW_ALL}>
			All
		</FilterLink>
		<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>
			Active
		</FilterLink>
		<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>
			Completed
		</FilterLink>
	</div>
);

export default Footer;

02 コンポーネントTodoListからコンテナVisibleTodoListを分ける

src/components/TodoList.jsも、関数コンポーネント(TodoList)を残します。

src/components/TodoList.js

// import { connect } from 'react-redux';
// import { toggleTodo, VisibilityFilters } from '../actions';

const TodoList = ({ todos, toggleTodo }) => (
	// ...[中略]...
);

/* const getVisibleTodos = (todos, filter) => {
	switch (filter) {
		case VisibilityFilters.SHOW_ALL:
			return todos
		case VisibilityFilters.SHOW_COMPLETED:
			return todos.filter(t => t.completed)
		case VisibilityFilters.SHOW_ACTIVE:
			return todos.filter(t => !t.completed)
		default:
			throw new Error('Unknown filter: ' + filter)
	}
}

const mapStateToProps = (state) => ({
	todos: getVisibleTodos(state.todos, state.visibilityFilter)
});

const mapDispatchToProps = (dispatch) => ({
	toggleTodo: (id) => dispatch(toggleTodo(id))
}); */

/* export default connect(
	mapStateToProps,
	mapDispatchToProps
)(TodoList); */
export default TodoList;

新たにつくるコンテナのモジュールは、src/containers/VisibleTodoList.jsです。前掲TodoListコンポーネントから抜き出したコードを、つぎのコード004のとおり移してください。

コード004■src/containers/VisibleTodoList.js


import { connect } from 'react-redux';
import TodoList from '../components/TodoList';  // 追加
import { toggleTodo, VisibilityFilters } from '../actions';

const getVisibleTodos = (todos, filter) => {
	switch (filter) {
		case VisibilityFilters.SHOW_ALL:
			return todos
		case VisibilityFilters.SHOW_COMPLETED:
			return todos.filter(t => t.completed)
		case VisibilityFilters.SHOW_ACTIVE:
			return todos.filter(t => !t.completed)
		default:
			throw new Error('Unknown filter: ' + filter)
	}
}

const mapStateToProps = (state) => ({
	todos: getVisibleTodos(state.todos, state.visibilityFilter)
});

const mapDispatchToProps = (dispatch) => ({
	toggleTodo: (id) => dispatch(toggleTodo(id))
});

export default connect(
	mapStateToProps,
	mapDispatchToProps
)(TodoList);

モジュールsrc/components/App.jsは、TodoListに替えてコンテナのVisibleTodoListをインポートしてテンプレートに差し込みます。

src/components/App.js

// import TodoList from './TodoList';
import VisibleTodoList from '../containers/VisibleTodoList';  // 追加

const App = () => (
	<div>

		{/* <TodoList /> */}
		<VisibleTodoList />

	</div>
);

書き改めたモジュールsrc/components/TodoList.jsの記述全体は、つぎのコード005のとおりです。

コード005■src/components/TodoList.js


import React from 'react';
import PropTypes from 'prop-types';
import Todo from './Todo';

const TodoList = ({ todos, toggleTodo }) => (
	<ul className="todo-list">
		{todos.map((todo) =>
			<Todo
				key={todo.id}
				{...todo}
				onClick={() => toggleTodo(todo.id)}
			/>
		)}
	</ul>
);

TodoList.propTypes = {
	todos: PropTypes.arrayOf(PropTypes.shape({
		id: PropTypes.number.isRequired,
		completed: PropTypes.bool.isRequired,
		text: PropTypes.string.isRequired
	}).isRequired).isRequired,
	toggleTodo: PropTypes.func.isRequired
};

export default TodoList;

03 AddTodoのパスを変える

モジュールsrc/components/AddTodo.jsは、パスをsrc/containers/AddTodo.jsに改めます(図001)。そして、プレゼンテーションとの切り分けはしません。分けようとすれば分けられるものの、テンプレートの中にデータの処理が含まれています。また、コードが少ないので、まだ無理に分けなくてもよいからです(「Designing Other Components」参照)。

図001■AddTodo.jsをフォルダcontainersに移す

図001

AddTodoコンポーネントのコードはそのものは変わりません。モジュールsrc/components/App.jsからインポートするパスが変わるだけです。その記述全体を、つぎのコード006に掲げます。

コード006■src/components/App.js


import React from 'react';
import AddTodo from '../containers/AddTodo';  // 変更
import VisibleTodoList from '../containers/VisibleTodoList';
import Footer from './Footer';
import '../App.css';

const App = () => (
	<div>
		<AddTodo />
		<VisibleTodoList />
		<Footer />
	</div>
);

export default App;

サンプル001■react-redux-todos-06

React + Redux入門


作成者: 野中文雄
作成日: 2019年9月4日


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