React Redux: 公式サイト「Basic Tutorial」の作例をフックで書き替える


React 16.8から、関数コンポーネントにReactの機能を加えるフック(hook)が採り入れられました。足並みを揃えてReact Redux 7.1.0に備わったのが、Reduxにつなげるフックです。React Redux公式「Basic Tutorial」の作例「Todo App with Redux」は、残念ながらReact 16.4.2でつくられ、どちらのフックも使われていません。そこで、フック対応に書き替えてみようというのが本稿のお題です。


>> Todo App with Redux

クラスコンポーネントを関数コンポーネントに書き替える

まず、モジュールsrc/components/AddTodo.jsは、コンポーネントに状態(state)をもたせるため、クラスで定められています。ReactのフックuseState()を使って、関数コンポーネントに直しましょう(「React Hooks: クラスのコンポーネントをuseState()で関数に書き替える」参照)。関数コンポーネントの中ではthis参照がなくなることにご注目ください。

src/components/AddTodo.js
// import React from 'react';
import React, { useState } from 'react';

// class AddTodo extends React.Component {
const AddTodo = ({ addTodo }) => {
    /* constructor(props) {
        super(props);
        this.state = { input: '' };
    } */
    const [input, setInput] = useState('');

    // updateInput = input => {
    const updateInput = input => {
        // this.setState({ input });
        setInput(input);
    };

    // handleAddTodo = () => {
    const handleAddTodo = () => {
        // this.props.addTodo(this.state.input);
        addTodo(input);
        // this.setState({ input: '' });
        setInput('');
    };

    // render() {
    return (
        <div>
            <input
                // onChange={e => this.updateInput(e.target.value)}
                onChange={e => updateInput(e.target.value)}
                // value={this.state.input}
                value={input}
            />
            {/* <button className='add-todo' onClick={this.handleAddTodo}> */}
            <button className='add-todo' onClick={handleAddTodo}>
                Add Todo
            </button>
        </div>
    );
    // }
};

useDispatch()を使う

つぎに、React Reduxのフックを使ってゆきます。connect()関数の第2引数に与え、アクションを発行するdispatch()につなげるフックがuseDispatch()です。アクションクリエーター(addTodo)は、フックから返された関数(dispatch())で呼び出します。

src/components/AddTodo.js
// import { connect } from 'react-redux';
import { useDispatch } from 'react-redux';

// const AddTodo = ({ addTodo }) => {
const AddTodo = () => {

    const dispatch = useDispatch();

    const handleAddTodo = () => {
        // addTodo(input);
        dispatch(addTodo(input));
    };

};

/* export default connect(
    null,
    { addTodo }
)(AddTodo); */
export default AddTodo;

これでモジュールsrc/components/AddTodo.jsが純粋な関数コンポーネントになりました。src/components/Todo.jsも、同じようにuseDispatch()connect()のラップが外せます。

src/components/Todo.js
// import { connect } from 'react-redux';
import { useDispatch } from 'react-redux';
import cx from 'classnames';
import { toggleTodo } from '../redux/actions';

// const Todo = ({ todo, toggleTodo }) => (
const Todo = ({ todo }) => {
    const dispatch = useDispatch();
    return (
        // <li className='todo-item' onClick={() => toggleTodo(todo.id)}>
        <li className='todo-item' onClick={() => dispatch(toggleTodo(todo.id))}>

        </li>
    );
}

/* export default connect(
    null,
    { toggleTodo }
)(Todo); */
export default Todo;

useSelector()を使う

connect()の第1引数(mapStateToProps)が担う、Storeから状態(state)を取り出すフックがuseSelector()です。渡すのは関数で、引数(state)の状態から処理した値を返します。モジュールsrc/components/TodoList.jsは、つぎのように書き替えればよいでしょう。

src/components/TodoList.js
// import { connect } from 'react-redux';
import { useSelector } from 'react-redux';

// const TodoList = ({ todos }) => (
const TodoList = () => {
    const todos = useSelector(state => {
        const visibilityFilter = state.visibilityFilter;
        const todos = getTodosByVisibilityFilter(state, visibilityFilter);
        return todos;
    });

}

/* const mapStateToProps = state => {
    const { visibilityFilter } = state;
    const todos = getTodosByVisibilityFilter(state, visibilityFilter);
    return { todos };
};
export default connect(mapStateToProps)(TodoList); */
export default TodoList;

残るモジュールsrc/components/VisibilityFilters.jsは、connect()にふたつの引数が与えられています。したがって、フックもuseSelector()useDispatch()をともに使わなければなりません。

src/components/VisibilityFilters.js
// import { connect } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';

// const VisibilityFilters = ({ activeFilter, setFilter }) => {
const VisibilityFilters = () => {
    const activeFilter = useSelector(state => state.visibilityFilter);
    const dispatch = useDispatch();
    return (
        <div className='visibility-filters'>
            {Object.keys(VISIBILITY_FILTERS).map(filterKey => {
                const currentFilter = VISIBILITY_FILTERS[filterKey];
                return (
                    <span

                        onClick={() => {
                            // setFilter(currentFilter);
                            dispatch(setFilter(currentFilter));
                        }}
                    >
                        {currentFilter}
                    </span>
                );
            })}
        </div>
    );
};

/* const mapStateToProps = state => {
    return { activeFilter: state.visibilityFilter };
};
export default connect(
    mapStateToProps,
    { setFilter }
)(VisibilityFilters); */
export default VisibilityFilters;

以上でReact Redux公式「Basic Tutorial」の作例が、フックを使った純粋な関数コンポーネントに書き直せました。でき上がりはCodeSandboxに「Todo App with Redux using Hooks」として公開しました。


>> Todo App with Redux using Hooks