React-Redux Example
React Redux Example
- In contact book example We used edit,delete and add operation for adding contact details.
- Create a new react project using create-react-app command. I choose the project name: "Contact Book."
C:\Users\Admin\OneDrive\Desktop\React\New folder\npx create-react-app Contact_book
- In ./src/app/contactSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "../app/store";
import Contact from "../model/Contact";
import { v4 as uuidv4 } from "uuid";
type initialStateType = {
contactList: Contact[];
};
const contactList: Contact[] = [
{
id: uuidv4(),
name: "Roger ndutiye",
email: "rogerndutiye@gmail.com",
telephone: "250788841494",
},
];
const initialState: initialStateType = {
contactList,
};
export const contactSlice = createSlice({
name: "contact",
initialState,
reducers: {
addContact: (state, action: PayloadAction<Contact>) => {
state.contactList.push(action.payload);
},
updateContact: (state, action: PayloadAction<Contact>) => {
const {
payload: { id, name, email, telephone },
} = action;
state.contactList = state.contactList.map((contact) =>
contact.id === id ? { ...contact, name, email, telephone } : contact
);
},
removeContact: (state, action: PayloadAction<{ id: string }>) => {
state.contactList = state.contactList.filter(
(contact) => contact.id !== action.payload.id
);
},
},
});
export const { addContact, updateContact, removeContact } =
contactSlice.actions;
export const getContactList = (state: RootState) => state.contact.contactList;
export default contactSlice.reducer;
- ./src/app/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
- ./src/app/store.ts
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import contactSlice from "../app/contactSlice";
export const store = configureStore({
reducer: {
contact: contactSlice,
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
- ./src/component/contactInfo.tsx
import * as React from "react";
import {
BsFillPenFill,
BsFillTrashFill,
BsMailbox2,
BsTelephoneFill,
BsFilePerson,
} from "react-icons/bs";
import Contact from "../model/Contact";
import { useAppDispatch } from "../app/hooks";
import { removeContact } from "../app/contactSlice";
interface ContactProps {
contact: Contact;
onContactUpdate: (id: string) => void;
}
const ContactInfo: React.FC<ContactProps> = (props) => {
const dispatch = useAppDispatch();
const contact = props.contact;
const setUpdatePage = (id: string) => {
props.onContactUpdate(id);
};
return (
<div className="bg-white-500">
<div className="flex flex-col pb-2 overflow-auto">
<div
className="relative flex flex-col items-start p-4 mt-3 bg-white rounded-lg cursor-pointer bg-opacity-90 group hover:bg-opacity-100"
draggable="true"
>
<button
className="absolute top-0 right-0 items-center justify-center hidden w-5 h-5 mt-3 mr-2 text-gray-500 rounded hover:bg-gray-200 hover:text-gray-700 group-hover:flex"
onClick={() => setUpdatePage(contact.id)}
>
<BsFillPenFill />
</button>
<button
className="absolute top-7 right-0 items-center justify-center hidden w-5 h-5 mt-3 mr-2 text-gray-500 rounded hover:bg-gray-200 hover:text-gray-700 group-hover:flex"
onClick={() => dispatch(removeContact({ id: contact.id }))}
>
<BsFillTrashFill />
</button>
<div className="rounded-md pl-6 text-sm font-medium text-gray-800">
<div className="flex items-center w-full mt-3 ">
<div className="flex items-center">
<BsFilePerson />
<span className="ml-1 leading-none">{contact.name}</span>
</div>
</div>
<div className="flex items-center w-full mt-3 text-xs font-medium text-gray-400">
<div className="flex items-center">
<BsTelephoneFill />
<span className="ml-1 leading-none">{contact.telephone}</span>
</div>
</div>
<div className="flex items-center w-full mt-3 text-xs font-medium text-gray-400">
<div className="flex items-center">
<BsMailbox2 />
<span className="ml-1 leading-none">{contact.email}</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default ContactInfo;
- ./src/component/contactIist.tsx
import { useState } from "react";
import ContactInfo from "../components/contactInfo";
import Contact from "../model/Contact";
import NewContact from "./NewContact";
import DialogBox from "./DialogBox";
interface ContactProps {
contacts: Contact[] | undefined;
}
const ContactList: React.FC<ContactProps> = ({ contacts }) => {
let [open, setOpen] = useState(false);
let [id, setID] = useState("");
const ContactUpdate = (id: string) => {
setID(id);
setOpen(true);
};
const DialogHandle = () => {
setOpen((current) => !current);
};
return (
// <div className="flex flex-col mx-20">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{contacts &&
contacts.map((contact) => (
<ContactInfo
key={contact.id}
contact={contact}
onContactUpdate={ContactUpdate}
/>
))}
{open && (
<DialogBox open={open} OnDialogHandle={DialogHandle}>
<NewContact id={id} />
</DialogBox>
)}
</div>
// </div>
);
};
export default ContactList;
- ./src/component/DialogBox.tsx
import { Fragment, useRef, useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { BsFillBookFill } from "react-icons/bs";
interface DialogBoxProps {
open: boolean;
OnDialogHandle: () => void;
}
const DialogBox: React.FC<DialogBoxProps> = (props) => {
let [isOpen, setIsOpen] = useState(props.open);
const closeBackHandle = () => {
setIsOpen(false);
props.OnDialogHandle();
};
const cancelButtonRef = useRef(null);
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog
as="div"
className="fixed z-10 inset-0 overflow-y-auto"
initialFocus={cancelButtonRef}
onClose={() => closeBackHandle()}
>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<span
className="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>
​
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-indigo-100 sm:mx-0 sm:h-10 sm:w-10">
<BsFillBookFill
className="h-6 w-6 text-indigo-600"
aria-hidden="true"
/>
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg leading-6 font-medium text-gray-900"
>
contact
</Dialog.Title>
<div className="mt-4 border-t-0 border-indigo-500">
{props.children}
</div>
</div>
</div>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
};
export default DialogBox;
- ./src/component/NewContact.tsx
import { useHistory } from "react-router-dom";
import { useAppDispatch, useAppSelector } from "../app/hooks";
import { addContact, updateContact } from "../app/contactSlice";
import { v4 as uuidv4 } from "uuid";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { BsFillCursorFill } from "react-icons/bs";
type ContactFormData = {
name: string;
email: string;
telephone: string;
};
const schema = yup
.object({
name: yup.string().required(),
email: yup.string().email().required(),
telephone: yup.string().required(),
})
.required();
interface NewContactProps {
id: string;
}
const NewContact: React.FC<NewContactProps> = ({ id }) => {
const {
register,
handleSubmit,
formState: { errors },
setValue,
} = useForm<ContactFormData>({
resolver: yupResolver(schema),
});
console.log(id);
const history = useHistory();
const dispatch = useAppDispatch();
const contactData = useAppSelector((state) =>
state.contact.contactList.find((contact) => contact.id === id)
);
setValue("name", contactData?.name || "");
setValue("email", contactData?.email || "");
setValue("telephone", contactData?.telephone || "");
const onSubmit = (data: ContactFormData) => {
const { name, email, telephone } = data;
if (id) {
editContact(name, email, telephone);
return;
}
dispatch(addContact({ name, email, telephone, id: uuidv4() }));
history.push("/");
};
const editContact = (name: string, email: string, telephone: string) => {
dispatch(updateContact({ name, email, telephone, id }));
history.push("/");
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="bg-white p-4 px-4 text-sm ">
<div className="md:col-span-5">
<label htmlFor="full_name" className="text-left">
Full Name
</label>
<input
type="text"
className="h-10 border mt-1 rounded px-4 w-full bg-gray-50"
placeholder="Full Name"
{...register("name")}
/>
<p className="mt-2 text-sm text-red-600">{errors.name?.message}</p>
</div>
<div className="mt-3">
<label htmlFor="email">Email</label>
<input
type="text"
className="h-10 border mt-1 rounded px-4 w-full bg-gray-50"
placeholder="email@domain.com"
{...register("email")}
/>
<p className="mt-2 text-sm text-red-600">{errors.email?.message}</p>
</div>
<div className="mt-3">
<label htmlFor="email">PhoneNumber</label>
<input
type="text"
className="h-10 border mt-1 rounded px-4 w-full bg-gray-50"
placeholder="250 788 841 494"
{...register("telephone")}
/>
<p className="mt-2 text-sm text-red-600">
{errors.telephone?.message}
</p>
</div>
<div className="mt-3 text-right">
<div className="inline-flex items-end">
<button
type="submit"
className="flex items-center bg-indigo-600 text-white hover:bg-purple-500 p-2 rounded text-sm w-auto"
// onClick={onSubmitHandle}
>
<BsFillCursorFill />
<span> Submit</span>
</button>
</div>
</div>
</div>
</form>
);
};
export default NewContact;
- ./src/model/contact.ts
class Contact {
id: string;
name: string;
email: string;
telephone: string;
constructor(name: string, email: string, telephone: string, id: string) {
this.name = name;
this.email = email;
this.telephone = telephone;
this.id = id;
}
}
export default Contact;
- ./src/pages/Homepage.tsx
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { BsBuilding } from "react-icons/bs";
import { useAppSelector } from "../app/hooks";
import Contact from "../model/Contact";
import ContactList from "../components/contactList";
import NewContact from "../components/NewContact";
import DialogBox from "../components/DialogBox";
import { BsPlusCircleFill } from "react-icons/bs";
const HomePage: React.FC = (props) => {
let [open, setOpen] = useState(false);
const getContactList = useAppSelector((state) => state.contact.contactList);
const [searchTerm, setSearchTerm] = useState("");
const [contactListData, setContactListData] = useState<Contact[]>();
useEffect(() => {
setContactListData(getContactList);
const filteredData = getContactList.filter((item) =>
item.name.toLowerCase().includes(searchTerm)
);
setContactListData(filteredData);
}, [getContactList, searchTerm]);
const DialogHandle = () => {
setOpen((current) => !current);
};
return (
<div className="flex flex-col w-screen h-screen overflow-auto text-gray-700 bg-gradient-to-tr from-green-200 via-indigo-200 to-pink-200">
<div className="flex items-center flex-shrink-0 w-full h-16 px-10 bg-white bg-opacity-75">
<Link to="/">
<BsBuilding className="w-8 h-8 text-indigo-600 stroke-current" />{" "}
</Link>
<input
className="flex items-center h-10 px-4 ml-10 text-sm w-1/3 bg-gray-200 rounded-full focus:outline-none focus:ring"
type="search"
placeholder="Search contact …"
onChange={(e) => setSearchTerm(e.target.value.toLowerCase())}
/>
<button
type="submit"
className="flex items-center text-indigo-600 p-2 rounded text-sm w-auto"
onClick={DialogHandle}
>
<BsPlusCircleFill />
<span> Add</span>
</button>
<button className="flex items-center justify-center w-8 h-8 ml-auto overflow-hidden rounded-full cursor-pointer">
{/* <img
src="https://i.pinimg.com/280x280_RS/ab/a2/8e/aba28eb29f66aab5f24db128a0232f3f.jpg"
alt=""
></img> */}
</button>
</div>
<ContactList contacts={contactListData} />
{open && (
<DialogBox open={open} OnDialogHandle={DialogHandle}>
<NewContact id={""} />
</DialogBox>
)}
</div>
);
};
export default HomePage;
- In App.css
.group:hover .group-hover\:flex {
display: flex !important;
}
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-float infinite 3s ease-in-out;
}
}
.App-header {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
}
.App-link {
color: rgb(112, 76, 182);
}
@keyframes App-logo-float {
0% {
transform: translateY(0);
}
50% {
transform: translateY(10px);
}
100% {
transform: translateY(0px);
}
}
- In App.Js
import React from "react";
import { Switch, Redirect, Route } from "react-router-dom";
import HomePage from "./Pages/HomePage";
import "./App.css";
function App() {
return (
<Switch>
<Route path="/" exact>
<Redirect to="/contacts" />
</Route>
<Route path="/contacts" exact component={HomePage} />
<Route path="*" component={HomePage} />
</Switch>
);
}
export default App;
- In index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { store } from "./app/store";
import { Provider } from "react-redux";
import App from "./App";
import "./index.css";
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
- Run the command in terminal
npm start
Output:
- Home Page
- Add Contact page
- Edit Page