React: Working with the server(Part 3)

ยท

8 min read

I've managed to turn this article into a series โ€” hope you've enjoyed it so far ๐Ÿ˜„.

In the last part, we cleaned up the code by creating components, we added a delete button to delete each contact from our display and from the backend, we also added a search feature.

In this article, we are going to add a confirmation window to the delete feature, we are also going to work on updating contacts โ€” in this case, replacing the number for a name that already exists on the phonebook with a new number.

I strongly advise going through the last part of this article before jumping on this.

Let's get started ๐Ÿ˜ƒ

Adding a Delete Prompt

Right now on our app when a contact gets deleted, it goes away forever without any confirmation. We want to allow users to confirm if they want a contact to be deleted.

To accomplish this, we use window.confirm() method.

In handleContactDelete we add the window.confirm() method above the axios.delete method, we need to get access to the name of the contact being deleted so we pass in a parameter to the handleContactDelete function, let's call it item.

Using template literal, our code would look like this

  const handleContactDelete = (id, item) => {
    window.confirm(`Delete ${item} from phonebook?`)

    axios.delete(`http://localhost:3001/persons/${id}`).then(() => {
      setPersons(
        persons.filter((l) => {
          return l.id !== id;
        })
      );
    });
  };

Now we need to pass in person.name as an argument to the handleContactDelete function reference found in App.js

 handleDelete={() => handleContactDelete(person.id, person.name)}

When we try to delete a contact, a window pops up asking if we want to delete โ€” clicking okay deletes the contact, clicking cancel also deletes the contact.

To fix this, we use a ternary expression.

  const handleContactDelete = (id, item) => {
    window.confirm(`Delete ${item} from phonebook ?`)
      ? axios.delete(`http://localhost:3001/persons/${id}`).then(() => {
          setPersons(
            persons.filter((l) => {
              return l.id !== id;
            })
          );
        })
      : null;
  };

This simply means when the confirmation pops up and okay is clicked the contact gets deleted else do nothing. If you have eslint extension installed on vscode it would show some red error lines, you can place this // eslint-disable-next-line no-unused-expressions above window.confirm to remove the error lines.

Now when we try to delete, it asks for confirmation โ€” we can delete or cancel the action if we want.

Updating a contact

We want to change the functionality to update new numbers ie if a number is already added to an existing user, the new number will replace the old number, the app will also confirm the contact already has a number using window.confirm().

In the first part of this series, we used alert() to show that there was an existing name or number.

The old logic was if newName is found in the nameCheck array or newNumber is found in numCheck an alert pops up saying contact already exists โ€” if that's not the case, the name and number are added to the persons array and displayed on the contact list.

    const nameCheck = persons.map((person) => person.name);
    const numCheck = persons.map((person) => person.number);
    nameCheck.includes(newName) || numCheck.includes(newNumber)
      ? alert(`Contact already exists, Change it`)
      : axios
          .post('http://localhost:3001/persons', addPerson)
          .then((response) => {
            setPersons(persons.concat(addPerson));
            setName('');
            setNumber('');
          });
  };

We remove the numCheck part since we want to replace old numbers with new ones.

To achieve this replacement, we use chained ternary expression โ€” I'll explain so please stay with me.

The new logic is if newName is found in the nameCheck array, a window confirmation pops up asking if we want to replace the number. On confirmation, we use the axios.put method to pass in a new object containing the new number and replace the old number on the backend. If we cancel confirmation, no changes happen. โ€” if newName is not included in the nameCheck array, we add the name and number as normal using axios.post and update the persons array using setPersons.

It's not as complicated as it seems, so I'll break it down further โ€” first, we remove alert() and add window.confirm()

    nameCheck.includes(newName)
      ? window.confirm(`${newName} already exist, replace old number with new one?`)
      : axios
          .post('http://localhost:3001/persons', addPerson)
          .then((response) => {
            setPersons(persons.concat(addPerson));
            setName('');
            setNumber('');
          });
  };

Now we need to chain the expression so that it does something when we click on the window.confirm() prompt โ€” for now, we log a string to the console

  nameCheck.includes(newName)
      ? window.confirm(
          `${newName} already exist, replace old number with new one?`
        )
        ? console.log('it works')
        : null
      : axios.post('http://localhost:3001/persons', addPerson).then(() => {
          setPersons(persons.concat(addPerson));
          setName('');
          setNumber('');
        });
  };

When we click on okay to confirm the replacement of number, we see it works logged on our console, and when we cancel nothing happens which is what we want. Awesome! now we replace console.log() with axios.put() method which is used to update data on the backend โ€” axios.put() would require a new object passed to it, we would create that โ€” we would also need to access the id of each contact we want to be replaced.

const addNewNumber = {
      name: newName,
      number: newNumber
    }

There is no need to add id to the above object since the backend generates an id for us, we can also remove the id we added onaddPerson object.

We need to retrieve individual id for each contact to add to the API URL that's needed by axios.put(). This was tricky for me to accomplish because there was no way I could access person.id and I couldn't pass down the id as a parameter through the handleSubmit function. I had to bring out a paper and pen and drew out what I wanted to accomplish before this method came to mind โ€” If you are reading this and there is an easier way to accomplish please drop a comment.

So what I did was map through the persons array and return the id of names that matched with newName and returned null if they didn't match, then I filtered the nulls so I could have just the id

 const id = persons
             .map((person) => (person.name === newName ? person.id : null))
             .filter((n) => n != null);

Now we have an id variable that matches with the particular contact we want to replace. We use this with the API URL.

Remember to use // eslint-disable-next-line no-unused-expressions above windows.confirm() to remove eslint error lines on Vscode.

    nameCheck.includes(newName)
      ? window.confirm(
          `${newName} already exist, replace old number with new one?`
        )
        ? axios
            .put(`http://localhost:3001/persons/${id}`, addNewNumber)
            .then((response) =>
              setPersons(
                persons.map((person) =>
                  person.name !== newName ? person : response.data
                )
              )
            )
        : null
      : axios.post('http://localhost:3001/persons', addPerson).then(() => {
          setPersons(persons.concat(addPerson));
          setName('');
          setNumber('');
        });
  };

To update the persons state, we simply map through the persons array and return our new array only when the newName matches person.name.

I tried to just concat directly here, that is setPersons(persons.concat(addNewNumber) it updated at the backend but did not re-render the page the changes only took effect when I manually refreshed the page. The map method creates a new array that only returns when the newName is the same as any name on the old array, if this condition isn't met person is just returned.

Testing our app, we can see that the contacts update successfully.

Extracting into modules

We separate our backend from the main code to make our code cleaner and readable. In src folder, we create a folder called services and create a file called persons.js

In persons.js, we import axios and save the API URL in a variable called url

import axios from 'axios';

const url = 'http://localhost:3001/persons';

Now we create the different functions for all our methods(post, put, get, delete) and export them.

const getAll = () => {
  const request = axios.get(url);
  return request.then((response) => response.data);
};
const create = (obj) => {
  const request = axios.post(url, obj);
  return request.then((response) => response.data);
};

const deleteObj = (id) => {
  const request = axios.delete(`${url}/${id}`);
  return request.then((response) => response.data);
};
const replaceNum = (id, obj) => {
  const request = axios.put(`${url}/${id}`, obj);
  return request.then((response) => response.data);
};

export default { getAll, create, deleteObj, replaceNum };

In App.js we import personService from the service folder and remove the axios import because we don't need it anymore

import personService from './services/persons';

Then we replace

 useEffect(() => {
    axios.get('http://localhost:3001/persons').then((response) => {
      setPersons(response.data);
    });
  }, []);

nameCheck.includes(newName)
      ? window.confirm(
          `${newName} already exist, replace old number with new one?`
        )
        ? axios
            .put(`http://localhost:3001/persons/${id}`, addNewNumber)
            .then((response) =>
              setPersons(
                persons.map((person) =>
                  person.name !== newName ? person : response.data
                )
              )
            )
        : null
      : axios.post('http://localhost:3001/persons', addPerson).then(() => {
          setPersons(persons.concat(addPerson));
          setName('');
          setNumber('');
        });
  };

const handleContactDelete = (id, item) => {
    // eslint-disable-next-line no-unused-expressions
    window.confirm(`Delete ${item} from phonebook ?`)
      ? axios.delete(`http://localhost:3001/persons/${id}`).then(() => {
          setPersons(
            persons.filter((l) => {
              return l.id !== id;
            })
          );
        })
      : null;
  };

with

useEffect(() => {
    personService.getAll().then((initialData) => {
      setPersons(initialData);
    });
  }, []);

nameCheck.includes(newName)
      ? window.confirm(
          `${newName} already exist, replace old number with new one?`
        )
        ? personService
            .replaceNum(id, addNewNumber)
            .then((numNew) =>
              setPersons(
                persons.map((person) =>
                  person.name !== newName ? person : numNew
                )
              )
            )
        : null
      : personService.create(addPerson).then(() => {
          setPersons(persons.concat(addPerson));
          setName('');
          setNumber('');
        });
  };

const handleContactDelete = (id, item) => {
    // eslint-disable-next-line no-unused-expressions
    window.confirm(`Delete ${item} from phonebook ?`)
      ? personService.deleteObj(id).then(() => {
          setPersons(
            persons.filter((l) => {
              return l.id !== id;
            })
          );
        })
      : null;
  };

This cleans up the app.js and also keeps the backend in its module. Promise chaining was used to achieve this and you can read up more on it here.

Our app still works fine, and there are no errors on the console.

If you've heard of CRUD operations, that's basically what we've achieved in these series โ€” We created, read, updated, and deleted data from our app and the backend.

Now we can apply some styling to bring life to it ๐Ÿ˜€

Thanks for reading and I hope you learned something. If there are simpler ways to achieve these please comment and as usual pardon all grammatical errors ๐Ÿ‘

Link to repo

ย