React with Redux toDoList パート4

Tuyoshi Akiyama
Jul 27, 2017 · 10 min read

前回の記事の続きになります。以下のページを参考に、

addTodo/deleteTodo/Undeleteコンポーネントを実装しました。


また、今回のTDDの流れを確認すると、

  1. 機能テストを書く
  2. アクションのテストを書く
    →テストが通るアクションを作る
  3. reducerのテストを書く
    →テストが通るreducerを作る
  4. componentのテストを書く
    →テストが通るcomponentを作る
  5. 最後にApp componentを書く
    →App testを作る

上の流れでは、1,2,5は比較的すぐに書くことが出来ましたが、reducerとcomponentの作成が考えさせられました。

特に、reducerでどのようにstateを管理していくのかが、早く覚えていく必要があると感じました。


では実際にコードをおって、見ていきます。

e2etest/test.js

it('should allow me to undelete a toDo', () => {
const todoText = 'get better a test';
browser.url('http://localhost:3000/');
browser.element('.todo-input').setValue(todoText);
browser.click('.todo-submit');
browser.click('.todo-delete');
browser.click('.todo-undelete');
const actual = browser.element('.todo-text');
expect(actual.state).to.equal('success');
});

入力して→submitして→deleteして→undeleteを押した時に、

入力値がちゃんとundeleteされているかを確認しています。


constants/index.js

const types = {
SUBMIT_TODO: 'SUBMIT_TODO',
DELETE_TODO: 'DELETE_TODO',
UNDELETE_TODO: 'UNDELETE_TODO',
};
export default types;

constatnsに新しいタイプを追加します。


actions/test.js

it('should create an action to undelete a todo', () => {
const expectedAction = {
type: types.UNDELETE_TODO,
};
expect(actions.undeleteTodo()).toEqual(expectedAction);
});

undeleteアクションはアクションのタイプのみを渡して、特に何か引数を持たせるなどはしません。

例えばここでdeleteアクションを作る場合、どのstate dataを削除したいかなどidを特定する必要があるので、下の様なテストになります。

it('should create an action to delete a todo', () => {
const expectedAction = {
type: types.DELETE_TODO,
id: 1,
};
expect(actions.deleteTodo(1)).toEqual(expectedAction);
})

actions/index.js

deleteTodo(id) {
return {
type: types.DELETE_TODO,
id,
};
},
undeleteTodo() {
return {
type: types.UNDELETE_TODO,
};
},

reducers/test.js

// test for delete
describe('delete todo', () => {
const staringState = {
todos: [
{
id: 1,
text: todoText,
},
],
deleted: {},
};
const action = {
type: types.DELETE_TODO,
id: 1,
};
const expectedState = {
todos: [],
deleted: {
id: 1,
text: todoText,
},
};
expect(reducer(staringState, action)).toEqual(expectedState);
});
// test for undelete
describe('undelete todo', () => {
const staringState = {
todos: [],
deleted: {
id: 1,
text: todoText,
},
};
const action = {
type: types.UNDELETE_TODO,
};
const expectedState = {
todos: [
{
id: 1,
text: todoText,
},
],
deleted: {},
};
expect(reducer(staringState, action)).toEqual(expectedState);
});

まず、今までのstateにtodosに加えて、deletedを設定します。

これによって、deleteTodo関数で消されたデータを、ここのdeletedで保持することができるようになります。

reducers/index.js

export const initialState = {
todos: [],
deleted: {},
};

case types.DELETE_TODO:
return {
...state,
todos: [
...state.todos.filter(todo => (
todo.id !== action.id
)),
],
deleted: state.todos.filter(todo => todo.id === action.id)[0],
};
case types.UNDELETE_TODO:
return {
...state,
todos: [
...state.todos,
state.deleted,
],
deleted: {},
};

まずinitialStateで、todosとdeletedを設定します。この時点で、何のデータをstoreしたいかによって、stateの内容は変わります。今回の場合は、todolistとデリートしたデータを保存して使いたかったので、この2つになっています。

それぞれのアクションタイプ別に、それぞれ違った処理で、新しいstateを返しています。

delete関数では、state.todosの配列からaction.idに等しいstate.idをfilteringしています。

そのfilteringされたstateはdeletedに代入されます。

またundelete関数では、todoにstate.todosの配列にプラスして、delete時に格納されたstate.deletedを代入しています。


次にコンポーネントについて見ていきます。

components/addTodo/addTodo.test.js

describe('AddTodo component', () => {
let component;
const submitMock = jest.fn();
const undelMock = jest.fn();
beforeEach(() => {
component = shallow(
<AddTodo
submitTodo={submitMock}
undeleteTodo={undelMock}
/>,
);
});

describe('Add undelete button', () => {
it('Should exist undelete button', () => {
expect(component.find('.todo-undelete').length).to.equal(1);
});
it('should call the undeleteTodo function when undelete button is clicked', () => {
component = mount(
<AddTodo
submitTodo={submitMock}
undeleteTodo={undelMock}
/>);
expect(undelMock.mock.calls.length).to.equal(0);
component.find('.todo-undelete').simulate('click');
expect(undelMock.mock.calls.length).to.equal(1);
});
});

components/index.js

フォーム内に、undeleteボタンを入れます。

        <button
className="todo-undelete"
onClick={() => undeleteTodo()}
>
Undelete
</button>

また、undeleteTodo関数はAddTodoコンポーネントの引数(props)となるので、次のようにvalidateします。

AddTodo.propTypes = {
submitTodo: PropTypes.func.isRequired,
undeleteTodo: PropTypes.func.isRequired,
};

UI画面、Appコンポーネントに上で設定したコンポーネントを入れていきます。

export const App = ({ submitTodo, todos, deleteTodo, undeleteTodo }) => (
<div>
<h1>Todo list</h1>
<p>Write something to do</p>
<AddTodo
submitTodo={submitTodo}
undeleteTodo={undeleteTodo} />
<TodoList
todos={todos}
deleteTodo={deleteTodo} />
</div>
);
const mapStateToProps = state => state.todoListApp;const mapDispatchToProps = dispatch => ({
submitTodo: (text) => {
if (text) {
dispatch(actions.submitTodo(text));
}
},
deleteTodo: (id) => {
dispatch(actions.deleteTodo(id));
},
undeleteTodo: () => {
dispatch(actions.undeleteTodo());
},
});export default connect(mapStateToProps, mapDispatchToProps)(App);

ここまでの、repositoryリンクです。


次にやること

  • Herokuに実際にdeployしてみる。

Tuyoshi Akiyama

Written by

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade