React Journey: Part 4 - Working with Web API

Zaki Mohammed Zaki Mohammed
Sep 21, 2023 | 6 min read | 383 Views | Comments

There is no app existence without communicating server or at least a fake server. In our React learning journey we will now make our Notesy app to communicate to API even though its fake. In this article, we will make use of json-server to create API endpoint for our Notesy app and make our create, read, delete operation little dynamic.

If you are just started with React and have no idea about what happens at the backend, then don't scare in this article we will create a backend for us without actually creating a backend. If you already know about backend and the React is the second thing you have taken then you can create your own API for that matter, but for this article will go for a fake one.

For creating a fake API will be using super awesome json-server. The steps for this are super simple and easy to follow. For making API call from our Notesy app we will be using another awesome package called axios.

We will head in this direction:

  • Setup JSON Server
  • Configure Axios
  • Create Service Endpoints
  • Invoke API - Get All
  • Invoke API - Add
  • Invoke API - Remove
  • Configure Error Handling
  • Configure Loader

Setup JSON Server

Steps to setup JSON server are easy-peasy. First install the package globally:

npm install -g json-server

Add this command to the package.json file:

"scripts": {
  "server": "json-server data.json"
}

As you can see in the command, we are referring to a data.json file. This file will hold the data for us, to which the json-sever will do the transaction (CRUD). The data.json file will be acting as a backend database in this case.

Let us create the data.json file for the same:

{
  "notes": [
    {
      "id": "44b0102d-bd91-4d12-b218-141ec217a6ac",
      "note": "Note 1"
    },
    {
      "id": "7a7d50cc-fe43-47df-931c-40e197a38251",
      "note": "Note 2"
    }
  ]
}

Here, we have defined an array of notes. Each note object has id and name. For starter we are going with some initial data. Also, notice we are using a GUID for id.

We need to run the below command in the terminal to run our server, make sure you run this command before our "npm run dev" otherwise you will get API failure errors:

npm run server

Once this executed your API is up and running on the default port 3000 and localhost. You will get default path to your endpoint as notes and get bunch of HTTP method like GET, POST, PUT, DELETE to perform a CRUD:

GET http://localhost:3000/notes/
GET http://localhost:3000/notes/{id}
POST http://localhost:3000/notes/
PUT http://localhost:3000/notes/{id}
DELETE http://localhost:3000/notes/{id}

For, GET, PUT and DELETE we must need to pass the id. For POST and PUT we need to pass the note object as part of the request body.

Configure Axios

Install the Axios package without taking any nap:

npm i axios

Create a folder named "services" for keeping our services intact from other files. Inside this folder create a service namely "services\NoteService.js". Add the Axios configuration code in this file as a first thing:

import axios from 'axios';

const client = axios.create({
  baseURL: `http://localhost:3000/notes/`,
});

Here, we are importing the axios package and then we are creating a constant called client using the axios.create() method. The method accept an object using which you can setup your Axios client instance. Right now we are just providing the baseURL to it as in our case things are enormously simple.

Create Service Endpoints

Now is the time to create our endpoints (only 3 - get, post, delete), lets add see them one by one:

const getAll = async () => {
  const response = await client.get();
  return response.data;
};

In the getAll() method we are calling the get() method of Axios client without passing any parameter to the method, since for calling the get all we don't need any extra parameter. Notice the get() method is returning a Promise and for that we are using JavaScript's async-await syntax. If you want to know more about async operation in JavaScript, checkout this learning series Async Operations In JavaScript.

const add = async note => {
  const options = {
    headers: {
      'Content-Type': 'application/json',
    },
  };
  const response = await client.post('', note, options);
  return response.data;
};

The loveable add() method requires "Content-Type" header for specifying the type of data we are passing to the POST method. this options constant we are passing as a third parameter to the client's post() method.

const remove = async id => {
  const response = await client.delete(`${id}`);
  return response.data;
};

Remove method is straight forward, we are using the delete() method and passing the id as parameter for deletion of the note object.

const NoteService = {
  getAll,
  add,
  remove,
};

export default NoteService;

Finally, we are exporting these methods from NoteService file as shown.

Invoke API - Get All

Time to do some action! Instead of getting static hard coded data from data.js file, this time we will bring it from json-server. For this we need to do changes in our List.jsx file. Open the List.jsx file and update only the useEffect() hook and nothing else.

// List.jsx

useEffect(() => {
  const getAllNotes = async () => {
    const data = await NoteService.getAll();
    const notesAll = data.reverse();
    
    getAll(notesAll);
  };
  
  getAllNotes();
}, []);

Here, we have added one async method called getAllNotes(), this method is much needed one, because we want to make an API call through NoteService getAll() method which is async in nature. Most importantly we cannot make the useEffect callback method as async, never do that it's an error and have implications. So, due to this we have created getAllNotes() method which we are calling directly within useEffect().

Speaking about the method, we are calling our configured API method getAll() that provides data which we are just reversing out to show the last one first. As mentioned in previous article, consider useEffect() as page load event kind of.

Invoke API - Add

For add API its easy-cheesy, just add the add() method of NoteService before calling the add method of the prop inside Form.jsx component.

// Form.jsx

const handleSubmit = async event => {
  ...
  const newNote = {
    id: uuid(),
    note,
  };
  
  await NoteService.add(newNote);
  
  add(newNote);
  ...
}

Beautifully done; we don't want anything from the add() method so just leaving it like that. Make sure to make the handleSubmit as async, there is no problem with event handler to become an async method.

Invoke API - Remove

Another simple stuff, just call the remove API method before calling the prop remove method. Notice that we are calling the API before updating the UI state, this is important to understand if you are starting with your coding journey, we must always show the actual picture to the user, if the API operation fails then UI must also be failing (it's okay to show error and fail) otherwise UI will get updated even though backend operation is not succeeded.

// Item.jsx

const handleRemove = async () => {
  try {
    await NoteService.remove(note.id);
    remove(note.id);
  } catch (error) {
    window.alert(`Error Occurred: ${error.message}`);
  }
};

Configure Error Handling

In all the other cases like add and remove we are doing this already, to make sure if our API fails, the catch block at least shows the browser's alert message, later you can show some catchy modal popups. This we not included in our getAll() method, lets complete this exercise.

// List.jsx

useEffect(() => {
  const getAllNotes = async () => {
    try {
      const data = await NoteService.getAll();
      const notesAll = data.reverse();
      
      getAll(notesAll);
    } catch (error) {
      window.alert(`Error Occurred: ${error.message}`);
    }
  };
  
  getAllNotes();
}, []);

Here, we just wrapped the code within try-catch and added the alert() method.

Configure Loader

API calls are the real deal, they are async in nature, their completion time is unknown and dynamic, and your user's patience is volcanic. So, to have a win-win situation here a need of Loader is inevitable. Let us create a loader component first.

// Loader.jsx

import { FaSnowflake } from 'react-icons/fa';

const Loader = () => {
  return (
    <>
      {
        <div className="loader">
          <FaSnowflake className="text-success" size={'5rem'} />
        </div>
      }
    </>
  );
};

export default Loader;

A must! create the above used CSS class named loader in index.css file:

// index.css

.loader {
  width: 100%;
  height: 100%;
  position: fixed;
  top: 0;
  left: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: rgba(255, 255, 255, 0.5);
  z-index: 5;
}

.loader svg {
  animation: spin infinite 1.5s linear;
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
}

Done! Now come we will add the Loader to App.jsx and control the show/hide feature of loader based on demand. Why we need to control the show/hide is because before making an API call we will show the loader and once the call is completed (passed/failed) we need to hide the loader. So, this behavior needs to be in our control. For this we are creating a state in App.jsx and pass this state as prop and control it using an event. Just repeating the previous article's steps.

// Home.jsx

const [load, setLoad] = useState();

const loader = state => {
  setLoad(state);
};

<Form add={add} loader={loader} />
<List notes={notes} remove={remove} getAll={getAll} loader={loader} />
<Empty notes={notes} />
{load && <Loader />}

Here, fast-forwarding the process and creating a state then passing the load prop and updating the state using an event called loader. Added the Loader component based on the Boolean load state. So, normally the Loader component won't be shown, it will only be disabled if load state is truthy.

Pass the load state and loader event as a prop to all the components Form, List and Item:

// List.jsx

const List = ({ notes, remove, getAll, loader }) => {

  useEffect(() => {
    const getAllNotes = async () => {
      try {
        loader(true);
        ...
      } catch (error) { ...
      } finally {
        loader(false);
      }
    };
  
    getAllNotes();
  }, []);
  ...
}
// Form.jsx

const Form = ({ add, loader }) => {
  const handleSubmit = async event => {
    try {
      event.preventDefault();
      loader(true);  
      ...
    } catch (error) { ...
    } finally {
      loader(false);
    }
  };
}
// Item.jsx

const Item = ({ note, remove, loader }) => {
  const handleRemove = async () => {
    try {
      loader(true);
      await NoteService.remove(note.id);
      remove(note.id);
    } catch (error) { ...
    } finally {
      loader(false);
    }
  };
}

Same steps taken for these components, before making call showing the loader using loader(true) and hiding it using a finally block by calling loader(false).

Feel free to comment down if you are following along and finding errors and issues will be happy to help.


Zaki Mohammed
Zaki Mohammed
Learner, developer, coder and an exceptional omelet lover. Knows how to flip arrays or omelet or arrays of omelet.