React with Redux toDoList パート4
前回の記事の続きになります。以下のページを参考に、
addTodo/deleteTodo/Undeleteコンポーネントを実装しました。
また、今回のTDDの流れを確認すると、
- 機能テストを書く
- アクションのテストを書く
→テストが通るアクションを作る - reducerのテストを書く
→テストが通るreducerを作る - componentのテストを書く
→テストが通るcomponentを作る - 最後に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リンクです。
次にやること
- 適切なタイミングで、ボタン押下ができるように、ボタンのdisabling機能をつけていきます。
- Herokuに実際にdeployしてみる。
