Se ve que ya conoces el término y de que se trata, pero no podes conectarlo con un use-case, así que voy a intentar proveer uno.
Imaginate que nos piden hacer una aplicación para una biblioteca: El personal necesita saber que libros hay disponibles en todo momento, los usuarios pueden retirar libros o devolver libros. Además, necesitamos mostrar la cantidad de libros actual en otra parte de la aplicación.
Se vería algo así:
Cuando nosotros agregamos o retiramos un libro, tanto el total, como la lista deberían actualizarse:
Como primer paso, vamos a hacer una función en la cual modifique la cantidad de libros en nuestra biblioteca y esto va a ocurrir con dos tipos de acciones, devolver
y retirar
libros. Nuestros libros tienen un título y un número único isbn
, que esta información va a venir dentro de nuestra acción como payload
// En un principio nuestra biblioteca va a tener 3 libros
// y dependiendo de nuestra acción, es qué vamos a hacer (agregar o quitar libros).
// Identificamos que libro sacar o agregar por medio de nuestro payload
let books = [
{
title: "The Fellowship of the Ring",
isbn: 1
},
{
title: "The End of Eternity",
isbn: 2
},
{
title: "Guards! Guards!",
isbn: 3
}
]
const library = (action = {}) => {
switch (action.type) {
case 'ADD_BOOK':
return books.push(action.payload);
case 'REMOVE_BOOK':
return books = books.filter((b) => b.isbn !== action.payload.isbn);
default:
return books;
}
}
Si probamos nuestra función library
, podemos ver que agrega y quita libros correctamente. Si no pasamos ninguna acción, va a devolver los libros actuales.
Pero de esta forma no podemos mantener los datos sincronizados, va a depender de cuando se lee el valor de books
.
Para poder lograr compartir los datos y mantener sincronizada la aplicación, necesitamos almacenar nuestros libros en un almacenamiento general y este a su vez, necesita informar al exterior que han ocurrido cambios en los datos.
Vamos a crear primero la función que va a manipular los datos de nuestra biblioteca:
// nuestro store va a recibir como parámetro una función que es la que va a modificar los datos
const createStore = updater => {
// inicializamos nuestro almacenamiento vacío
let store;
// y necesitamos saber quienes están esperando los cambios
const listeners = [];
// Creamos un método para obtener el estado en el momento actual
const getState = () => store
// La única forma en que se van a actualizar los datos, es mediante este método.
// Se encarga de llamar al updater y guardar los valores actualizados en store.
// Debemos pasar que acción queremos realizar, esta va a contener tipo y payload
const dispatch = action => {
// pasamos nuestro store actual al updater y la acción que se quiere aplicar
store = updater(store, action);
// llamamos a todos los listeners para que actualicen sus valores
listeners.forEach((listener) => listener());
}
// necesitamos este método para poder saber y almacenar las funciones que hay que
// llamar cuando se actualicen los datos
const subscribe = listener => {
listeners.push(listener);
}
// antes de exponer todos nuestros métodos, necesitamos popular información en
// nuestro store, así que llamamos a dispatch con una acción vacía:
dispatch({});
// Hacemos uso de Closures
return { getState, dispatch, subscribe };
}
Ahora deberíamos modificar nuestra función library
, para que en vez de modificar una variable, devuelva un nuevo store
, usando como referencia el que se le pasa como argumento (proveniente de createStore):
// el primer argumento viene de nuestro Store, sería el `state` actual de nuestros libros
// la primera vez que se llama, el argumento books va a estar vacío, por lo cual,
// inicalizamos con los libros definidos en `books`, renombrado a `initialBooks`
const library = (books = initialBooks, action) => {
switch (action.type) {
case "RETURN_BOOK":
return [...books, action.payload];
case "BORROW_BOOK":
return books.filter(b => b.isbn !== action.payload.isbn);
default:
return books;
}
};
Es hora de poner todas las cosas a funcionar al mismo tiempo, vamos a crear nuestra biblioteca:
// MyLibrary contiene getState, para obtener los libros actuales
// dispatch: para enviar nuevas acciones y actualizar los datos
// subscribe: para poder escuchar estos cambios y mostrarlos en pantalla
const MyLibrary = createStore(library);
Vamos a crear los métodos para poder mostrar en pantalla los datos.
Voy a crear renderBooks
para mostrar la lista de libros y renderTotal
para mostrar el total. Voy a remarcar lo importante con comentarios:
const renderBooks = () => {
// Obtenemos los libros del estado de biblioteca
const books = MyLibrary.getState();
const bookList = document.querySelector(".book-list");
bookList.innerHTML = "";
// y usamos sus valores para mostrar una lista de libros
books.forEach(book => {
const bookItem = document.createElement("li");
bookItem.className = "list-group-item";
bookItem.innerHTML = `Titulo: ${book.title} <br/> ISBN: ${book.isbn}`;
bookList.appendChild(bookItem);
});
};
const renderTotal = () => {
// obtengo los libros de la biblioteca
const books = MyLibrary.getState();
const total = document.querySelector(".total");
// y muestro el total en pantalla
total.innerHTML = books.length;
};
Subscribimos a los cambios a ambas funciones (las guardamos como listeners) y las llamamos para popular los valores iniciales en pantalla:
MyLibrary.subscribe(renderBooks);
MyLibrary.subscribe(renderTotal);
renderTotal();
renderBooks();
Lo único que queda ahora es obtener los datos del formulario y enviarlos utilizando a dispatch
, junto con nuestras acciones:
const returnBookButton = document.querySelector(".return-book");
returnBookButton.onclick = e => {
e.preventDefault();
const title = document.querySelector(".title").value;
const isbn = document.querySelector(".isbn").value;
if (!title || !isbn) {
return;
}
// envío la acción devolver libro cuando hago click en el botón correspondiente
// paso a la acción el título e isbn ingresado para agregarlos a la lista
MyLibrary.dispatch({ type: "RETURN_BOOK", payload: { title, isbn } });
// Recordemos que una vez actualizados estos datos en el store,
// cada listener se va a llamar para mostrar los datos nuevos
title.value = "";
isbn.value = "";
};
const borrowBookButton = document.querySelector(".borrow-book");
borrowBookButton.onclick = e => {
e.preventDefault();
const isbn = document.querySelector(".isbn").value;
if (!isbn) {
return;
}
// envío la acción retirar libro cuando hago click en el botón correspondiente
// paso a la acción el isbn ingresado para sacarlo de la lista
MyLibrary.dispatch({
type: "BORROW_BOOK",
payload: { isbn: parseInt(isbn, 10) }
});
isbn.value = "";
};
Ahora si queremos mostrar los libros en cualquier parte de la aplicación, solamente tenemos que suscribir un método a nuestro store
y toda la aplicación se va a actualizar, porque hay un solo source of truth.
Acá está el Codesandbox de esta mini aplicación, hay un par de cositas que agregué extras.
Este patrón es el que usa trás bambalinas la librería Redux.
Espero que este use-case haya servido de algo!