参考にできそうなサイトを探すと出てはくるのですが、どういった順番で作成するのかがわからない記事が多かったため、動画を参考にさせていただき作成しました。
*作るもの
Todoタスクの追加、削除、編集する機能を実装しました。チェックを付けたタスクを「完了」とし、一括で全て完了/未完了にしたり、完了にしたタスクを一括削除する機能も付けました。また、タスクを完了/未完了/全てでフィルタリングして表示できるようにもしてあります。React の基礎的な学習をすることを目的としているので、レイアウトは適当です。
*目次
- プロジェクト作成
- 準備
- モックを作成
- Formコンポーネントの作成
- Stateを追加
- Todoコンポーネントの作成
- タスク追加機能の実装
- 完了チェックボックスの実装
- 一括チェックボックスの実装
- 完了タスク一括削除ボタンの実装
- フィルター機能の実装
- 削除機能を実装
- 編集機能の実装
*参考
*環境
- MacOS
- create-react-app 3.0.1
- react 16.8.6
- react-dom 16.8.6
- react-scripts 3.0.1
- node 12.4.0
*プロジェクト作成
create-react-app
を使ってプロジェクトの雛形を作成します。インストール方法については下記の記事を参照してください。
create-react-app
を実行し、作成されたディレクトリの中に入ります。$ create-react-app react-todo
$ cd react-todo/
ディレクトリ構成は下記になります。
react-todo
├── README.md
├── node_modules
├── package-lock.json
├── package.json
├── public
└── src
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
└── serviceWorker.js
src/index.js
が React アプリのエントリーポイントになります。(アプリケーション起動時に最初に動くファイル)public/index.html
に<div id="root"></div>
があり、src/index.js
がそのdiv要素を取得して React コンポーネントのApp
としてマウントしています。このpublic/index.html
が、React アプリがレンダリングされるときの本体になります。src/index.js
でインポートされているsrc/App.js
は、雛形作成直後にアプリケーションを起動すると表示されるレイアウトが書かれています。<src/index.js>
// Reactコンポーネントをマウント
ReactDOM.render(<App />, document.getElementById('root'));
補足ですが、開発用サーバーでアプリケーションを起動したい場合は下記コマンドを使います。
// 開発用サーバーで起動
$ npm run start
// 本番用にビルド
$ npm run build
また、Chrome拡張の「React Developer Tools」を入れるとブラウザでReactのソースコードの確認ができて便利です。
*準備
src
ディレクトリ配下にある7ファイルを全部削除し、src/index.js
を新規作成します。react-dom
はDOM要素を変換するために使います。react-dom
が持っているrender()
にDOM要素と表示する場所を指定して使います。index.js
を下記実装にすると画面に「hoge」が表示されます。<src/index.js>
import React from 'react'
import ReactDOM from 'react-dom'
ReactDOM.render(<div>hoge</div>, document.getElementById('root'));
components
ディレクトリを作成してApp.js
を新規作成します。下記では class コンポーネントを使って書いていますが、function コンポーネントで書くこともできます。
<src/components/App.js>
import React from 'react';
class App extends React.Component {
render() {
return <div>hoge</div>;
};
}
export default App;
上記では class コンポーネントを使って書いていますが、function コンポーネントで書くこともできます。
<src/components/App.js>
import React from 'react';
const App = () => <div>hoge</div>;
export default App;
src/index.js
を修正します。作成した
App
コンポーネントを読み込み、render()
の引数のDOM要素だった部分を<App />
に置き換えます。画面での表示は変わらず「hoge」が表示されます。
<src/index.js>
import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/App';
ReactDOM.render(<App />, document.getElementById('root'));
*モックを作成
App.js
にレイアウトを追加します。return
にカッコを追加し、DOM要素を書いていきます。まずは動かなくていいのでサンプルデータを使って全レイアウトを作成します。<src/components/App.js>
import React from 'react';
class App extends React.Component {
render() {
return (
<div>
<form>
<input type="text" />
<button>追加</button>
</form>
<label>
<input type="checkbox" />
全て完了にする
</label>
<select>
<option>全て</option>
<option>未完了</option>
<option>完了済み</option>
</select>
<ul>
<li>
<label>
<input type="checkbox" />
朝ごはんを食べる
</label>
<button>編集</button>
<button>削除</button>
</li>
<li>
<label>
<input type="checkbox" />
筋トレをする
</label>
<button>編集</button>
<button>削除</button>
</li>
<li>
<label>
<input type="checkbox" />
勉強をする
</label>
<button>編集</button>
<button>削除</button>
</li>
</ul>
<button>完了済みを全て削除</button>
</div>
)
};
}
export default App;
*Formコンポーネントの作成
App.js
に書いた追加ボタンの Form タグをコンポーネント化します。components/Form.js
を新規作成します。return
の中に書くDOM要素はApp.js
からコピーします。<src/components/Form.js>
import React from 'react';
class Form extends React.Component {
render() {
return (
<form>
<input type="text" />
<button>追加</button>
</form>
)
};
}
export default Form;
App.js
を修正します。form タグを Form コンポーネントに置き換えます。
画面に表示される情報は今までと同じ状態です。
<src/components/App.js>
import React from 'react';
// ----- 追加 -----
import Form from './Form';
class App extends React.Component {
render() {
return (
<div>
// ----- Formに修正 -----
<Form />
<label>
<input type="checkbox" />
全て完了にする
</label>
...
*Stateを追加
入力したタスクを保存できるよう React のstate
という機能を使って状態を管理します。Form.js
にconstructor()
を追加してthis.state
を定義します。super()
はReact.Component
を呼び出していることを意味しています。次に
handleChange()
を追加し、this.setState()
で入力されたタスク名を保存します。e
はイベントを意味しています。入力されたイベントが発生したときにhandleChange()
を呼び出したいため、input
タグにonChange
を追加してhandleChange()
を指定します。処理の流れは下記になります。
- 入力されたら
onChange
イベントが発生し、handleChange()
が呼ばれる。 setState()
でstate
を更新state.input
の値が入力フォームに表示
import React from 'react';
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
input: ''
};
}
render() {
return (
<form>
<input type="text" value={this.state.input} onChange={this.handleChange} />
<button>追加</button>
</form>
);
}
handleChange = e => {
this.setState({ input: e.currentTarget.value })
}
}
export default Form;
*Todoコンポーネントの作成
1つのタスクが表示される部分をコンポーネント化します。components/Todo.js
を新規作成します。render()
するときの DOM は1つでなければいけないので、親要素として div タグで囲ってひとまとまりにする必要があります。タスクは複数登録されるので、汎用的に使えるよう React の
props
という機能を使ってテキストの値を可変にします。props
を使うと呼び出し元から値を受け取ることができます。this.props
でid
とtext
を持ったオブジェクトを受け取るので、{text}
でtext
の値だけ取り出しています。<src/components/Todo.js>
import React from 'react';
class Todo extends React.Component {
render() {
const { text } = this.props
return (
<div>
<label>
<input type="checkbox" />
{text}
</label>
<button>編集</button>
<button>削除</button>
</div>
)
}
}
export default Todo;
App.js
を修正します。タスク表示部分を Todo コンポーネントに置き換えます。
この時点でも画面に表示される情報は今までと同じ状態です。
<src/components/App.js>
import React from 'react';
import Form from './Form';
// ----- 追加 -----
import Todo from './Todo';
class App extends React.Component {
render() {
return (
<div>
<Form />
<label>
<input type="checkbox" />
全て完了にする
</label>
<select>
<option>全て</option>
<option>未完了</option>
<option>完了済み</option>
</select>
<ul>
// ----- Todoに修正 -----
<li>
<Todo id={0} text="朝ごはんを食べる" />
</li>
<li>
<Todo id={0} text="筋トレをする" />
</li>
<li>
<Todo id={0} text="勉強をする" />
</li>
</ul>
<button>完了済みを全て削除</button>
</div>
)
};
}
export default App;
*タスク追加機能の実装
入力したタスクを動的に追加できるようForm.js
を修正します。追加ボタンが押されたときに動作する
handleSubmit()
を新規作成し、これを form タグのonSubmit
イベントで呼び出すようにします。form を
submit
するとページ遷移してしまうので、その挙動をとめるためにhandleSubmit()
のなかでpreventDefault()
を使います。タスクの追加後は入力フォームが空の状態にしたいので、
this.setState({ input: '' })
で初期化しています。<src/components/Form.js>
import React from 'react';
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
input: ''
};
}
render() {
return (
// ----- onSubmitを追加 -----
<form onSubmit={this.handleSubmit}>
<input type="text" value={this.state.input} onChange={this.handleChange} />
<button>追加</button>
</form>
);
}
handleChange = e => {
this.setState({ input: e.currentTarget.value })
};
// ----- 追加 -----
handleSubmit = e => {
e.preventDefault();
this.setState({ input: '' })
}
}
export default Form;
追加したタスクを管理できるようにするため、
App.js
にstate
を追加します。constructor()
を追加しthis.state
でタスクのリストを定義します。次に Todo コンポーネントを呼び出す ul タグの中で
state.todos
を使うように修正します。画面に表示される情報は今までと同じ状態です。
<src/components/App.js>
import React from 'react';
import Form from './Form';
import Todo from './Todo';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
todos: [
{
id: 0,
text: "朝ごはんを食べる"
},
{
id: 1,
text: "筋トレをする"
},
{
id: 2,
text: "勉強をする"
}
]
};
}
render() {
return (
<div>
<Form />
<label>
<input type="checkbox" />
全て完了にする
</label>
<select>
<option>全て</option>
<option>未完了</option>
<option>完了済み</option>
</select>
<ul>
{this.state.todos.map(({ id, text }) => (
<li key={id}>
<Todo text={text}/>
</li>
))}
</ul>
<button>完了済みを全て削除</button>
</div>
)
};
}
export default App;
タスクを
state
に置き換えることができたので、次は入力された値が実際に表示されるようApp.js
を修正します。サンプルとして作成していた
todos
のリストの値を削除します。<src/components/App.js>
...
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
// ----- データを削除 -----
todos: []
};
}
...
さらに
App.js
に、追加ボタンが押されたときの処理handleSubmit()
を実装します。追加タスクの ID を連番にしたいので
currentId
を定義し、タスク追加後に +1 していきます。...this.state.todos
では今まで入力されてたタスクの配列を展開しています。この配列の末尾に新しいタスクを追加し、新しい配列としてstate
に登録します。props
を使いonSubmit
として Form コンポーネントにhandleSubmit()
を渡すようにします。(これをForm コンポーネントから呼び出すようにします)<src/components/App.js>
import React from 'react';
import Form from './Form';
import Todo from './Todo';
// ----- 追加 -----
let currentId = 0;
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
todos: []
};
}
render() {
return (
<div>
// ----- onSubmitを追加 -----
<Form onSubmit={this.handleSubmit} />
<label>
<input type="checkbox" />
全て完了にする
</label>
<select>
<option>全て</option>
<option>未完了</option>
<option>完了済み</option>
</select>
<ul>
{this.state.todos.map(({ id, text }) => (
<li key={id}>
<Todo text={text}/>
</li>
))}
</ul>
<button>完了済みを全て削除</button>
</div>
);
}
// ----- 追加 -----
handleSubmit = text => {
const newTodo = {
id: currentId,
text: text,
}
const newTodos = [...this.state.todos, newTodo]
this.setState({ todos: newTodos })
currentId++;
};
}
export default App;
Form.js
を修正します。props
を受け取り、App.js
のhandleSubmit()
を呼び出すようにします。<src/components/Form.js>
import React from 'react';
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
input: ''
};
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="text" value={this.state.input} onChange={this.handleChange} />
<button>追加</button>
</form>
);
}
handleChange = e => {
this.setState({ input: e.currentTarget.value })
};
handleSubmit = e => {
e.preventDefault();
// ----- onSubmit()の呼び出しを追加 -----
this.props.onSubmit(this.state.input);
this.setState({ input: '' })
}
}
export default Form;
ここまで実装すると、入力したタスクが画面に表示されるようになります。
*完了チェックボックスの実装
チェックボックスにチェックを入れたタスクは、完了したタスクとして管理できるようにしたいため、state
で管理する値にチェックの状態も追加します。App.js
のhandleSubmit()
で新しいタスクをstate
に登録しているので、ここにcompleted
の値を追加します。(初期値はfalse
)次に、Todo コンポーネントに
completed
も渡すようprops
に追加します。さらにチェックボックスに変更があったときに動作するhandleChangeCompleted()
を作成し、Todo コンポーネントのonChange
イベントとして設定します。<src/components/App.js>
import React from 'react';
import Form from './Form';
import Todo from './Todo';
let currentId = 0;
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
todos: []
};
}
render() {
return (
<div>
<Form onSubmit={this.handleSubmit} />
<label>
<input type="checkbox" />
全て完了にする
</label>
<select>
<option>全て</option>
<option>未完了</option>
<option>完了済み</option>
</select>
<ul>
{this.state.todos.map(({ id, text, completed }) => (
<li key={id}>
// ----- completed,onChangeを追加 -----
<Todo id={id} text={text} completed={completed} onChange={this.handleChangeCompleted} />
</li>
))}
</ul>
<button>完了済みを全て削除</button>
</div>
);
}
handleSubmit = text => {
const newTodo = {
id: currentId,
text: text,
completed: false
}
const newTodos = [...this.state.todos, newTodo]
this.setState({ todos: newTodos })
currentId++;
}
// ----- 追加 -----
handleChangeCompleted = (id, completed) => {
const newTodos = this.state.todos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed
}
}
return todo
})
this.setState({ todos: newTodos})
}
}
export default App;
Todo.js
を修正します。handleChangeCompleted()
を追加してチェックボックスに変更があったときに動くようonChange
イベントとして設定します。props
のcompleted
には現在の値が入っているので、これを逆の値に修正してApp.js
のhandleChangeCompleted()
を実行します。<src/components/Todo.js>
import React from 'react';
class Todo extends React.Component {
render() {
const { text, completed } = this.props
return (
<div>
<label>
// ----- checked,onChangeを追加 -----
<input type="checkbox" checked={completed} onChange={this.handleChangeCompleted} />
{text}
</label>
<button>編集</button>
<button>削除</button>
</div>
)
}
// ----- 追加 -----
handleChangeCompleted = () => {
const { onChange, id, completed } = this.props;
onChange(id, !completed);
}
}
export default Todo;
*一括チェックボックスの実装
一括チェックボックスがチェックされると全タスクにチェックが入り、一括チェックボックスのチェックがはずれると全タスクのチェックがはずれる機能を実装します。また、全タスクにチェックを入れたときに、一括チェックボックスにもチェックが入るようにします。(表示も変更します)
components/CheckAll.js
を新規作成します。props
として一括チェックボックスの値allCompleted
を受け取るようにします。javaScript の仕様で、input タグでchecked
を使う場合は必ずonChange
を指定する必要があります。<src/components/CheckAll.js>
import React from 'react';
class CheckAll extends React.Component {
render() {
const { allCompleted } = this.props
return (
<label>
<input type="checkbox" checked={allCompleted} onChange={this.handleChange} />
全て{allCompleted ? '未完了' : '完了'}にする
</label>
);
}
handleChange = () => {
const { onChange, allCompleted } = this.props;
onChange(!allCompleted)
}
}
export default CheckAll;
App.js
を修正します。handleChangeAllCompleted()
を作成し、CheckAll コンポーネントから呼び出すようprops
のonChange
として渡します。every
というのは配列が条件をすべて満たす場合にtrue
を返す javaScript の機能です。初期表示時(todosが空)にこの条件がtrue
になってチェックが付いてしまうため、todos.length > 0
で条件を追加しています。<src/components/App.js>
import React from 'react';
import Form from './Form';
import Todo from './Todo';
// ----- 追加 -----
import CheckAll from './CheckAll';
let currentId = 0;
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
todos: []
};
}
render() {
// ----- stateを取得 -----
const { todos } = this.state;
return (
<div>
<Form onSubmit={this.handleSubmit} />
// ----- CheckAllに置き換え -----
<CheckAll
allCompleted={todos.length > 0 && todos.every(({ completed }) => completed)}
onChange={this.handleChangeAllCompleted} />
<select>
<option>全て</option>
<option>未完了</option>
<option>完了済み</option>
</select>
<ul>
{this.state.todos.map(({ id, text, completed }) => (
<li key={id}>
<Todo id={id} text={text} completed={completed} onChange={this.handleChangeCompleted} />
</li>
))}
</ul>
<button>完了済みを全て削除</button>
</div>
);
}
handleSubmit = text => {
const newTodo = {
id: currentId,
text: text,
completed: false
}
const newTodos = [...this.state.todos, newTodo]
this.setState({ todos: newTodos })
currentId++;
}
// ----- 追加 -----
handleChangeAllCompleted = completed => {
const newTodos = this.state.todos.map(todo => ({
...todo,
completed
}));
this.setState({ todos: newTodos });
}
handleChangeCompleted = (id, completed) => {
const newTodos = this.state.todos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed
}
}
return todo
})
this.setState({ todos: newTodos})
}
}
export default App;
*完了タスク一括削除ボタンの実装
一括削除ボタンを押すと、完了チェックボックスにチェックされていたタスクが全て削除されるようにします。単純な処理なため、コンポーネント化はしないでApp.js
に処理を追加しました。App.js
にhandleClickDeleteCompleted()
を追加し、filter()
で完了していないタスクのみを取得して新しい配列としてstate
に保存します。このメソッドをボタンが押されたときに呼び出すようonClick
イベントに登録します。<src/components/App.js>
...
// ----- onClickを追加 -----
<button onClick={this.handleClickDeleteCompleted}>完了済みを全て削除</button>
...
// ----- 追加 -----
handleClickDeleteCompleted = () => {
const newTodos = this.state.todos.filter(({ completed }) => !completed)
this.setState({ todos: newTodos })
}
}
*フィルター機能の実装
タスクをセレクトボックスの「全て、未完了、完了済み」で絞り込んで表示できるようにします。components/Filter.js
を新規作成します。セレクトボックスの値をそれぞれ
value
で定義しておく必要があります。props
でセレクトボックスの値を受け取り、変更があった場合にApp.js
からonChange
として渡されたメソッドを呼び出します。<src/components/Filter.js>
import React from "react";
class Fileter extends React.Component {
render() {
const { filter } = this.props;
return (
<select value={filter} onChange={this.handleChange}>
<option value="all">全て</option>
<option value="uncompleted">未完了</option>
<option value="completed">完了済み</option>
</select>
);
}
handleChange = e => {
this.props.onChange(e.currentTarget.value);
};
}
export default Fileter;
App.js
を修正します。セレクトボックスの部分を Filter コンポーネントに置き換えます。
また、セレクトボックスの値を管理できるようにしたいため、
state
にfilter
の値を追加します。(初期値はall
)次に
handleChangeFilter()
を追加し、state
のfilter
を更新するようにします。このメソッドを Filter コンポーネントにprops
で渡して呼び出してもらうようにします。filter()
した結果となるfilteredTodos
を新しく作成し、これを読み込んで画面に表示させます。<src/components/App.js>
import React from "react";
import Form from "./Form";
import Todo from "./Todo";
import CheckAll from "./CheckAll";
// ----- 追加 -----
import Filter from "./Filter";
let currentId = 0;
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
// ----- 追加 -----
filter: "all",
todos: []
};
}
render() {
// ---- フィルターする値を取得 ----
const { todos, filter } = this.state;
// ---- フィルターしたリストを取得 ----
const filteredTodos = todos.filter(({ completed }) => {
switch (filter) {
case "all":
return true;
case "uncompleted":
return !completed;
case "completed":
return completed;
default:
return true;
}
});
return (
<div>
<Form onSubmit={this.handleSubmit} />
<CheckAll
allCompleted={
todos.length > 0 && todos.every(({ completed }) => completed)
}
onChange={this.handleChangeAllCompleted}
/>
// ----- 追加 -----
<Filter filter={filter} onChange={this.handleChangeFilter} />
<ul>
// ----- フィルター後のリストに修正 -----
{filteredTodos.map(({ id, text, completed }) => (
<li key={id}>
<Todo
id={id}
text={text}
completed={completed}
onChange={this.handleChangeCompleted}
/>
</li>
))}
</ul>
<button onClick={this.handleClickDeleteCompleted}>
完了済みを全て削除
</button>
</div>
);
}
handleSubmit = text => {
const newTodo = {
id: currentId,
text: text,
completed: false
};
const newTodos = [...this.state.todos, newTodo];
this.setState({ todos: newTodos });
currentId++;
};
handleChangeAllCompleted = completed => {
const newTodos = this.state.todos.map(todo => ({
...todo,
completed
}));
this.setState({ todos: newTodos });
};
// ---- 追加 ----
handleChangeFilter = filter => {
this.setState({ filter });
};
handleChangeCompleted = (id, completed) => {
const newTodos = this.state.todos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed
};
}
return todo;
});
this.setState({ todos: newTodos });
};
handleClickDeleteCompleted = () => {
const newTodos = this.state.todos.filter(({ completed }) => !completed);
this.setState({ todos: newTodos });
};
}
export default App;
*削除機能を実装
タスクにそれぞれ付いている削除ボタンを押すと、そのタスクが削除されるようにします。Todo.js
を修正します。handleClickDelete()
を作成し、削除ボタンが押されたときにonClick
イベントとして呼び出すように設定します。どのタスクを削除するのか知る必要があるので、
props
でid
を受け取ります。props
として受け取ったonDelete
のメソッドを呼び出すようにします。<src/components/Todo.js>
import React from "react";
class Todo extends React.Component {
render() {
const { text, completed } = this.props;
return (
<div>
<label>
<input
type="checkbox"
checked={completed}
onChange={this.handleChangeCompleted}
/>
{text}
</label>
<button>編集</button>
// ----- onClickを追加 -----
<button onClick={this.handleClickDelete}>削除</button>
</div>
);
}
handleChangeCompleted = () => {
const { onChange, id, completed } = this.props;
onChange(id, !completed);
};
// ----- 追加 -----
handleClickDelete = () => {
const { onDelete, id } = this.props;
onDelete(id);
};
}
export default Todo;
App.js
を修正します。handleClickDelete()
を作成し、削除するタスク以外で新しい配列を作成してstate
に登録します。さらに Todo コンポーネントから呼び出してもらえるよう
props
のonDelete
としてこのメソッドを渡します。<src/components/App.js>
<ul>
{filteredTodos.map(({ id, text, completed }) => (
<li key={id}>
<Todo
id={id}
text={text}
completed={completed}
onChange={this.handleChangeCompleted}
// ----- onDeleteを追加 -----
onDelete={this.handleClickDelete}
/>
</li>
))}
</ul>
...
// ----- 追加 -----
handleClickDelete = id => {
const newTodos = this.state.todos.filter(todo => todo.id !== id);
this.setState({ todos: newTodos });
};
*編集機能の実装
タスクにそれぞれ付いている編集ボタンを押すと、そのタスク名を更新できるようにします。編集時は別コンポーネントにしたいため、
components/EditTodo.js
を新規作成します。編集中にキャンセルボタンを押すと元に戻り、更新ボタンを押すと新しく入力されたタスク名に更新されます。
<src/components/EditTodo.js>
import React from "react";
class EditTodo extends React.Component {
constructor(props) {
super(props);
this.state = {
text: props.text
};
}
render() {
return (
<div>
<input
type="text"
value={this.state.text}
onChange={this.handleChange}
/>
<button onClick={this.handleClickCancel}>キャンセル</button>
<button onClick={this.handleSubmit}>更新</button>
</div>
);
}
handleChange = e => {
this.setState({ text: e.currentTarget.value });
};
handleClickCancel = () => {
const { onCancel, id } = this.props;
onCancel(id, "editing", false);
};
handleSubmit = () => {
const { onSubmit, id } = this.props;
if (!this.props.text) return;
onSubmit(id, this.state.text);
};
}
export default EditTodo;
App.js
を修正します。タスクの登録時に動作する
handleSubmit()
で、state
として保存するtodos
の値にediting
を追加します。(初期値はfalse
)このediting
がtrue
のときに EditTodo コンポーネントが呼び出されるようにします。handleUpdateTodoText()
を作成して EditTodo コンポーネントから呼び出されるようprops
で渡します。編集後はediting
の値は必ずfalse
になります。Todo コンポーネントの
onChange
イベントで使っていたhandleChangeCompleted()
を EditTodo コンポーネントでも使えるようにしたいため、メソッド名をhandleChangeTodoAttribute()
に修正しkey
とvalue
で更新できるように引数と処理を修正します。<src/components/App.js>
import React from "react";
import Form from "./Form";
import Todo from "./Todo";
import CheckAll from "./CheckAll";
import Filter from "./Filter";
// ----- 追加 -----
import EditTodo from "./EditTodo";
let currentId = 0;
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
filter: "all",
todos: []
};
}
render() {
const { todos, filter } = this.state;
const filteredTodos = todos.filter(({ completed }) => {
switch (filter) {
case "all":
return true;
case "uncompleted":
return !completed;
case "completed":
return completed;
default:
return true;
}
});
return (
<div>
<Form onSubmit={this.handleSubmit} />
<CheckAll
allCompleted={
todos.length > 0 && todos.every(({ completed }) => completed)
}
onChange={this.handleChangeAllCompleted}
/>
<Filter filter={filter} onChange={this.handleChangeFilter} />
<ul>
{filteredTodos.map(({ id, text, completed, editing }) => (
<li key={id}>
// ----- 分岐処理を追加 -----
{editing ? (
<EditTodo
id={id}
text={text}
onCancel={this.handleChangeTodoAttribute}
onSubmit={this.handleUpdateTodoText}
/>
) : (
<Todo
id={id}
text={text}
completed={completed}
onChange={this.handleChangeTodoAttribute}
onDelete={this.handleClickDelete}
/>
)}
</li>
))}
</ul>
<button onClick={this.handleClickDeleteCompleted}>
完了済みを全て削除
</button>
</div>
);
}
handleSubmit = text => {
// ----- editingを追加 -----
const newTodo = {
id: currentId,
text: text,
completed: false,
editing: false
};
const newTodos = [...this.state.todos, newTodo];
this.setState({ todos: newTodos });
currentId++;
};
handleChangeAllCompleted = completed => {
const newTodos = this.state.todos.map(todo => ({
...todo,
completed
}));
this.setState({ todos: newTodos });
};
handleChangeFilter = filter => {
this.setState({ filter });
};
// ----- key,valueで更新できるよう修正 -----
handleChangeTodoAttribute = (id, key, value) => {
const newTodos = this.state.todos.map(todo => {
if (todo.id === id) {
return {
...todo,
[key]: value
};
}
return todo;
});
this.setState({ todos: newTodos });
};
handleUpdateTodoText = (id, text) => {
const newTodo = this.state.todos.map(todo => {
if (todo.id === id) {
return {
...todo,
text,
editing: false
};
}
return todo;
});
this.setState({ todos: newTodo });
};
handleClickDelete = id => {
const newTodos = this.state.todos.filter(todo => todo.id !== id);
this.setState({ todos: newTodos });
};
handleClickDeleteCompleted = () => {
const newTodos = this.state.todos.filter(({ completed }) => !completed);
this.setState({ todos: newTodos });
};
}
export default App;
Todo.js
を修正します。handleClickEdit()
を作成し、編集ボタン押下時に動作するようonClick
イベントに設定します。App.js
での修正に伴い、handleChangeCompleted()
のonChange
の引数がid, key, value
になるよう修正します。<src/components/Todo.js>
import React from "react";
class Todo extends React.Component {
render() {
const { text, completed } = this.props;
return (
<div>
<label>
<input
type="checkbox"
checked={completed}
onChange={this.handleChangeCompleted}
/>
{text}
</label>
<button onClick={this.handleClickEdit}>編集</button>
<button onClick={this.handleClickDelete}>削除</button>
</div>
);
}
handleChangeCompleted = () => {
const { onChange, id, completed } = this.props;
onChange(id, "completed", !completed);
};
handleClickEdit = () => {
const { onChange, id, editing } = this.props;
onChange(id, "editing", !editing);
};
handleClickDelete = () => {
const { onDelete, id } = this.props;
onDelete(id);
};
}
export default Todo;
補足ですが、何も入力していない状態でタスクを追加できないように
Form.js
のhandleSubmit()
に処理を追加します。<src/components/Form.js>
import React from "react";
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
input: ""
};
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
type="text"
value={this.state.input}
onChange={this.handleChange}
/>
<button>追加</button>
</form>
);
}
handleChange = e => {
this.setState({ input: e.currentTarget.value });
};
handleSubmit = e => {
e.preventDefault();
// ----- if文を追加 -----
if (!this.state.input) return;
this.props.onSubmit(this.state.input);
this.setState({ input: "" });
};
}
export default Form;
完成した画面です。
*所感
動画を参考にさせていただいたのですが、コンポーネントの分割方法など順を追って進めてくださり、React について丁寧に解説もしてくださったので大変わかりやすかったです。業務では Vue.js を使っており Vue のほうが簡単だと思っていましたが、コンポーネントに分割するという面では React も Vue と同様で、DOM要素を直接書ける React も使いやすいと思ってきました。
今回は Redux を使わなかったので React に集中して学べたこともあり、基礎的な部分についてはかなり理解が深まりました。Redux や スタイルの導入などについても理解を深めていきたいと思います。
Sign up here with your email
ConversionConversion EmoticonEmoticon