はじめに
create-react-appはReactで簡単にアプリを作成するためのツールです。
Reactのみで簡単なTodoアプリを作成したので備忘録としてまとめたいと思います。
なお、多少の環境構築が必要なので、よろしければこの記事を参考にしてください。
また今回はJSX、props、useStateの知識を扱います。この記事では説明を省きますので
ご了承ください。
挙動イメージ
- 1. Todoを入力したら未完了のTodoに反映される
- 2. 未完了のTodoは完了のTodoに移動できる or 削除できる
- 3. 完了のTodoは未完了のTodoに移動できる
目次
1.create-react-app
2.HTMLとCSSで構造を作成
3.Reactを意識したモックに変更
4.Todoの入力機能
5.Todoの追加機能
6.Todoの削除機能
7.Todoの完了機能
8.Todoの戻す機能
9.コンポーネント化
1. create-react-app
環境構築が終了したらターミナルでcreate-react-app
を実行しましょう。
projects $ npx create-react-app 任意のフォルダ名(今回はreact-todo)
projects $ cd react-todo
// サーバー立ち上げ
react-todo $ npm start
ブラウザが立ち上がってReactの初期画面が表示されれば成功です!
2. HTMLとCSSで構造を作成
まずは、イメージの通りに骨組みを作成します。
srcディレクトリの必要なファイルは以下通りです。
App.js
はコンポーネントと明示的にわかるようにApp.jsx
と拡張子を変更しましょう。
src
|-App.jsx
|-index.js
|-App.css
次にindex.js
を編集します。
import React from 'react';
import ReactDom from 'react-dom';
import { App } from './App';
ReactDom.render(<App />, document.getElementById('root'));
上から2行はおまじないです。
3行目はApp.jsx
のAppという関数をimportしますという意味です。
最後にrender関数でAppの中身をindex.htmlのidがrootのdivタグに格納しています。
準備が整ったのでメインのApp.jsxを編集します。
import React from 'react';
// App.cssを読み込み
import './App.css';
export const App = () => {
return (
<>
<div className="outline">
<div className="inputArea">
<input placeholder="todoを入力"/>
<button>追加</button>
</div>
<div className="imcompleteArea">
<p className="title">未完了のtodo</p>
<ul>
<li className="todoList">
<p>テスト</p>
<button>完了</button>
<button>削除</button>
</li>
</ul>
</div>
<div className="completeArea">
<p className="title2">完了のtodo</p>
<ul>
<li className="todoList">
<p>テスト</p>
<button>戻す</button>
</li>
</ul>
</div>
</div>
</>
);
};
構成としては大きくinputArea
、imcompleteArea
、completeArea
に分かれます。
Todoが格納させるエリアにはリストタグで個々のtodoにボタンで操作できるようにしておきます。
ポイント
- return内が複数行になるときは
<> </>
で全体をくくる - JSXでclassを付与するときはclassName記述する
次にcssですが、ここはお好みで調整可能です。
記述方法は通常のcssとかわりません。
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
li {
list-style: none;
}
.outline {
padding: 50px;
}
.inputArea {
width: 300px;
height: 70px;
background-color: lightblue;
padding: 20px;
margin-bottom: 50px;
border-radius: 15px;
}
input {
border-radius: 16px;
border: none;
outline: none;
padding: 6px 16px;
margin-right: 10px;
}
button {
border-radius: 16px;
border: none;
padding: 4px 16px;
}
button:hover {
background-color: #FF7FFF;
color: #FFF;
cursor: pointer;
}
/* 未完了のタスクエリア */
.imcompleteArea {
width: 800px;
min-height: 300px;
background-color: lightgray;
padding: 20px;
margin-bottom: 50px;
border-radius: 15px;
}
.title {
text-align: center;
font-weight: bold;
}
.todoList {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: space-evenly;
}
/* 完了のタスクエリア */
.completeArea {
width: 800px;
min-height: 300px;
background-color: lightgreen;
padding: 20px;
border-radius: 15px;
}
.title2 {
text-align: center;
font-weight: bold;
}
以上で骨組みはできました。
3. Reactを意識したモックに変更
現状のApp.jsx内では未完了と完了のtodoはpタグに直接書き込まれています。
しかし、本来は動的にtodoの内容は反映してほしいのでuseState
を用いて状態を管理していきたいと思います。
// Reactの部分を{ useState }に編集
import { useState } from 'react';
import './App.css';
export const App = () => {
const [incompleteTodos, setIncompleteTodos] = useState(["宿題", "洗濯"]);
return (
:
:
);
};
const [incompleteTodos, setIncompleteTodos]
未完了のtodoの状態を管理するための配列です。incompleteTodos
が初期値。setIncompleteTodos
が更新するための変数です。
useState(["宿題", "洗濯"])
は初期値のincompleteTodosに宿題と洗濯という2つの値を渡しています。
次にreturn内を編集していきます。まずは未完了のtodoからです。
return (
:
:
<div className="imcompleteArea">
<p className="title">未完了のtodo</p>
<ul>
<li className="todoList">
<p>テスト</p>
<button>完了</button>
<button>削除</button>
</li>
</ul>
</div>
:
:
);
↓ // 変更
<div className="imcompleteArea">
<p className="title">未完了のtodo</p>
<ul>
{incompleteTodos.map((todo) => {
return (
<li key={todo} className="todoList">
<p>{todo}</p>
<button>完了</button>
<button>削除</button>
</li>
);
})}
</ul>
</div>
順番に解説していきます。
まずmap関数
を用いて先程定義したincompleteTodos
の中身を繰り返し処理で表示させます。その際の引数はtodoとします。
key={todo}
と記述したのは繰り返し処理によるレンダリングでどのtodoかを判別するためです。最後にpタグの中身は{ }
でtodoを囲んで表示させます。
ポイント
- return内でJavascriptを扱う際は{ }で囲む
同じようにして完了のtodoもuseStateを用いて状態を管理します。
completeArea
を以下のように編集してください。
export const App = () => {
const [incompleteTodos, setIncompleteTodos] = useState(["宿題", "洗濯"]);
// 追記
const [completeTodos, setCompleteTodos] = useState(["掃除"]);
return (
:
:
<div className="completeArea">
<p className="title2">完了のtodo</p>
<ul>
{completeTodos.map((todo) => {
return (
<li key={todo} className="todoList">
<p>{todo}</p>
<button>戻す</button>
</li>
);
})}
</ul>
</div>
);
};
これで準備が整いました!
それでは順番に機能を実装していきたいと思います。
4. Todoの入力機能
まずは、入力されたtodoの状態を管理するためのuseStateを用意します。
初期値は''
で空にしておきましょう。
export const App = () => {
// 追記
const [todoText, setTodoText] = useState('');
};
入力されたtodoText
はinputタグのvalueとして設定します。
{ }
で変数を囲むのを忘れないでください。
<div className="inputArea">
<input placeholder="todoを入力" value={todoText} />
<button>追加</button>
</div>
この状態だと入力欄にテキストを打ち込んでも反応しません。それは初期値の''
が常に渡されているためです。
そこでonChangeメソッド
を用いてtodoText
の値を更新する関数を作成する必要があります。
inputタグを次のように編集しましょう。
<div className="inputArea">
<input placeholder="todoを入力" value={todoText} onChange={onChangeTodoText}/>
<button>追加</button>
</div>
inputにtodoが入力されたら、onChangeメソッドでonChangeTodoText
という関数を呼び出しています。この関数の中でtodoの値を更新する処理を記述していきます。
では、onChangeTodoText関数を定義します。関数はreturnの前に定義しましょう。
export const App = () => {
// 関数の定義
const onChangeTodoText = (event) => setTodoText(event.target.value);
};
onChangeTodoTextは引数を取ります。今回はevent
を取ります。
event.target.value
は入力されたtodoの値のことです。これをsetTodoText関数の引数として渡すことで入力された値がsetTodoTextとして更新されます。
5. Todoの追加機能
4までの内容でtodoを入力することはできました。
次に追加ボタンを押してtodoを未完了エリアに移動させる機能を実装します。
buttonタグにonClickメソッド
でボタンを押したときに関数を呼び出します。
今回はonClickAdd
という関数名にします。
<div className="inputArea">
<input placeholder="todoを入力" value={todoText} onChange={onChangeTodoText} />
<button onClick={onClickAdd}>追加</button>
</div>
ではonClickAddの中身を記述しましょう。
これもreturnの前に記述します。
const onClickAdd = () => {
if (todoText === '') return;
const newTodos = [...incompleteTodos, todoText];
setIncompleteTodos(newTodos);
setTodoText('');
}
2行目のifは空の入力は無効にするという意味です。
3行目の[...incompleteTodos, todoText]
は配列のコピーをしています。
[...変数名]
と記述するとその変数の値をコピーできます。すでにある未完了のtodoに新しく入力したtodoText
を加えて、新しくnewTodos
という変数に格納しています。
4行目は未完了のtodoのstateを更新したいのでnewTodosをsetIncompleteTodos関数
の引数に渡して値を更新します。
5行目は追加ボタンを押した際に入力欄をリセットしたいのでsetTodoText関数
を空にします。
6. Todoの削除機能
未完了エリアのtodoを削除する機能を実装します。方針は次の通りです。
1. 削除ボタンにonClickメソッドを設定して関数を呼び出す
2. 削除ボタンを押したら未完了エリアから選んだtodoが削除される
削除ボタンにonClickメソッドを設定
todoを削除する関数をonClickDelete
とします。
<div className="imcompleteArea">
<p className="title">未完了のtodo</p>
<ul>
{incompleteTodos.map((todo, index) => {
return (
<li key={todo} className="todoList">
<p>{todo}</p>
<button }>完了</button>
<button onClick={() => onClickDelete(index)>削除</button>
</li>
);
})}
</ul>
</div>
ポイントとしてはmap関数に新しくindex
を引数に設定しています。
これはtodoを削除する際に何番目のtodoかを判別する際に必要です。関数の中でもindexは必要なのでonClickDeleteの引数にもindexを渡します。
関数の定義
例によってreturnの前に記述しましょう。
const onClickDelete = (index) => {
const newTodos = [...incompleteTodos];
// newTodosから指定のindex番号を1つ削除する
newTodos.splice(index, 1);
setIncompleteTodos(newTodos);
};
2行目で現在の未完了のtodoをnewTodos
という変数に格納します。
3行目でspliceメソッド
を使用しています。spliceは配列の要素を操作できるメソッドで要素の追加や削除することができます。第一引数に削除対象のindexを渡して第二引数は指定したindexから何番目までの要素を操作するかを設定します。(今回は1なのでつまり、指定したtodoのみを削除します。)
4行目で削除し終えた未完了のtodoの状態を更新するためにsetIncompleteTodos関数
にnewTodosを渡して値を更新します。
7. Todoの完了機能
未完了のtodoを完了エリアに移動させます。方針は以下の通りです。
1. 完了ボタンにonClickメソッドを設定して関数を呼び出す
2. 完了ボタンを押したら未完了エリアからtodoが削除される
3. 完了ボタンを押したら削除されたtodoが完了エリアに追加される
完了ボタンにonClickメソッドを設定
関数名はonClickComplete
とします。
<div className="imcompleteArea">
<p className="title">未完了のtodo</p>
<ul>
{incompleteTodos.map((todo, index) => {
return (
<li key={todo} className="todoList">
<p>{todo}</p>
<button onClick={() => onClickComplete(index)}>完了</button>
<button onClick={() => onClickDelete(index)}>削除</button>
</li>
);
})}
</ul>
</div>
onClickComplete
にも、どのtodoを完了にするか判別するために引数としてindexを渡しましょう。
関数を定義
const onClickComplete = (index) => {
const newIncompleteTodos = [...incompleteTodos];
// newTodosから指定のindex番号を1つ削除する
newIncompleteTodos.splice(index, 1);
const newCompleteTodos = [...completeTodos, incompleteTodos[index]];
setIncompleteTodos(newIncompleteTodos);
setCompleteTodos(newCompleteTodos);
};
2~4行目で未完了エリアから選んだtodoを削除しています。[...incompleteTodos]
で現在の未完了のtodoをコピーして、変数newIncompleteTodos
に格納します。次にspliceメソッドでtodoを削除します。
6~8行目で選んだ未完了エリアと完了エリアにtodoを更新してます。
[...completeTodos, incompleteTodos[index]]
で完了エリアに現在の完了のtodoであるcompleteTodos
をコピーし、選んだ未完了のtodoであるincompleteTodos[index]
を追加し
変数newCompleteTodos
に格納します。
最後に未完了エリアのtodoの値と完了エリアのtodoの値をuseStateで更新しています。
8. Todoの戻す機能
機能としてはこれで最後です。方針は以下の通りです。
1. 戻すボタンにonClickメソッドを設定し関数を呼び出す
2. 戻すボタンを押したら完了エリアから選んだtodoを削除する
3. 戻すボタンを押したら未完了エリアに選んだtodoを追加する
完了機能の逆ですね
戻すボタンにonClickメソッドを設定
関数名はonClickBack
とします。
<div className="completeArea">
<p className="title2">完了のtodo</p>
<ul>
{completeTodos.map((todo, index) => {
return (
<li key={todo} className="todoList">
<p>{todo}</p>
<button onClick={() => onClickBack(index)}>戻す</button>
</li>
);
})}
</ul>
</div>
例によってmap関数とonClickBack関数にindexを引数として渡します。
関数を定義
const onClickBack = (index) => {
const newCompleteTodos = [...completeTodos];
newCompleteTodos.splice(index, 1);
const newIncompleteTodos = [...incompleteTodos, completeTodos[index]];
setCompleteTodos(newCompleteTodos);
setIncompleteTodos(newIncompleteTodos);
};
2~3行目で完了エリアから選んだtodoを削除します。
5~7行目は完了機能の逆なので説明は割愛します。
これで一応機能は実装しました。最終的なコードはこんな感じです。
import { useState } from 'react';
import './App.css';
export const App = () => {
const [todoText, setTodoText] = useState('');
const [incompleteTodos, setIncompleteTodos] = useState([]);
const [completeTodos, setCompleteTodos] = useState([]);
const onChangeTodoText = (event) => setTodoText(event.target.value);
const onClickAdd = () => {
if (todoText === '') return;
const newTodos = [...incompleteTodos, todoText];
setIncompleteTodos(newTodos);
setTodoText('');
}
// タスクを削除する
const onClickDelete = (index) => {
const newTodos = [...incompleteTodos];
// newTodosから指定のindex番号を1つ削除する
newTodos.splice(index, 1);
setIncompleteTodos(newTodos);
};
// 未完了のタスクを完了タスクへ
const onClickComplete = (index) => {
const newIncompleteTodos = [...incompleteTodos];
// newTodosから指定のindex番号を1つ削除する
newIncompleteTodos.splice(index, 1);
const newCompleteTodos = [...completeTodos, incompleteTodos[index]];
setIncompleteTodos(newIncompleteTodos);
setCompleteTodos(newCompleteTodos);
};
// 完了のタスクを未完了のタスクへ
const onClickBack = (index) => {
const newCompleteTodos = [...completeTodos];
newCompleteTodos.splice(index, 1);
const newIncompleteTodos = [...incompleteTodos, completeTodos[index]];
setCompleteTodos(newCompleteTodos);
setIncompleteTodos(newIncompleteTodos);
};
return (
<>
<div className="outline">
<div className="inputArea">
<input placeholder="todoを入力" value={todoText} onChange={onChangeTodoText} />
<button onClick={onClickAdd}>追加</button>
</div>
<div className="imcompleteArea">
<p className="title">未完了のtodo</p>
<ul>
{incompleteTodos.map((todo, index) => {
return (
<li key={todo} className="todoList">
<p>{todo}</p>
<button onClick={() => onClickComplete(index)}>完了</button>
<button onClick={() => onClickDelete(index)}>削除</button>
</li>
);
})}
</ul>
</div>
<div className="completeArea">
<p className="title2">完了のtodo</p>
<ul>
{completeTodos.map((todo, index) => {
return (
<li key={todo} className="todoList">
<p>{todo}</p>
<button onClick={() => onClickBack(index)}>戻す</button>
</li>
);
})}
</ul>
</div>
</div>
</>
);
};
それでは、ここからコンポーネント化をしてコードをスッキリさせたいと思います。
9. コンポーネント化
今回はインプットエリア、未完了エリア、完了エリアの3つにコンポーネント化していきます。
まずはインプットエリアから始めます。src配下にcomponents
ディレクトリを作成し、InputTodo.jsx
というファイルを作成します。
src
|-components
|-InputTodo.jsx
|-index.js
|-App.jsx
|-App.css
inputAreaのコンポーネント化
それではInputTodo.jsxに必要な記述をしていきます。
App.jsxのreturn内のinputArea
の中身をInputTodo.jsxのreturn内に切り抜きします。
また、App.jsxでこのファイルを使用できるようにexportします。
import React from 'react';
export const InputTodo = () => {
return (
<div className="inputArea">
<input placeholder="todoを入力" value={todoText} onChange={onChangeTodoText} />
<button onClick={onClickAdd}>追加</button>
</div>
);
};
次にApp.jsxを編集します。
InputTodo.jsxをimportします。
import { useState } from 'react';
import './App.css';
// 追記
import { InputTodo } from './components/InputTodo';
:
:
return (
<div className="outline">
<InputTodo />
<div className="imcompleteArea">
:
:
);
このままではInputTodo.jsx内にtodoText
やonChangeTodoText
などの変数や関数が設定されてないのでエラーが起きてしまいます。なのでpropsを用いてInputTodo.jsxに必要な要素を渡します。
:
:
return (
:
:
<InputTodo
todoText={todoText}
onChange={onChangeTodoText}
onClick={onClickAdd}
/>
);
InputTodoに3つのpropsを設定します。
それぞれtodoText
、onChange
、onClick
とします。
import React from 'react';
export const InputTodo = (props) => {
const { todoText, onChange, onClick } = props;
return (
<div className="inputArea">
<input
placeholder="todoを入力"
value={todoText}
onChange={onChange}
/>
<button onClick={onClick}>追加</button>
</div>
);
};
propsを引数に取り、それぞれの値として渡します。const { todoText, onChange, onClick } = props;
は分割代入といってメソッドを書くときに簡略化することができます。例えば本来ならprops.todoText
やprops.onChange
と書かないといけないところをtodoText
やonChange
と省略できます。
これでインプットエリアのコンポーネント化は完了です。
imcompleteAreaのコンポーネント化
コンポーネント用のファイルを作成します。components配下にIncompleteTodos.jsx
を作成します。
src
|-components
|-InputTodo.jsx
|-IncompleteTodos.jsx
|-index.js
|-App.jsx
|-App.css
同じようにincompleteAreaの中身を切り取ります。
import React from 'react';
export const IncompleteTodos = () => {
return (
<div className="imcompleteArea">
<p className="title">未完了のtodo</p>
<ul>
{incompleteTodos.map((todo, index) => {
return (
<li key={todo} className="todoList">
<p>{todo}</p>
<button onClick={() => onClickComplete(index)}>完了</button>
<button onClick={() => onClickDelete(index)}>削除</button>
</li>
);
})}
</ul>
</div>
);
};
次にApp.jsxを編集します。
import { useState } from 'react';
import './App.css';
import { InputTodo } from './components/InputTodo';
// 追記
import { IncompleteTodos } from './components/IncompleteTodos';
:
:
rerun (
:
:
<IncompleteTodos />
:
);
それでは必要なpropsを設定します。
:
:
return (
:
<IncompleteTodos
todos={incompleteTodos}
onClickComplete={onClickComplete}
onClickDelete={onClickDelete}
/>
:
);
IncompleteTodos.jsx内で使用する変数と関数をそれぞれ渡します。
import React from 'react';
export const IncompleteTodos = (props) => {
const { todos, onClickComplete, onClickDelete } = props;
return (
<div className="imcompleteArea">
<p className="title">未完了のtodo</p>
<ul>
// incompleteTodosをtodosに変更
{todos.map((todo, index) => {
return (
<li key={todo} className="todoList">
<p>{todo}</p>
<button onClick={() => onClickComplete(index)}>完了</button>
<button onClick={() => onClickDelete(index)}>削除</button>
</li>
);
})}
</ul>
</div>
);
};
先程と同じように分割代入でpropsを渡します。
それでは最後に完了エリアのコンポーネント化をしていきます。
completeAreaのコンポーネント化
コンポーネント用のファイルを作成します。components配下にCompleteTodos.jsxを作成します。
src
|-components
|-InputTodo.jsx
|-IncompleteTodos.jsx
|-CompleteTodos.jsx
|-index.js
|-App.jsx
|-App.css
ここも同様にcompleteAreaの中身を切り取ります。
import React from 'react';
export const CompleteTodos = () => {
return (
<div className="completeArea">
<p className="title2">完了のtodo</p>
<ul>
{completeTodos.map((todo, index) => {
return (
<li key={todo} className="todoList">
<p>{todo}</p>
<button onClick={() => onClickBack(index)}>戻す</button>
</li>
);
})}
</ul>
</div>
);
};
次にApp.jsxを編集します。
import { useState } from 'react';
import './App.css';
import { InputTodo } from './components/InputTodo';
import { IncompleteTodos } from './components/IncompleteTodos';
// 追記
import { CompleteTodos } from './components/CompleteTodos';
:
return (
:
:
<CompleteTodos />
:
);
それでは必要なpropsを設定します。
:
:
return (
:
<CompleteTodos
todos={completeTodos}
onClickBack={onClickBack}
/>
);
それではpropsを渡しましょう。
import React from 'react';
export const CompleteTodos = (props) => {
const { todos, onClickBack} = props;
return (
<div className="completeArea">
<p className="title2">完了のtodo</p>
<ul>
{todos.map((todo, index) => {
return (
<li key={todo} className="todoList">
<p>{todo}</p>
<button onClick={() => onClickBack(index)}>戻す</button>
</li>
);
})}
</ul>
</div>
);
};
以上でコンポーネント化完了です!
終わりに
記事が長くなってしまいましたが、最後まで読んでいただきありがとうございました!
React学習の助けになれば幸いです。。。