# Agenda de Contactos en ReactJS con Redux y Firebase (Parte 2)

## Introducción

En la primer parte de este tutorial, creamos una agenda de contactos, donde el usuario puede añadir nuevos contactos, actualizarlos y borrarlos.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697576237611/ac42c127-436e-4536-a58d-59aeb5579d76.gif align="center")

Utilizamos Redux toolkit para la gestión del estado de la aplicación de manera organizada y eficaz. Ahora conectaremos la aplicación con Firebase para una persistencia de datos segura.

## Firebase

Ve a tu cuenta de Firebase y crea un nuevo proyecto, yo lo llamé "agenda-contactos".

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697579534648/ea00b2b5-0289-466e-a030-d980e4b13cee.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697579566423/1a161f70-0590-4ab2-a59a-8ba64ea8aea0.png align="center")

Como es un proyecto de ejemplo, puedes deshabilidar la opción de Google Analytics, luego haz click en "Crear proyecto".

Ve a "Cloud Firestore", y luego haz click en "Crear base de datos".

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697579795504/ba770e82-91b2-487d-bf3b-d2463f0d9f6a.png align="center")

Activa el modo "Prueba", luego te pedirá habilitar la ubicación donde se almacenará los datos de Cloud Firestore. Dale habilitar.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697657310017/9c10cd1a-731d-46a5-94e3-bd6b0499d691.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697657336510/9245b512-9d5a-426a-8a00-9c7158dc1b31.png align="center")

Accederás a tu panel de control. Ahora hay que registrar tu app para el proyecto. Haz clic en el ícono de configuración y elige "Configuración de proyecto".

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697657395761/aa20bf33-3246-4d88-965e-7cfe7e1a8ab6.png align="center")

Ve a la sección "Tus apps", aquí debemos registrar la app para nuestra web.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697659612053/aba46223-d940-4e61-9205-70d25dffcd43.jpeg align="center")

Una vez que registres el nombre de la app tendrás acceso a la configuración del SDK de firebase para tu proyecto.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697659659934/d09960d6-5224-4824-b6e9-4abc3e26355d.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697660370247/cc9af256-b224-40e4-b103-b0ff23cac56e.jpeg align="center")

Esta configuración es la que debemos pegar en el fichero de configuración de firebase, `src/firebase/firebase-config.js` . Primero instalamos el SDK de firebase con `npm i firebase` .

```javascript
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  authDomain: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  projectId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  storageBucket: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  messagingSenderId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  appId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
};

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

export { db };
```

Además de inicializar la app, accedemos a la base de datos de Cloud Firestore y lo guardamos en `const db` , luego lo exportamos. Ya tenemos todo listo para comenzar a trabajar con Cloud Firestore.

## createAsyncThunk

Es una función proporcionada por Redux Toolkit que simplifica la creación de acciones asincrónicas en una aplicación Redux. Esta función es muy útil cuando necesitas manejar operaciones asincrónicas, como solicitudes a una API, llamadas a bases de datos, o cualquier operación que tome tiempo para completarse.

Vamos a utilizar esta función para generar los "thunks" para cada una de las peticiones asíncronas entre nuestra aplicación y la base de datos en firestore.

Comencemos creando la función para añadir un nuevo contacto. Abre el fichero `src/features/contacts/contactsSlice.js` y realiza las siguientes modificaciones.

```javascript
import { createAsyncThunk, createEntityAdapter, createSlice } from "@reduxjs/toolkit"
import { addDoc, collection } from "firebase/firestore"
import { db } from "../../firebase/firebase-config"

export const addNewContact = createAsyncThunk("contacts/addNewContact", async (contact) => {
    const docRef = await addDoc(collection(db, "contacts"), contact)
    return {...contact, id: docRef.id }
})
```

Realizamos las siguientes importaciones: `createAsyncThunk` de Redux Toolkit, `addDoc` y `collection` de `firebase/firestore` y la instancia de la conexión de la base de datos `db` .

Creamos el "thunk" `addNewContact` con `createAsyncThunk` . La cadena es un nombre descriptivo de la acción, luego se define una función que recibe un objeto `contacto` .

Dentro de la función asincrónica, se utiliza `addDoc` para agregar un documento a la colección "contacts" en Firebase, pasando el objeto `contact` como datos del documento.

Finalmente, se devuelve un objeto que combina los datos del contacto y el ID del documento, que se utilizará para actualizar el estado de Redux con la información del nuevo contacto.

Ahora dentro de `contactsSlice` agrega lo siguiente:

```javascript
export const contactsSlice = createSlice({
    name: "contacts",
    initialState,
    reducers: {
        addContact: {
            reducer: contactsAdapter.addOne,
            prepare(firstname, lastname, phone, email){
                return {
                    payload: {
                        id: Date.now(),
                        firstname,
                        lastname,
                        phone,
                        email,
                    }
                }
            }
        },
        removeContact: contactsAdapter.removeOne,
        updateContact: contactsAdapter.updateOne,
    },
    extraReducers(builder){
        builder.addCase(addNewContact.fulfilled, (state, action) => {
            contactsAdapter.addOne(state, action.payload)
        })
    }
})
```

Este código configura cómo el estado de Redux debe actualizarse cuando la acción asincrónica `addNewContact` se completa con éxito.

Utilizamos `contactsAdapter.addOne` para agregar un nuevo contacto a la lista de contactos en el estado de Redux, utilizando los datos proporcionados en `action.payload`.

Para utilizar el "thunk", simplemente reemplaza el action `addContact`, por `addNewContact`.

Abre el fichero `src/components/NewContactForm.jsx` haz las siguientes modificaciones.

```javascript
import { useRef } from "react"
import { useDispatch } from "react-redux"
import { addNewContact } from "../features/contacts/contactsSlice"

export const NewContactForm = () => {
    const dispatch = useDispatch()

    const inputFirstname = useRef(null)
    const inputLastname = useRef(null)
    const inputPhone = useRef(null)
    const inputEmail = useRef(null)

    const handleSave = () => {
        const firstname = inputFirstname.current.value
        const lastname = inputLastname.current.value
        const phone = inputPhone.current.value
        const email = inputEmail.current.value

        //Reemplazamos el action addContact por addNewContact
        dispatch(addNewContact({firstname, lastname, phone, email}))

        inputFirstname.current.value = ""
        inputLastname.current.value = ""
        inputPhone.current.value = ""
        inputEmail.current.value = ""
    }

    return (
        <>
            <form action="">
                <div className="row mb-3">
                    <div className="col">
                        <input type="text"
                                name="firstname"
                                className="form-control"
                                placeholder="First name"
                                aria-label="First name"
                                ref={inputFirstname}
                        />
                    </div>
                    <div className="col">
                        <input type="text"
                                name="lastname"
                                className="form-control"
                                placeholder="Last name"
                                aria-label="Last name"
                                ref={inputLastname}
                        />
                    </div>
                </div>
                <div className="row mb-3">
                    <div className="col">
                        <input type="text"
                                name="phone"
                                className="form-control"
                                placeholder="Phone number"
                                aria-label="Phone number"
                                ref={inputPhone}
                        />
                    </div>
                    <div className="col">
                        <input type="text"
                                name="email"
                                className="form-control"
                                placeholder="E-mail"
                                aria-label="E-mail"
                                ref={inputEmail}
                        />
                    </div>
                </div>
                <div className="row">
                    <div className="col text-end">
                        <button type="button" className="btn btn-primary" onClick={handleSave}>GUARDAR</button>
                    </div>
                </div>
            </form>
        </>
    )
}
```

Ten en cuenta que ahora la función `addNewContact` recibe un objeto `contact`, por eso pasamos los parámetros entre llaves.

Con esto ya deberías ser capaz de añadir un nuevo contacto a la base de datos Firebase y ver el store de la aplicación actualizado.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697749897956/de91603f-5e60-4873-b06a-5e1f3b312403.gif align="center")

Sin embargo, tenemos un problema, cuando actualizamos la página, la tabla de contactos aparece vacía. Esto es por que debemos realizar un petición asíncrona para obtener todos los contactos de la base de datos al iniciar la aplicación. Vamos a resolver esto a continuación.

## Obtener todos los contactos

Para recuperar todos los datos debemos crear un nuevo "thunk". Justo debajo de `addNewContact` añade el siguiente código.

```javascript
export const getContacts = createAsyncThunk("contacts/getContacts", async () => {
    const contactsSnapshot = await getDocs(collection(db, "contacts"))
    return contactsSnapshot.docs.map((doc) => {
        return {
            id: doc.id,
          ...doc.data()
        }
    })
})
```

Este código define la acción asincrónica en Redux llamada "contacts/getContacts" que se utiliza para recuperar la lista de contactos desde nuestra base de datos. La función asincrónica se encarga de realizar la consulta y formatear los datos antes de devolverlos como resultado.

Ahora debemos integralo a los `extraReducers` como sigue:

```javascript
extraReducers(builder){
        builder.addCase(addNewContact.fulfilled, (state, action) => {
            contactsAdapter.addOne(state, action.payload)
        }),
        builder.addCase(getContacts.fulfilled, (state, action) => {
            contactsAdapter.setAll(state, action.payload)
        })
    }
```

Este código configura cómo el estado de Redux debe actualizarse cuando la acción asincrónica `getContacts` se completa con éxito. Utilizamos `contactsAdapter.setAll` para actualizar la lista de contactos en el estado con los datos proporcionados en `action.payload`.

Para utilizar este "thunk" abrimos el fichero `src/App.jsx` y lo incorporamos como sigue:

```javascript
import { useEffect } from "react"
import { ContactList } from "./components/ContactList"
import { NewContactForm } from "./components/NewContactForm"
import { getContacts } from "./features/contacts/contactsSlice"
import { useDispatch } from "react-redux"

function App() {
  const dispatch = useDispatch()

  useEffect(() => {
    dispatch(getContacts())
  })

  return (
    <>
      <main className='container-fluid'>
        <div className='row'>
          <div className='col-12 col-lg-8 mx-auto'>
            <h1 className='display-2 text-center'>AGENDA</h1>
            <hr />
          </div>
        </div>
        <div className='row'>
          <div className='col-12 col-lg-8 mx-auto'>
            <h2 className="display-6 text-center">Nuevo contacto</h2>
            <NewContactForm />
            <hr />
          </div>
        </div>
        <div className='row'>
          <div className='col-12 col-xl-8 mx-auto'>
            <h2 className="display-6 text-center">Contactos</h2>
            <ContactList />
            <hr />
          </div>
        </div>
      </main>
    </>
  )
}

export default App
```

Cuando el componente se monta, se llama a la acción asincrónica `getContacts` utilizando `dispatch`. Esto inicia el proceso de carga de la lista de contactos desde la base de datos y actualiza el estado global de la aplicación.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697752022549/1d466ed8-c4d5-47b9-bb99-51bc395bfa48.gif align="center")

Demora un poco en cargar los datos, pero con esto ya tienes sincronizado el store y la base de datos. A continuación seguimos con la acción de borrar un contacto.

## Borrar contacto

Creamos un nuevo "thunk" para eliminar un contacto, justo debajo de `getContacts` agrega el siguiente código:

```javascript
export const deleteContact = createAsyncThunk("contacts/deleteContact", async (id) => {
    await deleteDoc(doc(db, "contacts", id))
    return id
})
```

La función asincrónica se encarga de realizar la operación de eliminación en la base de datos y devuelve el `id` del contacto eliminado como resultado.

`await deleteDoc(doc(db, "contacts", id))` se utiliza para eliminar un documento específico de la colección "contacts" en la base de datos.

Ahora debemos integralo a los `extraReducers` como sigue:

```javascript
extraReducers(builder){
        builder.addCase(addNewContact.fulfilled, (state, action) => {
            contactsAdapter.addOne(state, action.payload)
        }),
        builder.addCase(getContacts.fulfilled, (state, action) => {
            contactsAdapter.setAll(state, action.payload)
        }),
        builder.addCase(deleteContact.fulfilled, (state, action) => {
            contactsAdapter.removeOne(state, action.payload)
        })
    }
```

Este código configura cómo el estado de Redux debe actualizarse cuando la acción asincrónica `deleteContact` se completa con éxito.

Utiliza `contactsAdapter.removeOne` para eliminar un contacto específico del estado en función del ID proporcionado en `action.payload`.

Ahora para utilizar el "thunk" abre el fichero `src/components/Contact.jsx` y reemplaza el action `removeContact` por `deleteContact`.

```javascript
import React, { useState } from "react"
import { deleteContact, updateContact } from "../features/contacts/contactsSlice"
import { useDispatch } from "react-redux"

export const Contact = ({ data }) => {
  const [edit, setEdit] = useState(false)
  const dispatch = useDispatch();

  const [contact, setContact] = useState({
    id: data.id,
    firstname: data.firstname,
    lastname: data.lastname,
    email: data.email,
    phone: data.phone
  });

  const toggleEdit = () => {
    setEdit(!edit);
  };

  let viewMode = {}
  let editMode = {}
  
  if(edit){
    viewMode.display = "none";
  } else {
    editMode.display = "none";
  }

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setContact({ ...contact, [name]: value });
  };

  const handleSave = () => {
    dispatch(updateContact({ id:contact.id, changes: contact}))
    toggleEdit();
  }

  const handleRemove = () => {
    //reemplazamos removeContact por deleteContact
    dispatch(deleteContact(contact.id));
  }

  return (
      <>
        <tr style={viewMode}>
          <th scope="row">{contact.id}</th>
          <td>{contact.firstname}</td>
          <td>{contact.lastname}</td>
          <td>{contact.phone}</td>
          <td>{contact.email}</td>
          <td>
            <button className="btn btn-sm btn-success" onClick={toggleEdit}>
              EDITAR
            </button>
            <button className="btn btn-sm btn-danger" onClick={handleRemove}>BORRAR</button>
          </td>
        </tr>

        <tr style={editMode}>
          <th scope="row">{contact.id}</th>
          <td>
            <input
              type="text"
              name="firstname"
              className="form-control form-control-sm"
              value={contact.firstname}
              onChange={handleInputChange}
            />
          </td>
          <td>
            <input
              type="text"
              name="lastname"
              className="form-control form-control-sm"
              value={contact.lastname}
              onChange={handleInputChange}
            />
          </td>
          <td>
            <input
              type="text"
              name="email"
              className="form-control form-control-sm"
              value={contact.email}
              onChange={handleInputChange}
            />
          </td>
          <td>
            <input
              type="text"
              name="phone"
              className="form-control form-control-sm"
              value={contact.phone}
              onChange={handleInputChange}
            />
          </td>
          <td>
            <button className="btn btn-sm btn-danger" onClick={toggleEdit}>
              CANCELAR
            </button>
            <button className="btn btn-sm btn-primary" onClick={handleSave}>
              GUARDAR
            </button>
          </td>
        </tr>
      </>
  );
};
```

Ahora ya podemos eliminar contactos de la base de datos y actualizar el store de la aplicación.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697835729011/b558d7ba-bda2-4298-af33-5e341f0662a5.gif align="center")

Pasamos a crear el "thunk" para actualizar un contacto.

## Actualización de contactos

Creamos un nuevo "thunk" para actualizar un contacto, justo debajo de `deleteContact` agrega el siguiente código:

```javascript
export const editContact = createAsyncThunk("contacts/editContact", async (contact) => {
    await updateDoc(doc(db, "contacts", contact.id), contact.changes)
    return contact
})
```

La acción se llama "contacts/editContact" y se utiliza para editar un contacto en una base de datos. La función asincrónica toma un objeto `contact` como argumento, que contiene información sobre el contacto que se va a editar. `await updateDoc(doc(db, "contacts",` [`contact.id`](http://contact.id)`), contact.changes)` se utiliza para actualizar un documento específico en la colección "contacts" en la base de datos. Se actualiza el documento con los cambios especificados en `contact.changes`. Se retorna un objeto `contact`, que se utilizará para actualiza el estado de la aplicación.

Ahora debemos integralo a los `extraReducers` como sigue:

```javascript
extraReducers(builder){
        builder.addCase(addNewContact.fulfilled, (state, action) => {
            contactsAdapter.addOne(state, action.payload)
        }),
        builder.addCase(getContacts.fulfilled, (state, action) => {
            contactsAdapter.setAll(state, action.payload)
        }),
        builder.addCase(deleteContact.fulfilled, (state, action) => {
            contactsAdapter.removeOne(state, action.payload)
        }),
        builder.addCase(editContact.fulfilled, (state, action) => {
            contactsAdapter.updateOne(state, {
                id: action.payload.id,
                changes: action.payload.changes
            })
        })
    }
```

Este código configura cómo el estado de Redux debe actualizarse cuando la acción asincrónica `editContact` se completa con éxito. Utiliza `contactsAdapter.updateOne` para actualizar un contacto específico en el estado en función del `id` proporcionado en `action.payload`.

Ahora para utilizar el "thunk" abre el fichero `src/components/Contact.jsx` y reemplaza el action `updateContact` por `editContact`.

```javascript
import React, { useState } from "react"
import { deleteContact, editContact } from "../features/contacts/contactsSlice"
import { useDispatch } from "react-redux"

export const Contact = ({ data }) => {
  const [edit, setEdit] = useState(false)
  const dispatch = useDispatch();

  const [contact, setContact] = useState({
    id: data.id,
    firstname: data.firstname,
    lastname: data.lastname,
    email: data.email,
    phone: data.phone
  });

  const toggleEdit = () => {
    setEdit(!edit);
  };

  let viewMode = {}
  let editMode = {}
  
  if(edit){
    viewMode.display = "none";
  } else {
    editMode.display = "none";
  }

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setContact({ ...contact, [name]: value });
  };

  const handleSave = () => {
    //reemplazamos el action updateContact por editContact
    dispatch(editContact({ id: contact.id, changes: {
      firstname: contact.firstname,
      lastname: contact.lastname,
      email: contact.email,
      phone: contact.phone
    } }))
    toggleEdit();
  }

  const handleRemove = () => {
    dispatch(deleteContact(contact.id));
  }

  return (
      <>
        <tr style={viewMode}>
          <th scope="row">{contact.id}</th>
          <td>{contact.firstname}</td>
          <td>{contact.lastname}</td>
          <td>{contact.phone}</td>
          <td>{contact.email}</td>
          <td>
            <button className="btn btn-sm btn-success" onClick={toggleEdit}>
              EDITAR
            </button>
            <button className="btn btn-sm btn-danger" onClick={handleRemove}>BORRAR</button>
          </td>
        </tr>

        <tr style={editMode}>
          <th scope="row">{contact.id}</th>
          <td>
            <input
              type="text"
              name="firstname"
              className="form-control form-control-sm"
              value={contact.firstname}
              onChange={handleInputChange}
            />
          </td>
          <td>
            <input
              type="text"
              name="lastname"
              className="form-control form-control-sm"
              value={contact.lastname}
              onChange={handleInputChange}
            />
          </td>
          <td>
            <input
              type="text"
              name="email"
              className="form-control form-control-sm"
              value={contact.email}
              onChange={handleInputChange}
            />
          </td>
          <td>
            <input
              type="text"
              name="phone"
              className="form-control form-control-sm"
              value={contact.phone}
              onChange={handleInputChange}
            />
          </td>
          <td>
            <button className="btn btn-sm btn-danger" onClick={toggleEdit}>
              CANCELAR
            </button>
            <button className="btn btn-sm btn-primary" onClick={handleSave}>
              GUARDAR
            </button>
          </td>
        </tr>
      </>
  );
};
```

Con esto tenemos todo listo para poder editar un contacto.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697922635098/311caf98-a53c-4ab0-be2a-0625b928d0b8.gif align="center")

## Conclusión

Hemos explorado cómo gestionar el estado de la aplicación de manera eficiente con Redux Toolkit y cómo aprovechar Firebase para la persistencia de datos en tiempo real. A lo largo de estas lecciones, hemos aprendido a crear, actualizar y eliminar contactos, así como a recuperarlos de la base de datos. También hemos trabajado con acciones asincrónicas y hemos utilizado Redux Toolkit para simplificar nuestra gestión de estado.

Espero que hayas adquirido un conjunto valioso de habilidades que te permitirán abordar proyectos más grandes y emocionantes en el futuro. La combinación de Redux Toolkit y Firebase ofrece un potente conjunto de herramientas para el desarrollo de aplicaciones web modernas y escalables.

Puedes encontrar el proyecto completo en mi repositorio de GitHub haciendo clic [**AQUÍ**](https://github.com/AlegreCode/react-redux-firebase).

Espero que lo hayas encontrado entretenido, instructivo y claro. Si tienes alguna duda, puedes hacérmelo saber en los comentarios. Pronto estaré subiendo más tutoriales.

Nos vemos en la próxima. Saludos!👋😊
