4D REST API + ReactJS

Deeplからの自動翻訳

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=updatePOSTリクエストを送信します。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フォーラムで気軽に質問してください。

Mourad Aouinat
Mourad Aouinatは、2020年6月にフルスタック開発者として4Dに入社しました。彼は、Webアプリケーションのレイアウト/ユーザーインターフェースの作成、技術的ニーズに基づく仕様や要件の収集と改良を担当しています。Mouradは経済と金融のバックグラウンドを持つ独学の開発者であり、オープンソースソフトウェアとユーザーエクスペリエンスに情熱を注いでいます。