A phone book is a directory used to store and organize contact information.
In this article, we will be developing a phonebook application using TypeScript and React.
TypeScript is a typed superset of JavaScript that allows us to catch errors at compile-time
Our phone book application will allow users to add, edit, and delete contacts.
By the end of this article, you will have a fully functional phone book application built with TypeScript and React.
Here is how the final application will look like
View Demo Here
We will be using vite
to setup our application. Vite is a build tool that is specifically designed for modern web development.
Note: Ensure that Node.js version 14.18+ is installed on your system
You can download Node.js from the official website here, and npm will be installed automatically with Node.js.
Firstly, navigate to the directory where you want to develop your application and scaffold your application with Vite. We can do this by running the following command:
npm create vite@latest
This would ask you for the project name, framework, and variant.
β For project name, you can enter phonebook
β For framework, select React from the list of options
β And for variant, select TypeScript.
When its done, navigate into your project folder by running the command cd phonebook
then run the command
npm install
to install all required dependencies and then npm run dev
to start your application.
You can open your project on Visual Studio code.
Now let's install the dependencies we will need for this application.
Open the command line or bash on your project directory and run the command:
npm install bootstrap@5.2.3 react-bootstrap@2.7.0 react-icons@4.7.1
Styling the application:
To add styling to our phonebook application, open the index.css file existing in your project folder, clear-off the existing styles and add the following content:
Next, you can access the main.tsx file in your project folder and place the import statement for the bootstrap CSS before the import statement for index.css, as shown below:
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css'
Inside your src
folder, create a new folder. Name it components
. Create a new file, call it Header.tsx
and add the following content to it.
Here, we import the "FC" (Functional Component) type from the 'react'.
We then define the Header
component as a function component. The ": FC" part after "Header" specifies that the component is of type "FC", which is a shorthand for "React.FunctionComponent"
. This type is used to indicate that the component is a function that returns JSX
.
The function itself returns a JSX element, which in this case is a header element with an h1 element inside it.
You can now test the application by run this command npm run dev
.
If every thing works fine, you should see this page display on your browser:
Now let's create the form that would enable us to take input from users.
Create a new file in the components
. Name it ContactForm.tsx
and add the following code to it.
Here, we import several items from the 'react' and 'react-bootstrap' packages:
We defined the component as a functional component, using the FC type from React. The function is named ContactForm.
We are using the useState
hook to initialize a piece of state called contact
, which is an object that contains the values of our form fields. Initially, all of the fields are empty strings.
We then define an event handler function called handleOnChange
that will be called whenever the user types something into one of the form fields.
This function uses destructuring to get the name and value properties of the target object, which is the input element that triggered the event.
It then uses the setContact
function to update the contact state, by returning a new object that has the same properties as the previous contact object, but with the name property set to the new value.
We defined another event handler function called handleOnSubmit
that will be called when the user submits the form.
This function prevents the default form submission behavior (which would cause a page refresh) and logs the contact object to the console.
We have not implemented the logic for submitting the form yet.
We returned a Form
element from the react-bootstrap package. The onSubmit
prop is set to the handleOnSubmit
function, so that it will be called when the form is submitted. The form also has a className
of "contact-form".
The component returns several Form.Group elements from the react-bootstrap package. Each Form.Group represents a form field, with a controlId prop that uniquely identifies it.
The Form.Control element within each Form.Group represents the actual input field, with props for name, value, type, placeholder, and onChange.
The component also returns a Button
element from the react-bootstrap package, which is used to submit the form. It has a variant of "primary", a type of "submit", and a className of "submit-btn".
Now, let's add our Header and ContactForm
component to our App.tsx
component. Open the App.tsx file and import these components there. Your App.tsx
file should now look like this:
Checking your application on your broswer, it should now look like this:
Storing submitted contact information:
Now, let's display the added phone contacts on the page.
To properly manage and store our contacts, we will be using the useReducer
hook, a built-in hook in React that would enable us to manage state using a reducer function.
Adding types to our application:
In your src
folder, create a file, name it types.ts
and add the following content to it:
Here, we have two interfaces- Contact and Action interface.
The Contact
interface defines the shape of an object that represents a contact.
The Action interface defines the shape of an object that represents an action.
Create a new folder inside your src
folder and name it reducers
. Create a file inside the reducers folder and name it contactsReducer.ts
and add the following content into it:
Here, we firstly import the Contact
and Action
interfaces from the types.ts
file
We then define an interface called AppState
which has a single property, contacts, which is an array of Contact objects. This represents the state of the application that this reducer manages.
We define and export the function contactsReducer
which takes two arguments: state and action.
The state argument represents the current state of the application, while the action argument represents an action that has been dispatched to update the state.
The return value of this function is the new state of the application.
We use a switch statement to check the type property of the action argument to determine what action is being performed.
In this case, the type property is expected to be a string that matches the value "ADD_CONTACT", which will be defined later.
We returned a new object that represents the new state of the application.
It copies all the properties of the previous state using the spread operator ...state, and then updates the contacts property with a new array that includes all the previous contacts (...state.contacts) plus the new contact that was passed in the action.payload property.
default: return state;
: This default case returns the current state unchanged if the action type is not recognized.
In order to use the contactsReducer
, let's import it into App.tsx.
Now, open the App.tsx file and update the code inside to this one below:
const initialState: AppState = { contacts: [] };
. In this line we define the initial state for thecontactsReducer
.
const [state, dispatch] = useReducer(contactsReducer, initialState);
In this line of code we use the useReducer
hook from React to create a state management system for the App component.
useReducer
takes two arguments: the first argument is a reducer function, and the second argument is the initial state.
In this case, the contactsReducer
function is used as the reducer, and the initialState
object is passed as the initial state.
useReducer
returns an array with two elements: the current state (state) and a dispatch function that can be used to dispatch actions to update the state.
The state variable is used to store the current state of the component, while dispatch is a function that can be used to update the state by dispatching actions to the contactsReducer
function.
<ContactForm dispatch={dispatch} />
Here, We pass the dispatch function as a prop to the ContactForm
component so we can dispatch the action to add a contact to the contacts array.
You will get a Typescript error highlighted at dispatch
. This is because when we pass any prop to the ContactForm component, we need to state the type of the dispatch
prop in the component props list.
In order to specify the dispatch prop type, you can declare an interface named ContactFormProps
above the component declaration in the ContactForm.tsx
file.
Add this code to your ContactForm.tsx file
interface ContactFormProps {
dispatch: React.Dispatch<Action>;
}
We use it inside the angle brackets after : FC.
Now, we use the dispatch prop like this:
const ContactForm: FC<ContactFormProps> = (props) => {
const { dispatch } = props;
// ....
};
Also ensure you add an import for the Action type at the top of the file like this:
import { Action } from '../types';
Your updated code in the ContactForm.tsx
file should now look like this:
So what we do in the code is to indicate that the ContactForm
component will receive a prop with the name dispatch
whose type is React.Dispatch.
Implementing the logic to submit our form:
inside the handleOnSubmit
method in ContactForm.tsx
file, add the following code:
dispatch({
type: 'ADD_CONTACT',
payload: contact
})
Here, we dispatch an action to the Redux store using the dispatch() function.
The action is an object with a type property set to the string 'ADD_CONTACT' and a payload property set to the variable contact.
Your ContactForm.tsx
file will look like this:
Adding the contact List Page:
Now, we want to display all the contacts we will be adding using the form.
Let's create a Page to handle that.
Inside your components folder, create a file, call it ContactList.tsx
and add the following content.
Here, the ContactList
component takes in an object with a contacts property of type Contact as its props. The component then renders a table that displays the contacts in the contacts prop.
The component returns a JSX that renders the contacts array as a table. It maps over the array and renders a table row for each Contact object, displaying their firstName, lastName, phoneNumber and address properties in separate columns.
We can now display the ContactList
component in the App.tsx
file.
Import the ContactList.tsx
file into the App.tsx file and add it below the ContactForm component.
Your App.tsx file should now look like this:
Here, we added a conditional statement that renders the ContactList
component if the contacts array in the state has any items in it. If the array is empty, the ContactList
component will not be rendered.
If you check your browser console, you will see a warning in the console as shown below:
This warning message is indicating that each child element of the list returned by the map function in ContactList
should have a unique "key" prop.
In React, when rendering a list of elements, it's important to provide a unique key for each item in the list.
This allows React to efficiently update and re-render only the elements that have changed, instead of re-rendering the entire list every time the state changes.
Let's fix this.
In the ContactForm.tsx
file, change the below code:
dispatch({
type: 'ADD_CONTACT',
payload: contact
});
to this one:
dispatch({
type: 'ADD_CONTACT',
payload: {
id: Date.now(), // returns current timestamp
...contact
}
});
Here, we have added extra id
property to the payload
Upon adding it, Typescript error will be underlined on it. Let's fix it.
Open the types.ts file and add id to the Contact interface like this:
export interface Contact {
id: number;
firstName: string;
lastName: string;
phoneNumber: string;
address: string;
email: string;
}
Now open the ContactList.tsx file and add the id as key prop to the child tr
elements that are returned by the map function like this:
{contacts.map(({ id, firstName, lastName, phoneNumber, address, email }) => (
<tr key={id}>
<td>{firstName}</td>
<td>{lastName}</td>
<td>{phoneNumber}</td>
<td>{address}</td>
<td>{email}</td>
</tr>
))}
Let's do a bit of refactoring to our code.
We will separate the component for displaying different information so itβs easy to re-use and test that component when required.
Create a ContactItem.tsx
file inside the components folder and move the contents from the map method of the ContactList.tsx file inside it.
Your ContactItem.tsx
file will look like this:
Then change the map method of the ContactList.tsx
file from the below code:
{
contacts.map(({ id, firstName, lastName, phoneNumber, email, address }) => (
<tr key={id}>
<td>{firstName}</td>
<td>{lastName}</td>
<td>{phoneNumber}</td>
<td>{email}</td>
<td>{address}</td>
</tr>
));
}
To this:
{contacts.map(({ firstName, lastName, phoneNumber, address, email, id}) => (
<ContactItem
key={id}
id={id}
firstName={firstName}
lastName={lastName}
phoneNumber={phoneNumber}
email={email}
address={address}
/>
))}
Do ensure you import the ContactItem
into the ContactList.tsx
like this:
import ContactItem from './ContactItem';
Now let's add the ability to Edit and Delete a phone contact.
We will add the Edit and Delete icons using the react-icons
library.
We have installed the library when we were starting the project.
In the ContactList.tsx
file, let's add two additional items for editing and deleting in the table heading like this.
<thead className='contacts-list-header'>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Phone</th>
<th>Email Address</th>
<th>Adress</th>
<th>Edit</th>
<th>Delete</th>
</tr>
</thead>
Now, open the ContactItem.tsx
file and replace it with this contents:
Here, we added this - import { AiFillDelete, AiFillEdit } from 'react-icons/ai';
to import the Edit and Delete icons from the react-icon library.
<td>
<AiFillEdit size={20} color='blue' className='icon' />
</td>
<td>
<AiFillDelete size={20} color='red' className='icon' />
</td>
We added this code to make use of our icons in the table data.
Your application should now look like this on your browser:
We will now implement the functionality for editing a contact.
In order to show the edit modal popup, we will make use of the react-bootstrap modal component.
Inside your component folder, create a file and name it EditModal.tsx
.
Add the this content below inside the file.
Here, we define a EditModal functional component. The component accepts toggleModal, dataToEdit, showModal, dispatch
props.
β toggleModal
which is a function that will toggle the state value used to hide or show the modal
β dataToEdit
will be an object containing contact details of the contact to be edited
β showModal
will be a boolean value indicating whether to show or hide the modal
β dispatch
function which will be helpful to dispatch the edit contact action
The component uses the Modal component from the react-bootstrap
package to render a modal dialog that displays a form for editing contact information. The ContactForm
component is used to render the form.
We also define the EditModalProps
interface, which describes the shape of the props object that the component receives.
Right now, our ContactForm
component accepts only the dispatch prop so letβs add the remaining required prop in the definition of the type.
Open the ContactForm.tsx
file and change the interface to the below code:
interface ContactFormProps {
dispatch: React.Dispatch<Action>;
dataToEdit: Contact | undefined;
toggleModal: () => void;
}
Don't forget to add an import for the Contact and Action at the top of the file like this:
import { Action, Contact } from '../types';
Now, update state object in the ContactForm
component to the content below:
const ContactForm: FC<ContactFormProps> = ({ dispatch, dataToEdit, toggleModal}) => {
const [contact, setContact] = useState({
firstName: dataToEdit?.firstName ? dataToEdit.firstName : '',
lastName: dataToEdit?.lastName ? dataToEdit.lastName : '',
phoneNumber: dataToEdit?.phoneNumber ? dataToEdit.phoneNumber : '',
address: dataToEdit?.address ? dataToEdit.address : '',
email: dataToEdit?.email ? dataToEdit.email : '',
});
Here we created a state object for the contact form, and populating its initial values based on any data that might be passed to the component for editing.
If any of the fields are edited by the user, the 'setContact' function can be used to update the state and re-render the component with the new values.
Update the handleOnSubmit
method to the below code:
const handleOnSubmit: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
if(!dataToEdit){
dispatch({
type: 'ADD_CONTACT',
payload:{
id: Date.now(),
...contact,
}
})
} else {
toggleModal();
}
};
So at application startup, the 'dataToEdit' variable will be undefined if the user has not clicked on the edit icon. If the edit icon has been clicked, 'dataToEdit' will contain the contact details to be edited.
To handle this, we check if we are currently editing a contact. If we are not, we dispatch the 'ADD_CONTACT' action to add the new contact. If we are editing a contact, we simply close the modal without taking any further action at the moment.
Once we have dispatched the 'ADD_CONTACT' action, we are calling the toggleModal
function to close the modal.
Modify the code for the 'Add Contact' button as follows:
<Button variant='primary' type='submit' className='submit-btn'>
Add Contact
</Button>
to this code:
<Button variant='primary' type='submit' className='submit-btn'>
{dataToEdit ? 'Update Contact' : 'Add Contact'}
</Button>
By default, the contact form will display the 'Add Contact' button text. If the user clicks on the edit icon, the 'dataToEdit' prop value will be set, and the modal will display the 'Update Contact' text while the user edits the contact information.
Now replace the current content of the App.tsx
file with the following code:
Here, two additional states have been declared in the code above.
The code includes two useState
functions. The first one controls whether the modal is visible or hidden, while the second one stores the details of the edited contact as an object.
Initially, the object is undefined, but once a value is set, it is of the Contact type with properties such as id, firstName
, lastName
, and phoneNumber
.
To avoid any future TypeScript errors while using dataToEdit
or setDataToEdit, the type is declared in angle brackets.
In the useEffect hook, the dataToEdit value is set back to undefined once the modal is closed, i.e., when the showModal
value is false.
In the toggleModal
function, the updater syntax of state is used to change the showModal state from true to false and vice versa.
If the modal is already open because the showModal
value is true, calling the toggleModal
function will close the modal. On the other hand, if the modal is already closed, calling the toggleModal
function will display the modal.
As you would observed in the App.tsx
file, dispatch
and handleEdit
are passed as props to the ContactList
component. Therefore, we need to accept them in the ContactList
component.
To do this, open the ContactList.tsx
file and replace its contents with the following code:
We can observe here that, handleEdit
function takes id as a parameter and does not return anything. Therefore, it has been defined in the ContactListProps
interface as follows:
interface ContactListProps {
// ...
handleEdit: (id: number) => void;
// ...
}
Next, we extract the handleEdit
and dispatch as props and pass them to the ContactItem
component as illustrated below:
<ContactItem
key={contact.id}
{...contact}
handleEdit={handleEdit}
dispatch={dispatch}
/>
Now, open the ContactItem.tsx
file and replace it with the following content:
Earlier, the ContactItem component was solely receiving attributes from the Contact interface.
we are now also providing the handleEdit
and dispatch props, and thus, we have defined another interface as follows:
interface ExtraProps {
handleEdit: (id: number) => void;
dispatch: React.Dispatch<Action>;
}
Upon reviewing the application, it can be observed that the modal displays the contact information of the selected contact being edited, as depicted below:
Implementing Edit functionality:
We currently have the user interface to edit and delete. Our next step is to include the code that will enable these functionalities.
To achieve this, you should access the contactsReducer.ts
file and append a switch case for the UPDATE_CONTACT
action.
Your contactsReducer.ts file should now look like this:
Here, The id of the contact and the updates object containing the new data are being sent for updating, as evident from the above.
The contact with the matching id is being updated using the array map method.
However, upon saving the file, a TypeScript error occurs with this case.
The reason for the error is that the Action interface specifies the payload as a Contact object with properties including id, firstName, lastName,, address and phoneNumber.
However, when handling the UPDATE_CONTACT case, we are only interested in the id and updates properties.
To resolve the error, we must inform TypeScript that the payload can be either a Contact object or a type that includes id and updates properties.
Now, in your types.ts file, let's add a new interface as shown below:
export interface Update {
id: number;
updates: Contact;
}
Then update the Action interface to this:
export interface Action {
type: 'ADD_CONTACT' | 'UPDATE_CONTACT'
payload: Contact | Update;
}
After this modification, payload is now a union type that can be either of Contact type or Update type, and type is a string literal type that can be either 'ADD_CONTACT'
or 'UPDATE_CONTACT'
.
Your types.ts file should now look like this:
Next, import the new action interface into the contactsReducer.ts file
However, you may encounter additional TypeScript errors in your contactsReducer.ts
once you make these changes. Lets see how to fix them.
So change ADD_CONTACT switch case from the below code:
case 'ADD_CONTACT':
return {
...state,
contacts: [...state.contacts, action.payload]
};
to this code:
case 'ADD_CONTACT':
return {
...state,
contacts: [...state.contacts, action.payload as Contact]
};
In this code above, we are using the as keyword for type casting, which explicitly informs TypeScript about the type of action.payload.
And now change the UPDATE_CONTACT switch case from the below code:
case 'UPDATE_CONTACT':
const { id, updates } = action.payload;
return {
...state,
contacts: state.contacts.map((contact) => {
if (contact.id === id) {
return {
...contact,
...updates
};
}
return contact;
})
};
To this one:
case 'UPDATE_CONTACT':
const { id, updates } = action.payload as Update;
return {
...state,
contacts: state.contacts.map((contact) => {
if (contact.id === id) {
return {
...contact,
...updates
};
}
return contact;
})
};
By specifying the type of action.payload as Update for the UPDATE_CONTACT case, we have resolved the TypeScript errors.
With this modification, the updated contactsReducer.ts
file will now look like this:
To update the contact, you need to dispatch the UPDATE_CONTACT action. You can do this by adding the following code inside the handleOnSubmit
method of the ContactForm.tsx
file, before calling the toggleModal
function:
Open the ContactForm.tsx
file and inside the handleOnSubmit
method, in the else block, before the toggleModal
function call, add the below code:
dispatch({
type: 'UPDATE_CONTACT',
payload: {
id: dataToEdit.id,
updates: {
id: Date.now(),
...contact
}
}
});
And inside the if condition after the dispatch call, add the below code:
setContact({
firstName: '',
lastName: '',
phoneNumber: '',
address: '',
email: ''
});
In this code, we are resetting the form state. This ensures that when we dispatch the ADD_CONTACT action, all the user-entered data is cleared out.
Now, if you check the application, you will notice that the contact can be edited successfully.
Upon adding the code in your handleSubmit, your final ContactForm.tsx
file should now look like this:
Implementing Delete Contact Functionality:
Let us proceed with the addition of the delete contact feature by opening the contactsReducer.ts
file and including a switch case for the DELETE_CONTACT action.
case 'DELETE_CONTACT': {
const { id } = action.payload;
return {
...state,
contacts: state.contacts.filter((contact) => contact.id !== id)
};
}
To remove a contact, we include only its id in the payload property and apply the array filter method to eliminate the corresponding contact
Now, in the types.ts file, change the Action interface to the following code:
export interface Action {
type: 'ADD_CONTACT' | 'UPDATE_CONTACT' | 'DELETE_CONTACT'
payload: Contact | Update;
}
Here, we have included DELETE_CONTACT as an additional type.
Open the ContactItem.tsx
file and, within the Delete icon, add an onClick attribute, an event handler that would be triggered when a user clicks on the delete button.
<AiFillDelete size={20} onClick={() => {
const confirmDelete = window.confirm(
`Are you sure you want to delete contact for user ${firstName} ${lastName}?`
);
if (confirmDelete) {
// dispatch action
}
}} color='red' className='icon' />
Here have added a function to display a confirmation dialog box asking the user if they want to delete the contact for the user whose first name and last name are stored in the firstName
and lastName
variables.
If the user confirms the delete action, the dispatch function is called to send an action to the store.
Your ContactItem.tsx
file should now look like this:
However, upon saving the file, a TypeScript error will appear, which is demonstrated below:
The reason for the error is that the Action interface in the contactsReducer.ts
file specifies that the payload can be either a Contact or Update, but for the DELETE_CONTACT action, we're only passing an object with an ID inside it and we're missing the 'updates' property.
To resolve this issue, you'll need to modify the Update interface in the following code:
export interface Update {
id: number;
updates: Contact;
}
to this code:
export interface Update {
id: number;
updates?: Contact;
}
In this instance, we've added a question mark symbol to the 'updates' property to make it optional. This resolves the TypeScript error, as we're providing the 'id' property and the 'updates' property can be disregarded.
By making this adjustment, the error in the ContactItem.tsx
file will disappear.
If you check the application now, you'll notice that the delete contact feature is functioning correctly
Implementing Validations in our form
At this point, the application's CRUD (Create, Read, Update, and Delete) functionality is complete.
However, we still need to implement validation for adding or modifying contacts.
To achieve this, begin by creating a new state within the ContactForm.tsx
file:
const [errorMsg, setErrorMsg] = useState('');
Here, we declare a state handle error messages
Modify the handleOnSubmit method in the ContactForm.tsx
file to the following code:
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const {firstName, lastName, phoneNumber, address, email} = contact;
if (
firstName.trim() === '' ||
lastName.trim() === '' ||
phoneNumber.trim() === ''||
address.trim() === ''||
email.trim() === ''
) {
setErrorMessage('All the fields are required.');
return;
} else if (phoneNumber.length < 3) {
setErrorMessage('Please enter a phone number with more than 3 numbers.');
return;
}
if (!dataToEdit) {
dispatch({
type: 'ADD_CONTACT',
payload: {
id: Date.now(),
...contact
}
});
setContact({
firstName: '',
lastName: '',
phoneNumber: '',
address: '',
email: '',
});
setErrorMessage('');
} else {
dispatch({
type: 'UPDATE_CONTACT',
payload: {
id: dataToEdit.id,
updates: {
id: Date.now(),
...contact
}
}
});
toggleModal();
}
};
In this handleSubmit
function, extracts the firstName, lastName, and phoneNumber, address, email properties from the contact state.
We then checks if these properties are empty or contain only whitespace, and if so, we set an error message with setErrorMessage and return from it.
If the phone number is not up to 3 numbers, it sets a different error message and returns from the function.
Now, we will display the error message on the screen. To do this, add the following code inside the Form tag and before the Form.Group tag while returning the JSX:
{errorMsg && <p className='errorMsg'>{errorMsg}</p>}
Your entire ContactForm.tsx files hould now look like this:
With these modifications, we have successfully completed building our Phone Book application using React + TypeScript.
The source-code for this project can be found here
Top comments (0)