4D REST API + ReactJS

Automatically translated from English

by Mourad Aouinat, Software Engineer at 4D Morocco

In a previous blog post, we saw how easy setting up a REST API using 4D. In this blog post, we will leverage the powerful 4D REST API in combination with React to build a To-Do app that includes features to open todos, create new ones, modify existing ones, and features for bulk modification and bulk deletion.

Full Application: TODO app using ReactJS and 4D REST API

Setting up the ReactJS app

In this tutorial, we will build a simple, beautiful Todo App.

This tutorial is the follow-up of the 4D REST server tutorial. It assumes that you have experience with ReactJS, and have Node.js 12.13.0 or higher installed on your computer.

First, paste this command into your terminal to clone the project files:

git clone https://github.com/mouradaouinat/todo-fd.git

After you clone the project, change directories to the root of the project:

cd todoapp

To follow along with the steps of this tutorial, switch branch to the starter files:

git checkout starter

If you want to skip ahead and go straight to the finished app, switch to the complete branch:

git checkout complete

Install the dependencies:

npm install

And then start the project by running:

npm start

This is what you are going to see:

Configuring the proxy server

To connect to your 4D rest server, make sure to configure the app’s dev server to the same rest API URL as the one you’ve configured on 4D:

"/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")],
    },
  },
};

Connecting the app to the 4D REST API

STEP 1: Getting Todos

First, we make sure that the 4D server is up and running, then we fetch the initial todo data using the built-in JavaScript fetch method.

To get the todos in Array format instead of the JSON object you just have to add $asArray to your REST request (e.g., $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: Adding a Todo

To add a new Todo, we send a POST request to /rest/Tasks/?$method=update. The $method=update part allows you to update and/or create one or more entities in a single POST request.

The response is the newly created todo with additional properties, __KEY, and __STAMP that we’re going to need to update each todo:

// 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: Updating a Todo

To update an entity, you must pass the __KEY and __STAMP parameters in the object along with any modified attributes. If both of these parameters are missing, an entity will be added with the values in the object you send in the body of your POST.

We’re implicitly passing the __Key and __STAMP using the JavaScript spread operator in this example:

// 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: Deleting a Todo

In our app, a todo is a specific Entity in the Tasks dataclass. To delete a todo, we’re going to send a POST request with query param ?$method=delete and pass the todo id as follows:  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: Bulk Updating

Updating multiple entities is the same as updating a single entity, but instead of passing one todo, we’re going to pass an array of todos to the body of the POST request:

// 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: Bulk Deleting

We can also delete multiple todos with a single POST request using the $filter query parameter. The lines below delete all the completed todos:

/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;

Takeaway

As we saw in this article, the 4D REST Server comes out of the box packed with features to quickly get up running when developing applications. Feel free to ask any questions you might have on the 4D forum!

Mourad Aouinat

Mourad Aouinat joined 4D as a full stack developer in June 2020. he’s in charge of creating web application layout/user interfaces and gathering and refining specifications and requirements based on technical needs.

Mourad is a self-taught developer with a background in economics and finance, passionate about open-source software and user experience.