「ReactでTodoアプリを作成してみた②-①」の続きです。
ステータスごとのTodoリストとチェックボタンを押下するとTodoが移動する処理を作成していきます。
ワイヤーフレーム
Todoアプリの構成
TodoListOrganisms.jsx、TodoFormOrganisms.jsxを追加しました。
変更した内容についはコンポーネントの説明にて行います。
src
│ index.jsx
│
├─components
│ TodoForm.jsx
│ TodoList.jsx
│
├─context
│ TodoProvider.jsx
│
├─organisms
│ TodoFormOrganisms.jsx
│ TodoListOrganisms.jsx
│
└─pages
TodoPage.jsx
コンポーネントの説明
TodoProvider.jsx
3種類のTodoListをオブジェクトで保存するようにuseStateの初期値を変更しました。
statusNameを条件としてtaskListの値を画面に一覧表示していきます。
import { useState, createContext } from 'react';
export const TodoContext = createContext();
export const TodoProvider = ({ children }) => {
const [todo, setTodo] = useState('');
const [todoList, setTodoList] = useState([
{ statusName: 'NOT STARTED', taskList: [] },
{ statusName: 'STARTED', taskList: [] },
{ statusName: 'COMPLETION', taskList: [] },
]);
return (
<TodoContext.Provider value={{ todo, setTodo, todoList, setTodoList }}>
{children}
</TodoContext.Provider>
);
};
TodoPage.jsx
TodoFormOrganismsコンポーネントに値入力用Formを、TodoListOrganismsコンポーネントに3種類のTodoリストを表示するように変更しています。
import { TodoFormOrganisms } from '../organisms/TodoFormOrganisms';
import { TodoListOrganisms } from '../organisms/TodoListOrganisms';
export const TodoPage = () => {
return (
<>
<TodoFormOrganisms />
<TodoListOrganisms />
</>
);
};
TodoFormOrganisms.jsx
入力フォームを表示するコンポーネントとなります。
useContextで定義したstateを呼び出してpropsとして子コンポーネントに渡していきます。
import { useContext } from 'react';
import { TodoContext } from '../context/TodoProvider';
import { Typography, Grid } from '@mui/material';
import { TodoForm } from '../components/TodoForm';
export const TodoFormOrganisms = () => {
const { todo, setTodo, todoList, setTodoList } = useContext(TodoContext);
return (
<Grid container>
<Grid item lg={4}>
<Typography variant='h5' component='h1'>
{'TODO APP'}
</Typography>
<TodoForm
todo={todo}
setTodo={setTodo}
todoList={todoList}
setTodoList={setTodoList}
/>
</Grid>
</Grid>
);
};
TodoForm.jsx
オブジェクトのtaskList配列に値を追加するようmap関数の処理を変更しています。
todoList.map((value) => { return value.statusName === 'NOT STARTED' ? { statusName: 'NOT STARTED', taskList: [...value.taskList, todo] } : value;
})
ステータスがNOT STARTEDの時に画面で入力された値をtaskListに追加したオブジェクトを返却します。NOT STARTEDでなければ現在のオブジェクトを返却します。
import { Box, TextField, IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
export const TodoForm = (props) => {
const { todo, setTodo, todoList, setTodoList } = props;
return (
<Box
sx={{
width: 250,
}}
>
<TextField
id='inputTodo'
label=''
type='text'
name='inputTodo'
size='small'
sx={{ maxWidth: 160 }}
value={todo}
onChange={(e) => {
console.log('inputTodo:onChange');
setTodo(e.target.value);
}}
variant='outlined'
/>
<IconButton
aria-label='add'
size='large'
type='submit'
onClick={() => {
console.log('addButton:onClick');
setTodoList(
todoList.map((value) => {
return value.statusName === 'NOT STARTED'
? { statusName: 'NOT STARTED', taskList: [...value.taskList, todo] }
: value;
})
);
}}
>
<AddIcon sx={{ fontSize: 18 }} />
</IconButton>
</Box>
);
};
TodoListOrganisms.jsx
3種類のTodoリストを表示するコンポーネントとなります。
map関数を使用してMaterial-UIのGridで3分割したTodoListコンポーネントを表示させます。
propsとしてTodoProvider.jsxで定義したstatusNameとindex,3種類のオブジェクトを定義したtodoListと状態更新関数を渡します。TodoListコンポーネントを共通コンポーネントとして利用しているため、statusNameとindexはTodoListコンポーネント内でステータスによってTodoリストの移動、削除、更新を行うかの条件分岐に利用します。
import { useContext } from 'react';
import { TodoContext } from '../context/TodoProvider';
import { Typography, Grid } from '@mui/material';
import { TodoList } from '../components/TodoList';
export const TodoListOrganisms = () => {
const { todoList, setTodoList } = useContext(TodoContext);
return (
<Grid container>
{todoList.map((value, index) => {
return (
<div key={index}>
<Grid item lg={4}>
<Typography variant='h6' component='h1'>
{value.statusName}
</Typography>
<TodoList
statusName={value.statusName}
statusIndex={index}
todoList={todoList}
setTodoList={setTodoList}
/>
</Grid>
</div>
);
})}
</Grid>
);
};
TodoList.jsx
ステータスごとにTodoを移動する処理を追加しました。削除、編集の処理を3種類のTodoリストに対して行えるよう変更しています。
mapやfilter関数を利用すると複雑な処理となるため可読性を重視してObject.assign関数を使用しました。Object.assign関数はオブジェクトをコピーすることができます。
let copyTodoList = Object.assign([],JSON.parse(JSON.stringify(todoList)));
JSON.parse(JSON.stringify(todoList)でオブジェクトをディープコピーしています。JSON.parseなしだとシャロ―コピーとなるためコピー先のオブジェクト更新時に元のオブジェクトが更新されてしまうので取り扱いに注意が必要となります。
今回追加したチェックボタンを押下するとTodoが次のステータスに移動する処理です。
if (statusName === 'NOT STARTED') {
copyTodoList[statusIndex].taskList.splice(index, 1);
copyTodoList[1].taskList.unshift(todoList[statusIndex].taskList[index]);
}
ステータスがNON STARTEDのチェックボタンを押下した場合、チェックされたTodoを削除後、STARTEDに移動する。spliceでNON STARTEDの要素を1件削除、unshiftでSTARTに要素を1件追加しています。これらの処理を3パターン分用意しました。
削除と編集の処理も同じようにObject.assign関数でディープコピーを行い、ディープコピーしたオブジェクトに対して削除、編集処理を行っています。処理終了後にuseStateでオブジェクトを保存します。
import { Box, TextField, IconButton } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import DoneIcon from '@mui/icons-material/Done';
export const TodoList = (props) => {
const { statusName, statusIndex, todoList, setTodoList } = props;
return (
<Box
sx={{
width: 250,
}}
>
{todoList[statusIndex].taskList.map((todo, index) => {
return (
<div key={index}>
<IconButton
aria-label='move'
size='large'
type='submit'
onClick={(e) => {
console.log('moveButton:onClick');
let copyTodoList = Object.assign(
[],
JSON.parse(JSON.stringify(todoList))
);
if (statusName === 'NOT STARTED') {
copyTodoList[statusIndex].taskList.splice(index, 1);
copyTodoList[1].taskList.unshift(
todoList[statusIndex].taskList[index]
);
}
if (statusName === 'STARTED') {
copyTodoList[statusIndex].taskList.splice(index, 1);
copyTodoList[2].taskList.unshift(
todoList[statusIndex].taskList[index]
);
}
if (statusName === 'COMPLETION') {
copyTodoList[statusIndex].taskList.splice(index, 1);
copyTodoList[0].taskList.unshift(
todoList[statusIndex].taskList[index]
);
}
setTodoList(copyTodoList);
}}
>
<DoneIcon sx={{ fontSize: 18 }} />
</IconButton>
<TextField
id='outputTodo'
label=''
name='outputTodo'
size='small'
sx={{ maxWidth: 160 }}
value={todo}
onChange={(e) => {
console.log('TodoList:onChange');
let copyTodoList = Object.assign(
[],
JSON.parse(JSON.stringify(todoList))
);
if (statusName === 'NOT STARTED') {
copyTodoList[statusIndex].taskList[index] = e.target.value;
}
if (statusName === 'STARTED') {
copyTodoList[statusIndex].taskList[index] = e.target.value;
}
if (statusName === 'COMPLETION') {
copyTodoList[statusIndex].taskList[index] = e.target.value;
}
setTodoList(copyTodoList);
}}
variant='standard'
/>
<IconButton
aria-label='delete'
size='large'
type='submit'
onClick={() => {
console.log('deleteButton:onClick');
let copyTodoList = Object.assign(
[],
JSON.parse(JSON.stringify(todoList))
);
if (statusName === 'NOT STARTED') {
copyTodoList[statusIndex].taskList.splice(index, 1);
}
if (statusName === 'STARTED') {
copyTodoList[statusIndex].taskList.splice(index, 1);
}
if (statusName === 'COMPLETION') {
copyTodoList[statusIndex].taskList.splice(index, 1);
}
setTodoList(copyTodoList);
}}
>
<DeleteIcon sx={{ fontSize: 20 }} />
</IconButton>
</div>
);
})}
</Box>
);
};
完成したTodoアプリ
フォームからTodoを追加後にチェックボタンを押下すると次のステータスに移動します。
各ステータスごとのTodoリストは編集、削除も可能となっています。
今回学んだこと
React hook useState オブジェクトの状態管理 オブジェクトのCRUD
ディープコピーとシャロ―コピー
map,filter関数以外での移動、削除、編集の方法
Material-UI Gridを使ったレイアウト
おすすめの書籍
JavaScriptのおすすめの書籍です。
おすすめのUdemy講座
ReactでおすすめのUdemy講座を紹介します。
モダンJavaScriptの基礎から始める挫折しないためのReact入門
ReactはJavaScriptの基本的な構文を使用するため、Reactだけの理解では足りないでしょう。
こちらのReact講座は、JavaScriptでのTODOアプリを作成してからReactでのTODOアプリを作成するため、ReactだけではなくJavaScriptを基礎から学ぶことができます。
Reactからいきなり入らずにJavaScriptの基礎から学べる点が、初心者にとてもおすすめできる内容となっています。
【Reactアプリ開発】3種類のReactアプリケーションを構築して、Reactの理解をさらに深めるステップアップ講座
APIを使用してデータを取得したり、react-router-domでのページ遷移を学べるためより実践的な内容となっています。
3種類のReactアプリケーションを作成することができるため、Reactの基礎的な内容を学んだ人におすすめです。
基本的に構文が載っているため、教科書代わりに使える内容となっています。
Reactの前にとてもお世話になりました。JavaScriptをこれから始める方におすすめです。