サイトトップ

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

HTML5テクニカルノート

React + Redux入門 08: Redux公式サイトのTodoリストの作例をフックuseDispatch()とuseSelector()で書き替える


前回は「React + Redux入門 02: フィールドに入力したテキストを項目リストに加える」の作例を、フックuseDispatch()とuseSelector()で書き替えました。このシリーズは、Redux公式サイトの「Example: Todo List」をつくるチュートリアルで、第6回ができ上がりです。けれど今回は、「React + Redux入門 05: PropTypesで型を確かめる」の作例(サンプル001)にもとづいて手を加えることにします。

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

01 状態をもつコンポーネントは切り分けるべきか

React + Redux入門 06: コンポーネントをプレゼンテーションとコンテナに分ける」は、ReduxのStoreと接続して、データを得たり状態を変えたりするコンポーネントは切り分けました。この考え方を唱えたのが、「Presentational and Container Components」です。けれど、今や目くじらを立てて分けなくても構わないとされます。

I don’t suggest splitting your components like this anymore. [...中略...] The main reason I found it useful was because it let me separate complex stateful logic from other aspects of the component. Hooks let me do the same thing without an arbitrary division.
(「Presentational and Container Components」より)

このようにコンポーネントを分けることは、もはやおすすめはしません。このやり方がよいと思った理由は、複雑な状態のロジックを、コンポーネントの他の側面と分けられるからです。フックを使えば、わざわざ分けなくても同じことができます。

ここで言及されているフックは、React標準の関数です。コンポーネント自身がもてばよい状態は、関数コンポーネントにすっきり与えられます。さらに、React Reduxのフックにより、Storeの状態(state)の参照や、Actionの発行もシンプルにできるようになりました。複数のコンポーネントがStoreを参照したり、複雑なやり取りをしないかぎりは、杓子定規にコンポーネントは切り分けなくてもよいのです。実際、今回フックで書き直した後掲作例(サンプル002)のコードは簡潔で、あえて分けることはないと考えました。

02 コンポーネントAddTodoを書き替える

モジュールsrc/components/AddTodo.jsは、前回フックuseDispatch()を使って書き直しました。けれど、シリーズのそのあとの回でコードが加えられています。改めて書き替え箇所を示したのが、つぎに抜き出したコードです。dispatch()は関数コンポーネント(AddTodo)の引数から取り出しませんので、PropTypesによる型指定は除きましょう。

src/components/AddTodo.js

// import PropTypes from 'prop-types';
// import { connect } from 'react-redux';
import { useDispatch } from 'react-redux';

// const AddTodo = ({ dispatch }) => {
const AddTodo = () => {
	const dispatch = useDispatch();

	return (
		<div>
			<form onSubmit={(event) => {

				dispatch(addTodo(text));
			}}>

			</form>
		</div>
	);
};

/* AddTodo.propTypes = {
	dispatch: PropTypes.func.isRequired
};

export default connect()(AddTodo); */
export default AddTodo;

手直ししたモジュールsrc/components/AddTodo.jsの記述全体は、つぎのコード001のとおりです。

コード001■src/components/AddTodo.js


import React from 'react';
import { useDispatch } from 'react-redux';
import { addTodo } from '../actions';

const AddTodo = () => {
	const dispatch = useDispatch();
	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 AddTodo;

03 コンポーネントTodoListを書き替える

src/components/TodoList.jsも、前回フックで書き直したモジュールです。けれど、useSelector()で取り出すべきデータは変わり、useDispatch()でActionの発行もしなければなりません。connect()に渡していた第1引数(mapStateToProps())がuseSelector()に、第2引数(mapDispatchToProps())はuseDispatch()に役割が分けられると理解すればよいでしょう。PropTypesによる型指定はやはり要らなくなります。

src/components/TodoList.js

// import PropTypes from 'prop-types';
// import { connect } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';

// const TodoList = ({ todos, toggleTodo }) => (
const TodoList = () => {
	const todos = useSelector((state) => getVisibleTodos(state.todos, state.visibilityFilter));
	const dispatch = useDispatch();
	return (
		<ul className="todo-list">
			{todos.map((todo) =>
				<Todo

					// onClick={() => toggleTodo(todo.id)}
					onClick={() => dispatch(toggleTodo(todo.id))}
				/>
			)}
		</ul>
	);
}

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

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

TodoList.propTypes = {

};

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

書き直したモジュールsrc/components/TodoList.jsの記述をまとめたのが、つぎのコード002です。かなりすっきりしたのではないでしょうか。

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


import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { toggleTodo, VisibilityFilters } from '../actions';
import Todo from './Todo';

const TodoList = () => {
	const todos = useSelector((state) => getVisibleTodos(state.todos, state.visibilityFilter));
	const dispatch = useDispatch();
	return (
		<ul className="todo-list">
			{todos.map((todo) =>
				<Todo
					key={todo.id}
					{...todo}
					onClick={() => dispatch(toggleTodo(todo.id))}
				/>
			)}
		</ul>
	);
}

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)
	}
}

export default TodoList;

04 コンポーネントLinkを書き替える

残るはsrc/components/Link.jsモジュールです。connect()には第1引数(mapStateToProps())、第2引数(mapDispatchToProps())ともに与えられています。そのとき、どちらもstateに加えて、ownPropsも受け取っていることに注目してください。フックでは、これらはつぎのように関数コンポーネント(Link)が受け取る引数オブジェクト(props)から取り出せばよいのです。

src/components/Link.js

// import { connect } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';

// const Link = ({ active, children, onClick }) => (
const Link = ({ children, filter }) => {
const active = useSelector((state) => filter === state.visibilityFilter);
const dispatch = useDispatch();
const onClick = () => dispatch(setVisibilityFilter(filter));
	return (
		<button
			onClick={onClick}
			disabled={active}

		>
			{children}
		</button>
	);
};

/* 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;

関数コンポーネント(Link)が引数propsを受け取っていますから、PropTypesによる型指定には必要な手を加えましょう。もっとも、他のモジュールもPropTypesはあまり使わなくなりました。本格的な開発には、これからはTypeScriptで型づけすることをお考えください。

src/components/Link.js

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

フックに直したモジュールsrc/components/Link.jsの記述全体は、つぎのコード003のとおりです。でき上がった作例は、以下のサンプル002としてCodeSandboxに公開しました。

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


import React from 'react';
import PropTypes from 'prop-types';
import { useSelector, useDispatch } from 'react-redux';
import { setVisibilityFilter } from '../actions';

const Link = ({ children, filter }) => {
	const active = useSelector((state) => filter === state.visibilityFilter);
	const dispatch = useDispatch();
	const onClick = () => dispatch(setVisibilityFilter(filter));
	return (
		<button
			onClick={onClick}
			disabled={active}
			style={{
				marginLeft: '4px',
			}}
		>
			{children}
		</button>
	);
};

Link.propTypes = {
	children: PropTypes.node.isRequired,
	filter: PropTypes.string.isRequired
};

export default Link;

サンプル002■react-redux-todos-08

React + Redux入門


作成者: 野中文雄
作成日: 2020年04月30日


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