by Mourad Aouinat, ソフトウェアエンジニア at 4D Morocco
前回のブログポストでは、4Dを使ったREST APIのセットアップがいかに簡単かを紹介しました。このブログ記事では、強力な4D REST APIとReactを組み合わせて、ToDoを開く機能、新規作成機能、既存のToDoの変更機能、一括変更と一括削除機能を備えたToDoアプリを構築します。
フルアプリケーションです。ReactJSと4D REST APIを使用したTODOアプリ
ReactJS アプリのセットアップ
このチュートリアルでは、シンプルで美しい Todo アプリを構築します。
このチュートリアルは、 4D REST サーバーチュートリアルのフォローアップです。ReactJSの使用経験があり、Node.js 12.13.0以降がインストールされていることを前提としています。
まず、このコマンドをターミナルに貼り付けて、プロジェクトファイルをクローンします。
git clone https://github.com/mouradaouinat/todo-fd.git
プロジェクトのクローンを作成したら、プロジェクトのルートにディレクトリを変更します。
cd todoapp
このチュートリアルのステップに従うには、スターターファイルにブランチを切り替えます。
git checkout starter
このチュートリアルの手順を進めるには、ブランチをスターターファイルに切り替えます。
git checkout complete
依存関係をインストールします。
npm install
そして、プロジェクトを実行することで開始します。
npm start
これが、これから見ることになるものです。

プロキシサーバーを設定する
4Dレストサーバーに接続するために、アプリの開発サーバーを、4Dで設定したものと同じレストAPI URLに設定することを確認します。
"/rest":{
target: ADDRESS:PORT,
secure: false,
},
// craco.config.js
module.exports = {
devServer: {
proxy: {
"/rest": {
target: "http://127.0.0.1:4000",
secure: false,
},
},
},
style: {
postcss: {
plugins: [require("tailwindcss"), require("autoprefixer")],
},
},
};
アプリを4D REST APIに接続する
STEP1:Todoの取得
まず、4D サーバーが起動していることを確認し、組み込みの JavaScript フェッチメソッドを使って、最初の todo データを取得します。
JSON オブジェクトの代わりに Array フォーマットの Todo を取得するには、REST リクエストに $asArray を追加します(例:$asArray=true)。
// src/components/Todos.tsx
import { useEffect } from "react";
import { useTodos } from "../Provider";
import TodoItem from "./TodoItem";
const Todos: React.FC = () => {
const { todos, setTodos } = useTodos();
useEffect(() => {
fetch("/rest/Tasks/?$asArray=true")
.then((resp) => resp.json())
.then((todos) => setTodos(todos))
.catch(console.error);
}, [setTodos]);
return (
<ul className="section-list">
{todos.map((todo) => (
<TodoItem todo={todo} key={todo.id} />
))}
</ul>
); };
export default Todos;
STEP 2: Todo を追加する
新しい Todo を追加するために、/rest/Tasks/?$method=update にPOSTリクエストを送信します。method=updateの部分により、1 回の POST リクエストで 1 つまたは複数のエンティティを更新および/または作成することができます。
レスポンスには、新しく作成されたTodoと、各Todoを更新するために必要な追加のプロパティ、__KEY、__STAMPが返されます。
// src/components/AddTodo.tsx
import React, { useState } from "react";
import { HiOutlineChevronDown } from "react-icons/hi";
import { useTodos } from "../Provider";
const AddTodo: React.FC = () => {
const { todos, setTodos } = useTodos();
const [input, setInput] = useState("");
const allChecked = todos.every((todoItem) => todoItem.completed);
function handleAddTodo(e: React.KeyboardEvent) {
if (e.key === "Enter" && input) {
const newTodo = {
title: input,
completed: false,
};
fetch("/rest/Tasks/?$method=update", {
method: "POST",
body: JSON.stringify(newTodo),
})
.then((res) => res.json())
.then((newTodo) => {
setTodos((state) => [newTodo, ...state]);
setInput("");
})
.catch(console.error);
}
}
function handleChange({
target: { value },
}: React.ChangeEvent) {
setInput(value);
}
return (
<div className="todo-prompt">
<div className="todo-prompt__container">
{todos.length ? ( <
button
className="todo-prompt__toggle"
onClick={() => {
setTodos((state) => {
if (allChecked) {
return state.map((item) => ({ ...item, completed: false, })); }
else { return state.map((item) => ({ ...item, completed: true, }));
} }); }} >
<HiOutlineChevronDown className="todo-prompt__toggle-icon" /> </button> ) : null}
</div>
<input
type="text"
placeholder="What needs to be done?"
className="todo-prompt__input"
value={input}
onChange={handleChange}
onKeyDown={handleAddTodo}
/>
</div>
);
};
export default AddTodo;
STEP 3: Todoを更新する
エンティティを更新するには、オブジェクト内の__KEYと__STAMPパラメータを、変更された属性と一緒に渡す必要があります。これらのパラメータが両方ともない場合、POST のボディで送信したオブジェクトの値でエンティティが追加されます。
この例では、JavaScriptのspread演算子を使用して、__Keyと__STAMPを暗黙のうちに渡しています。
// src/components/TodoItem.tsx
import { useState } from "react";
import DeleteBtn from "./DeleteBtn";
import { useTodos } from "../Provider";
import { ITodo } from "../interfaces";
const TodoItem: React.FC<{ todo: ITodo }> = ({ todo }) => {
const { setTodos } = useTodos();
const [isEditing, setIsEditing] = useState(false);
const [inputValue, setInputValue] = useState(todo.title);
function handleCompleted() {
fetch("/rest/Tasks/?$method=update", {
method: "POST",
body: JSON.stringify({
...todo,
completed: !todo.completed,
}),
})
.then((res) => res.json())
.then(({ __STATUS, uri, ...rest }) => {
if (__STATUS.success) {
setTodos((todos) => {
return todos.map((item) => {
if (todo.id === item.id) {
return rest;
}
return item;
});
});
}
})
.catch(console.error);
}
function updateTodoTitle() {
fetch("/rest/Tasks/?$method=update", {
method: "POST",
body: JSON.stringify({
...todo,
title: inputValue,
}),
})
.then((res) => res.json())
.then(({ __STATUS, uri, ...rest }) => {
if (__STATUS.success) {
setTodos((todos) => {
return todos.map((item) => {
if (todo.id === item.id) {
return rest;
}
return item;
});
});
}
})
.catch(console.error);
setIsEditing(false);
}
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === "Escape") {
setInputValue(todo.title);
setIsEditing(false);
}
if (e.key === "Enter") {
updateTodoTitle();
}
}
return (
<li
className="todo-item"
onDoubleClick={() => {
if (!todo.completed) { setIsEditing(true);
}
}} >
{isEditing ? (
<input
className="todo-item__input"
autoFocus value={inputValue}
onChange={({ target: { value } }) => {
setInputValue(value);
}}
onBlur={updateTodoTitle}
onKeyDown={handleKeyDown}
/>
) : (
<div className="todo-item__container">
<div className="todo-item__devider">
<input
type="checkbox"
className="todo-item__checkbox"
checked={todo.completed}
onChange={handleCompleted} />
</div>
<label
className={`todo-item__title ${
todo.completed
? "todo-item__title--completed"
: "todo-item__title--active"
}`}
>
{todo.title}
</label>
<div className="todo-item__delete-btn group-hover:flex">
<DeleteBtn id={todo.id} />
</div>
</div>
)}
</li>
);
};
export default TodoItem;
STEP 4: Todo の削除
このアプリでは、Todo は Tasks データクラス内の特定のエンティティです。Todo を削除するには、クエリパラメータに?$method=deleteを指定してPOSTリクエストを送信し、TodoIDを次のように渡します:id /rest/DataClass(id)/?$method=delete
// src/components/DeleteBtn.tsx
import React from "react";
import { ImCross } from "react-icons/im";
import { useTodos } from "../Provider";
const DeleteBtn: React.FC<{ id: number }> = ({ id }) => {
const { setTodos } = useTodos();
return (
<button
onClick={() => {
fetch(`/rest/Tasks(${id})/?$method=delete`, {
method: "POST",
})
.then((res) => {
if (res.ok) {
setTodos((todos) => todos.filter((todo) => todo.id !== id));
}
})
.catch(console.error);
}}
> <ImCross className="todo-item__delete-icon" /> </button> ); };
export default DeleteBtn;
STEP 5: 一括更新
複数のエンティティの更新は、単一のエンティティの更新と同じですが、1つのTodoを渡す代わりに、Todoの配列をPOSTリクエストのボディに渡すことにします。
// src/components/AddTodo.tsx
import React, { useState } from "react";
import { HiOutlineChevronDown } from "react-icons/hi";
import { useTodos } from "../Provider";
const AddTodo: React.FC = () => {
const { todos, setTodos } = useTodos();
const [input, setInput] = useState("");
const allChecked = todos.every((todoItem) => todoItem.completed);
function handleAddTodo(e: React.KeyboardEvent) {
if (e.key === "Enter" && input) {
const newTodo = {
title: input,
completed: false,
};
fetch("/rest/Tasks/?$method=update", {
method: "POST",
body: JSON.stringify(newTodo),
})
.then((res) => res.json())
.then((newTodo) => {
setTodos((state) => [newTodo, ...state]);
setInput("");
})
.catch(console.error);
}
}
function handleChange({
target: { value },
}: React.ChangeEvent) {
setInput(value);
}
function toggleAllTodosCompleted() {
fetch("/rest/Tasks/?$method=update", {
method: "POST",
body: JSON.stringify(
todos.map((todo) => ({ ...todo, completed: allChecked ? false : true }))
),
})
.then((res) => {
if (res.ok) {
fetch("/rest/Tasks/?$asArray=true")
.then((resp) => resp.json())
.then((todos) => setTodos(todos))
.catch(console.error);
}
})
.catch(console.error);
}
return (
<div className="todo-prompt">
<div className="todo-prompt__container">
{todos.length ? (
<button
className="todo-prompt__toggle"
onClick={toggleAllTodosCompleted}
>
<HiOutlineChevronDown className="todo-prompt__toggle-icon" />
</button> ) : null}
</div>
<input
type="text"
placeholder="What needs to be done?"
className="todo-prompt__input" value={input}
onChange={handleChange}
onKeyDown={handleAddTodo}
/>
</div>
);
};
export default AddTodo;
STEP 6: 一括削除
クエリパラメータ$filterを使って、一度の POST リクエストで複数の ToDo を削除することもできます。以下の行は、完了したTODOをすべて削除します。
/rest/Tasks/?$filter=”completed=true”&$method=delete
// src/components/Footer.tsx
import { useTodos } from "../Provider";
const Footer: React.FC = () => {
const { todos, setTodos } = useTodos();
function handleDeleteCompleted() {
fetch('/rest/Tasks/?$filter="completed=true"&$method=delete', {
method: "POST",
})
.then((res) => {
if (res.ok) {
setTodos((state) => {
return state.filter((item) => !item.completed);
});
}
})
.catch(console.error);
}
return todos.length ? (
<div className="app-footer">
<span>{todos.filter((item) => !item.completed).length} items left</span>
<div className="app-footer__action-box">
{todos.some((item) => item.completed) ? (
<button
className="app-footer__clear-btn"
onClick={handleDeleteCompleted}
>
Clear Completed
</button>
) : null}
</div>
</div>
) : null;
};
export default Footer;
まとめ
この記事で見たように、4D REST Serverは、アプリケーションを開発するときに素早く実行するための機能が満載です。質問があれば、4Dフォーラムで気軽に質問してください。
