HTML5テクニカルノート
React + Redux入門 06: コンポーネントをプレゼンテーションとコンテナに分ける
- ID: FN1909001
- Technique: HTML5 / ECMAScript 2015
- Library: React 16.9.0
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に移す
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入門
- 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年9月4日
Copyright © 2001-2019 Fumio Nonaka. All rights reserved.