DEV Community

Cover image for How To Use HarperDB Custom Functions With Your React App.
Ankur Tyagi
Ankur Tyagi

Posted on • Updated on • Originally published at theankurtyagi.com

How To Use HarperDB Custom Functions With Your React App.

Last week, I got a chance to explore HarperDB - a fast, modern database that allows you to develop full-stack apps.

I've developed a ToDo React app with HarperDB Custom Functions.

HarperDB is a distributed database focused on making data management easy

  • It supports both SQL and NoSQL queries.
  • It also offers to access the database instance directly inside the client-side application.

In this article, let's learn about HarperDB and how to build a React app using HarperDB Custom Functions!

Let's talk about HarperDB custom functions

  • Add your own API endpoints to a standalone API server inside HarperDB.
  • Use HarperDB Core methods to interact with your data at lightning speed.
  • Custom Functions are powered by Fastify, so they’re extremely flexible.
  • Manage in HarperDB Studio, or use your own IDE and Version Management System.
  • Distribute your Custom Functions to all your HarperDB instances with a single click.

harperDB1.JPG

What are we building

We will create a simple ToDo React App. When we are done, it will look like this when it runs in localhost:

7.JPG

Let's look at how we develop our To-Do React app

This ToDo app allows a user to create a task that needs to be completed by the user.

It has 2 states

  • Active
  • Completed

Users can filter the tasks list based on the status of tasks as well. It will also allow the user to edit a task & delete one as well.

So the main idea is whatever task is created by the user which you can see in the "View All" list, all the tasks will be saved in HarperDB with the help of Custom Functions.

Project setup overview

Create React App is the best way to start building a new single-page application in React.

npx create-react-app my-app
cd my-app
npm start

Dependencies used:


 "@emotion/react": "^11.5.0",
    "@emotion/styled": "^11.3.0",
    "@mui/icons-material": "^5.0.5",
    "@mui/material": "^5.0.6",
    "@testing-library/jest-dom": "^5.15.0",
    "@testing-library/react": "^11.2.7",
    "@testing-library/user-event": "^12.8.3",
    "axios": "^0.24.0",
    "classnames": "^2.3.1",
    "history": "^5.1.0",
    "lodash.debounce": "^4.0.8",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "^6.0.1",
    "react-scripts": "4.0.3",
    "web-vitals": "^1.1.2"

Enter fullscreen mode Exit fullscreen mode

it just creates a frontend build pipeline for this project, so we can use HarperDB in the backend.

Alternatively, you can clone the GitHub repository and use the start directory as your project root. It contains the basic project setup that will get you ready. In this project for the CSS you can refer to Tasks.css (src\todo-component\Tasks.css)

Let's talk about the react components which are being used

This is the folder structure:

file.JPG

In file structure, we can see that Tasks is the container component where we are managing the application's state, here the app state means the data we are getting from HarperDB using API endpoints, and this data is shared across all child components through props.

Task component (Tasks.jsx)

Here is the file reference in the project:

src\todo-component\Tasks.jsx

This component acts as a container component (which is having a task list & task search as a child component)

import React, { useEffect, useCallback, useState, useRef } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import TaskSearch from './task-search-component/TaskSearch';
import './Tasks.css';
import axios from 'axios';
import debounce from '@mui/utils/debounce';
import TaskItem from './task-list-component/TaskList';
import Snackbar from '@mui/material/Snackbar';

export default function Tasks() {
  const navigate = useNavigate();
  const [searchParams, setSearchParams] = useSearchParams();
  const [taskList, setTaskList] = useState([]);
  const [filteredList, setFilteredList] = useState([]);
  const [open, setOpen] = useState(false);
  const [msg, setMsg] = useState('')
  const selectedId = useRef();
  useEffect(() => {
    getFilteredList();
  }, [searchParams, taskList]);

  const setSelectedId = (task) => {
    selectedId.current = task;
  };
  const saveTask = async (taskName) => {
    if (taskName.length > 0) {
      try {
        await axios.post(
          'your_url_here',
          { taskTitle: taskName, taskStatus: 'ACTIVE', operation: 'sql' }
        );
        getTasks();
      } catch (ex) {
        showToast();
      }
    }
  };

  const updateTask = async (taskName) => {
    if (taskName.length > 0) {
      try {
        await axios.put(
          'your_url_here',
          {
            taskTitle: taskName,
            operation: 'sql',
            id: selectedId.current.id,
            taskStatus: selectedId.current.taskStatus,
          }
        );
        getTasks();
      } catch (ex) {
        showToast();
      }
    }
  };

  const doneTask = async (task) => {
    try {
      await axios.put(
        'your_url_here',
        {
          taskTitle: task.taskTitle,
          operation: 'sql',
          id: task.id,
          taskStatus: task.taskStatus,
        }
      );
      getTasks();
    } catch (ex) {
        showToast();
    }
  };

  const deleteTask = async (task) => {
    try {
      await axios.delete(
        `your_url_here/${task.id}`
      );
      getTasks();
    } catch (ex) {
        showToast();
    }
  };

  const getFilteredList = () => {
    if (searchParams.get('filter')) {
      const list = [...taskList];
      setFilteredList(
        list.filter(
          (item) => item.taskStatus === searchParams.get('filter').toUpperCase()
        )
      );
    } else {
      setFilteredList([...taskList]);
    }
  };

  useEffect(() => {
    getTasks();
  }, []);

  const getTasks = async () => {
    try {
    const res = await axios.get(
      'your_url_here'
    );
    console.log(res);
    setTaskList(res.data);
    } catch(ex) {
        showToast();
    }
  };

  const debounceSaveData = useCallback(debounce(saveTask, 500), []);
  const searchHandler = async (taskName) => {
    debounceSaveData(taskName);
  };

  const showToast = () => {
    setMsg('Oops. Something went wrong!');
    setOpen(true)
  }

  return (
    <div className="main">
      <TaskSearch searchHandler={searchHandler} />
      <ul className="task-filters">
        <li>
          <a
            href="javascript:void(0)"
            onClick={() => navigate('/')}
            className={!searchParams.get('filter') ? 'active' : ''}
          >
            View All
          </a>
        </li>
        <li>
          <a
            href="javascript:void(0)"
            onClick={() => navigate('/?filter=active')}
            className={searchParams.get('filter') === 'active' ? 'active' : ''}
          >
            Active
          </a>
        </li>
        <li>
          <a
            href="javascript:void(0)"
            onClick={() => navigate('/?filter=completed')}
            className={
              searchParams.get('filter') === 'completed' ? 'active' : ''
            }
          >
            Completed
          </a>
        </li>
      </ul>
      {filteredList.map((task) => (
        <TaskItem
          deleteTask={deleteTask}
          doneTask={doneTask}
          getSelectedId={setSelectedId}
          task={task}
          searchComponent={
            <TaskSearch
              searchHandler={updateTask}
              defaultValue={task.taskTitle}
            />
          }
        />
      ))}
      <Snackbar
        open={open}
        autoHideDuration={6000}
        onClose={() => setOpen(false)}
        message={msg}
      />
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

your_url_here = you should replace this with your HarperDB endpoint URL.

For an example of the URL, look below:

16.JPG

Task List (TaskList.jsx)

Here is the file reference in the project:

src\todo-component\task-list-component\TaskList.jsx

This component is used to render all the list of tasks that we are getting from the HarperDB


import React, { useState } from 'react';
import classNames from 'classnames';
import IconButton from '@mui/material/IconButton';
import DoneIcon from '@mui/icons-material/Done';
import EditIcon from '@mui/icons-material/Edit';
import ClearIcon from '@mui/icons-material/Clear';
import DeleteIcon from '@mui/icons-material/Delete';
import TextField from '@mui/material/TextField';

export default function TaskItem({ task, searchComponent, getSelectedId, doneTask, deleteTask }) {
  const [editing, setEditing] = useState(false);
  const [selectedTask, setSelectedTask] = useState();
  let containerClasses = classNames('task-item', {
    'task-item--completed': task.completed,
    'task-item--editing': editing,
  });

  const updateTask = () => {
      doneTask({...task, taskStatus: task.taskStatus === 'ACTIVE' ? 'COMPLETED' : 'ACTIVE'});
  }

  const renderTitle = task => {
    return (
      <div className="task-item__title" tabIndex="0">
        {task.taskTitle}
      </div>
    );
  }
  const resetField = () => {
      setEditing(false);
  }
  const renderTitleInput = task => {
    return (
    React.cloneElement(searchComponent, {resetField})
    );
  }

  return (
    <div className={containerClasses} tabIndex="0">
      <div className="cell">
        <IconButton color={task.taskStatus === 'COMPLETED' ? 'success': 'secondary'} aria-label="delete" onClick={updateTask} className={classNames('btn--icon', 'task-item__button', {
            active: task.completed,
            hide: editing,
          })} >
          <DoneIcon />
        </IconButton>
       </div>

      <div className="cell">
        {editing ? renderTitleInput(task) : renderTitle(task)}
      </div>

      <div className="cell">
      {!editing && <IconButton onClick={() => {setEditing(true); getSelectedId(task)}} aria-label="delete" className={classNames('btn--icon', 'task-item__button', {
            hide: editing,
          })} >
          <EditIcon />
        </IconButton> }
        {editing && <IconButton onClick={() => {setEditing(false); getSelectedId('');}} aria-label="delete" className={classNames('btn--icon', 'task-item__button', {
            hide: editing,
          })} >
          <ClearIcon />
        </IconButton> }
        {!editing && <IconButton onClick={() => deleteTask(task)} aria-label="delete" className={classNames('btn--icon', 'task-item__button', {
            hide: editing,
          })} >
          <DeleteIcon />
        </IconButton> }
       </div>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Task Search (TaskSearch.jsx)

Here is the file reference in the project:

src\todo-component\task-search-component\TaskSearch.jsx

This component provides a text box to users where users can enter the name of the task which they need to perform. (Same component we are using while editing a task)


import React from 'react';
import TextField from '@mui/material/TextField';

export default function TaskSearch({ searchHandler, defaultValue, resetField }) {
  const handleEnterKey = event => {
    if(event.keyCode === 13) {
      searchHandler(event.target.value);
      event.target.value = '';
      if(resetField) {
        resetField();
      }
    }
  }

    return (
        <TextField
        id="filled-required"
        variant="standard"
        fullWidth 
        hiddenLabel
        placeholder="What needs to be done?"
        onKeyUp={handleEnterKey}
        defaultValue={defaultValue}
      />
    );
}
Enter fullscreen mode Exit fullscreen mode

Here you can find the complete source code of the ToDo App.

In the Tasks.js component, you can see we are leveraging Custom Function APIs which allows us to save & edit the data from HarperDB.

8.JPG

How we develop an API using HarperDB Custom functions:

Let's create the schema first

10.JPG

Created table:

11.JPG

Create a project

Tip: Before creating a project, you need to enable custom functions, once you click on functions you will see a pop up like below:

12.JPG

Click on the green button "enable the custom function" it will look like πŸ‘‡

13.JPG

Now let's create project "ToDoApi" which will look like πŸ‘‡

15.JPG

Under the section "/ToDoApi/routes" we will see one file example.js contains the API endpoints.

Let's write our own API endpoints in order to :

  • create a task
  • edit a task
  • delete a task
  • get task

Save Task endpoint

Which is used to store data in DB

  server.route({
    url: '/saveTask',
    method: 'POST',
    // preValidation: hdbCore.preValidation,
    handler: (request) => {
      request.body= {
        operation: 'sql',
        sql: `insert into example_db.tasks (taskTitle, taskStatus) values('${request.body.taskTitle}', '${request.body.taskStatus}')`
      };
      return hdbCore.requestWithoutAuthentication(request);

    },
  });

Enter fullscreen mode Exit fullscreen mode

Edit Task endpoint

This is used to edit an existing record in your DB, we are using the same endpoint as the save task but having a different method type as PUT.

 server.route({
    url: '/saveTask',
    method: 'PUT',
    // preValidation: hdbCore.preValidation,
    handler: (request) => {
      request.body= {
        operation: 'sql',
        sql: `update example_db.tasks set taskTitle='${request.body.taskTitle}', taskStatus='${request.body.taskStatus}' where id='${request.body.id}'`
      };
      return hdbCore.requestWithoutAuthentication(request);

    },
  });

Enter fullscreen mode Exit fullscreen mode

Delete a task endpoint

server.route({
    url: '/deleteTask/:id',
    method: 'DELETE',
    // preValidation: hdbCore.preValidation,
    handler: (request) => {
      request.body= {
        operation: 'sql',
        sql: `delete from example_db.tasks where id='${request.params.id}'`
      };
      return hdbCore.requestWithoutAuthentication(request);

    },
  });

Enter fullscreen mode Exit fullscreen mode

Get task endpoint

// GET, WITH ASYNC THIRD-PARTY AUTH PREVALIDATION
  server.route({
    url: '/tasks',
    method: 'GET',
    // preValidation: (request) => customValidation(request, logger),
    handler: (request) => {
      request.body= {
        operation: 'sql',
        sql: 'select * from example_db.tasks'
      };

      /*
       * requestWithoutAuthentication bypasses the standard HarperDB authentication.
       * YOU MUST ADD YOUR OWN preValidation method above, or this method will be available to anyone.
       */
      return hdbCore.requestWithoutAuthentication(request);
    }
  });


Enter fullscreen mode Exit fullscreen mode

All about helpers in Custom Functions

In this, we can implement our own custom validation using JWT.

helper.JPG

In our, ToDo React app on the UI.

How to get the endpoint URL to hit on the UI.

16.JPG

You can host a static web UI

Your project must meet the below details to host your static UI

  • An index file located at /static/index.html
  • Correctly path any other files relative to index.html
  • If your app makes use of client-side routing, it must have [project_name]/static as its base (basename for react-router, base for vue-router, etc.):
<Router basename="/dogs/static">
    <Switch>
        <Route path="/care" component={CarePage} />
        <Route path="/feeding" component={FeedingPage} />
    </Switch>
</Router>

Enter fullscreen mode Exit fullscreen mode

The above example can be checked out at HarperDB as well.

Custom Functions Operations

There are 9 operations you can do in total:

  • custom_functions_status
  • get_custom_functions
  • get_custom_function
  • set_custom_function
  • drop_custom_function
  • add_custom_function_project
  • drop_custom_function_project
  • package_custom_function_project
  • deploy_custom_function_project

You can have a more indepth look at every individual operation in HarperDB docs.

Restarting the Server

For any changes you’ve made to your routes, helpers, or projects, you’ll need to restart the Custom Functions server to see them take effect. HarperDB Studio does this automatically whenever you create or delete a project, or add, edit, or edit a route or helper. If you need to start the Custom Functions server yourself, you can use the following operation to do so:

{
    "operation": "restart_service",
    "service": "custom_functions"
}

Enter fullscreen mode Exit fullscreen mode

That was it for this blog.

I hope you learned something new today. If you did, please like/share so that it reaches others as well.

If you’re a regular reader, thank you, you’re a big part of the reason I’ve been able to share my life/career experiences with you.

Connect with me on Twitter

Top comments (8)

Collapse
 
yonasjs profile image
Yon Yon

Very informative article well written succinctly by providing practical examples that would help you understand the logic behind "How To Use HarperDB Custom Functions With Your React App".

                  Thanks a lot Ankur πŸ‘
Enter fullscreen mode Exit fullscreen mode
Collapse
 
tyaga001 profile image
Ankur Tyagi

Glad you liked it. Thank you

Collapse
 
abh1navv profile image
Abhinav Pandey

Nice article Ankur πŸ‘

Collapse
 
tyaga001 profile image
Ankur Tyagi

Glad you liked it. Thank you

Collapse
 
bhosalepratim profile image
Pratim Bhosale

Very well explained! Thank you for writing it Ankur.

Collapse
 
tyaga001 profile image
Ankur Tyagi

Glad you liked it. Thank you

Collapse
 
sampurna profile image
Sampurna Chapagain

Nice article. Very well explained Ankur !

Collapse
 
tyaga001 profile image
Ankur Tyagi

@sampurna Glad you liked it. Thank you