I started writing about working with the server, it was supposed to be just one article but here we are π
In the first part of this article we created a fake backend database using JSON SERVER β we created a phonebook app that lets us add new contacts and also updates the backend database.
In this part, we are going to work through deleting a contact, and building a search feature.
Creating Components
The App.js
file is starting to look a lot clustered so before we jump into deleting and adding other features, let's refactor the code to make it cleaner by creating components.
In src
folder, create a folder called components
and a file called Header.js
β remember that components' names should always start with caps.
In Header.js
we import react
and create the Header
component.
Using destructuring, this component would be accepting a text
props that would be passed from App.js
import React from 'react';
const Header = ({ text }) => {
return (
<>
<h3>{text}</h3>
</>
);
};
export default Header;
We import the Header
component in App.js and replace the appropriate headers passing the value for the text props
import Header from './components/Header';
<Header text='Add New Contact'/>
<Header text='Contact List'/>
Now, we create another component for form
called AddContact.js
In AddContact.js
, we get the different props needed for the inputs, onSubmit
, and onChange
, and pass them to the appropriate elements that need them. Since the onChange
and value
are needed by two different input fields, we give them befitting names to easily differentiate them.
import React from 'react';
const AddContact = ({
onSubmit,
onChangeName,
onChangeNum,
valueName,
valueNum,
type,
}) => {
return (
<>
<form onSubmit={onSubmit}>
<div>
name: <input type={type} onChange={onChangeName} value={valueName} />
</div>
<div>
number: <input type={type} onChange={onChangeNum} value={valueNum} />
</div>
<button>ADD NEW</button>
</form>
</>
);
};
export default AddContact;
In App.js
, we replace the form element with our AddContact
component.
import AddContact from './components/AddContact';
return (
<>
<h2>PHONEBOOK</h2>
<Header text='Add New Contact' />
<AddContact
onSubmit={handleSubmit}
onChangeName={handleNameChange}
onChangeNum={handleNumberChange}
valueName={newName}
valueNum={newNumber}
type='text'
/>
<Header text='Contact List' />
{persons.map((person) => (
<p key={person.id}>
{person.name} {person.number}
</p>
))}
</>
);
AddContact
component has a button element that can also be extracted into its own component called Button.js
passing it a text props
import React from 'react';
const Button =({text})=>{
return(
<>
<button>{text}</button>
</>
)
}
export default Button
Then we replace the button
element with the Button
component in the AddContact
file
import Button from './Button';
<form onSubmit={onSubmit}>
<div>
name: <input type={type} onChange={onChangeName} value={valueName} />
</div>
<div>
number: <input type={type} onChange={onChangeNum} value={valueNum} />
</div>
<Button text='ADD NEW'/>
</form>
Next, we create another component to render the contact list called Contacts.js
, we pass in two props for the name and number(always using destructuring)
import React from 'react';
const Contacts = ({ name, number }) => {
return (
<>
<p>
{name} {number}
</p>
</>
);
};
export default Contacts;
In App.js, we replace the p
element with the Contacts
component when mapping through persons
array
import Contacts from './components/Contacts';
{persons.map((person) => (
<Contacts key={person.id} name={person.name} number={person.number} />
))}
We check our console to make sure there are no errors and test that we can still add contacts. The code looks cleaner and more readable now.
Creating a delete feature
We have to add a delete button to each contact on the list to be able to delete them individually. Since we already have the Button
component, we easily import it into our Contacts
component.
import Button from './Button';
const Contacts = ({ name, number }) => {
return (
<>
<p>
{name} {number}
<Button text='Delete' />
</p>
</>
);
};
export default Contacts;
In App.js, we create a function handleContactDelete
to handle delete β For now, we would just log "delete' to the console
const handleContactDelete = () => {
console.log('delete');
};
We pass this function as a prop to the Contacts
component since that's where the Button
component is located
{persons.map((person) => (
<Contacts
key={person.id}
name={person.name}
number={person.number}
handleDelete={handleContactDelete}
/>
))}
In the Contacts
component file, we pass down handleDelete
prop to the Button
component
const Contacts = ({ name, number, handleDelete }) => {
return (
<>
<p>
{name} {number}
<Button text='Delete' handleDelete={handleDelete} />
</p>
</>
);
};
Finally, In the Button
component file we add the onClick event to the button tag and assign it the handleDelete
prop
const Button = ({ text, handleDelete }) => {
return (
<>
<button onClick={handleDelete}>{text}</button>
</>
);
};
This process of passing down props through components that don't need them to get to components that need them is called prop drilling, it can be cumbersome when there are a lot of prop layers to get to β In a more complex project, React Context API is recommended to avoid prop drilling but for now, this will do.
Now, when we click on any delete button, we see 'delete' logged on our console. The handleContactDelete
function and prop passing worked so we can go ahead to write a proper delete function.
Since we are trying to delete contacts individually, we go to the handleContactDelete
function in our App.js
file and pass it a parameter(id). We filter the persons
array using the filter method and return persons that their ids do not match the id that's passed in as an argument. Then we update the persons
state.
const handleContactDelete = (id) => {
console.log('delete');
setPersons(
persons.filter((l) => {
return l.id !== id;
})
);
};
We pass in an argument to the function, so we change the value passed in as prop in the Contacts componenthandleDelete={handleContactDelete}
becomes handleDelete{()=>handleContactDelete(person.id)
Checking our application we can see the delete button works but it doesn't update the database when you refresh your page the deleted contact returns.
Deleting from the server
We use axios delete
method to delete from the server, the method requires the URL of the individual contact that is to be deleted so we can use the id
to locate that. When adding the URL to the axios delete
method, we use template literals(backticks)
so we can add the id params to the URL.
const handleContactDelete = (id) => {
axios
.delete(`http://localhost:3001/persons/${id}`)
.then((response) => {
setPersons(
persons.filter((l) => {
return l.id !== id;
})
);
});
};
Now any deleted contact also gets deleted from the database.
Searching through contacts
I wrote an article about building a search component and we are going to follow the same steps there.
We create a new component called Search.js
, the component returns an input field and two props passed from App.js
import React from 'react';
const Search = ({ type, value }) => {
return (
<>
<div>
Search Contacts <input type={type} value={value} />
</div>
</>
);
};
export default Search;
In App.js
, we import Search
component and pass the props type
and value
to it.
import Search from './components/Search';
<Search type='search' value='SEARCH CONTACT' />
We also need an onChange function to handle changes made in the value and we need a state to store the value from the input.
const [search, setSearch] = useState('')
const handleChange = (e) => {
setSearch(e.target.value);
};
<Search type='search' value={search} onChange={handleChange} />
We also have to update the Search
component with the onChange prop that has been added
const Search = ({ type, value, onChange }) => {
return (
<>
<div>
Search Contacts <input type={type} value={value} onChange={onChange} />
</div>
</>
);
};
The search bar is displayed on our page, but the functionality doesn't work yet.
We have to filter through the persons
state and return the contacts that match a letter or word typed in the search bar. We also use the toLowerCase()
function to return correct data irrespective of caps.
const filteredPersons = persons.filter((person) =>
person.name.toLowerCase().includes(search.toLowerCase())
);
We map through filteredPersons
instead of mapping through persons
state in App.js
{filteredPersons.map((person) => (
<Contacts
key={person.id}
name={person.name}
number={person.number}
handleDelete={() => handleContactDelete(person.id)}
/>
))}
The search feature works properly now.
Recap
In this article, we created an add contact feature, added delete buttons, were able to delete individual contacts, and created a search feature that lets us search for a particular contact β We also learned how to update the backend for each of these functionalities.
It was interesting for me to code, test the app and write simultaneously π.
In the next article, I would try to improve the app by adding a feature that allows already added numbers to be replaced by new ones and maybe give it a bit of styling.
As usual, pardon all grammatical errors and drop a comment if there are better or simpler ways to achieve these features. I would love to learn π
Link to repo