Skip to content

Latest commit

 

History

History
516 lines (409 loc) · 20.4 KB

Redux.md

File metadata and controls

516 lines (409 loc) · 20.4 KB

Redux

Redux es una herramienta de gestión de estados framework agnostic (es decir, que se puede usar con diferentes frameworks), la cual nos ayuda a manejar mejor los estados, evitando los problemas a futuro junto al debugging.
Para esta guía crearemos un proyecto básico en el que podremos agregar películas a una lista de películas favoritas y de películas vistas.

Guía de Temas

  1. Instalación
  2. Configuración de entorno
  3. Código

Instalación

Para este caso usaremos Redux con React, para ello comenzaremos creando un nuevo proyecto de React, ya sea con create-react-app o con vite, luego de ello abriremos nuestro proyecto en el editor de código.

Para este proyecto vamos a usar Tailwind

Luego de esto, empezaremos con la instalación de Redux Toolkit, el cual nos ayudará a configurar Redux en nuestro proyecto. Para ello abriremos la consola y usaremos el comando que nos proporciona la documentación.

npm i @reduxjs/toolkit react-redux

Sumado a esto podemos instalar una extensión que nos ayudará con el debugging de Redux, la cual está disponible para Chrome, Firefox y otros navegadores.

Store

Para empezar a usar Redux es necesario crear nuestro store, que será donde guardaremos todos nuestros estados. Para ello debemos crear una carpeta dentro de src llamada app y dentro de ella un archivo llamado store.js, y dentro del mismo crearemos nuestro store con el siguiente código.

import { configureStore } from '@reduxjs/toolkit'       // Importamos lo necesario de Redux

const store = configureStore({          // Creamos nuestra store
    reducer: {},
})

export default store;       // La exportamos

Con nuestra store creada debemos indicarle a nuestra app cual será el store que usaremos, para ello iremos a nuestra main.jsx (o index.js en caso de haber usado create-react-app) y allí usaremos el Provider que nos facilita Redux de la siguiente manera.

import "./index.scss";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "react-redux";         // Importamos el provider de Redux
import store from "./app/store";            // Importamos la store que creamos anteriormente

ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <Provider store={store}>        {/* Usamos el provider para envolver nuestra app y le pasamos la store como prop */}
            <App />
        </Provider>
    </React.StrictMode>
);

Slice

Luego de crear nuestro store será necesario crear nuestro slice, es decir, nuestro modificador de estados. Para ello crearemos una carpeta features dentro de la carpeta src, dentro de la misma crearemos la carpeta movies y por ultimo el archivo favMovieListSlice.js en la que tendremos nuestro slice.

Una forma fácil de hacer esto sería escribir features/movies/favMovieListSlice.js dentro de src

Hecho esto agregamos el código en nuestro archivo

import { createSlice } from "@reduxjs/toolkit";         // Importamos lo necesario para crear el slice

const initialState = [{             // Creamos el estado inicial
    id: "hq",
    name: 'scarface',
    desc: 'Tells the story of Cuban refugee Tony Montana (Al Pacino), who arrives penniless in Miami during the Mariel boatlift and becomes a powerful and extremely homicidal drug lord.'
}];                            

export const favMovieListSlice = createSlice({      // Exportamos el slice que crearemos
    name: 'favMovieList',                           // Le asignamos un nombre
    initialState,                               // Le pasamos el estado inicial
    reducers:{}                                     // Y los reducers que crearemos luego
})

export default favMovieListSlice.reducer;           // Exportamos los reducers

Y por ultimo importamos este slice en nuestro store de la siguiente manera

import { configureStore } from '@reduxjs/toolkit'
import favMovieListSlicer from '../features/movies/favMovieListSlice';      // Importamos el slice

const store = configureStore({
    reducer:{
        favMovieList: favMovieListSlicer            // Y lo asignamos con el nombre que queremos
    }
})

export default store;

Con esto hecho tendremos la configuración de nuestro store completo.

Uso del estado

Con nuestro estado configurado podemos empezar a utilizarlo. Para ello lo llamaremos en nuestro App.jsx de la siguiente manera.

import "./App.scss";
import { useSelector } from "react-redux";      // Importamos la función que llama al estado

function App() {
    const favMovieListArray = useSelector((state) => state.favMovieList);       // Usamos la función especificando el estado y lo guardamos en una constante
        
    console.log(favMovieListArray);         // Imprimimos la constante en la consola para comprobar que funciona
        
    return (
        <div>
            <h1 className="text-3xl font-bold underline">Hello world!</h1>
        </div>
    );
}

export default App;

Agregar datos

Ya podemos llamar los datos, ahora es momento de agregar datos, para ello crearemos la función necesaria en nuestro favMovieListSlice.js como un reducer de la siguiente manera.

import {
    createSlice
} from "@reduxjs/toolkit";

const initialState = [{
    id: "hq"
    name: 'scarface',
    desc: 'Tells the story of Cuban refugee Tony Montana (Al Pacino), who arrives penniless in Miami during the Mariel boatlift and becomes a powerful and extremely homicidal drug lord.'
}];

export const favMovieListSlice = createSlice({
    name: 'favMovieList',
    initialState,
    reducers: {
        addMovie: (state, action) => {          // Le asignamos un nombre al reducer
            state.push(action.payload)          // Y usamos push para mandarlos al array
        }
    }
})

export const {
    addMovie
} = favMovieListSlice.actions                   // Exportamos la acción para usarla en otro lado
export default favMovieListSlice.reducer;

Para crear el id de cada uno de nuestros datos de momento utilizaremos uuid, el cual instalaremos de la siguiente manera.

npm i uuid

Luego de esto podemos crear el resto de las funciones para generar la película.

import "./App.scss";
import $ from "jquery";                                      // Utilizamos jQuery para agilizar la explicación de js
import { useSelector, useDispatch } from "react-redux";         // Importamos el dispatch para llamar a la acción
import { addMovie } from "./features/movies/favMovieListSlice";     // Importamos la acción
import { v4 as uuid } from "uuid";                                  // Importamos uuid

function App() {
    const favMovieListArray = useSelector((state) => state.favMovieList);

    const dispatch = useDispatch();             // Llamamos al useDispatch y la guardamos en una constante

    const handleSubmit = (e) => {           // Creamos un handleSubmit para obtener y guardar los datos
        e.preventDefault();
        dispatch(                           // Utilizamos el dispatch y le pasamos el objeto como parámetro para guardar
            addMovie({ 
                id: uuid(),
                name: $("#movieName").val(),
                desc: $("#movieDesc").val() 
            })
        );
    };

    return (
        <div className="bg-gray-900 text-white min-h-screen h-fit text-center flex flex-col gap-8 p-4">
            <form
                className="flex flex-col w-1/4 mx-auto text-black gap-4"
                onSubmit={handleSubmit}
            >                                           {/* Creamos el form para guardar los datos */}
                <input
                  className="rounded p-2"
                  type="text"
                  placeholder="movie name"
                  id="movieName"
                  required
                />
                <textarea
                  className="rounded p-2"
                  name="desc"
                  id="movieDesc"
                  cols="30"
                  rows="5"
                  placeholder="description"
                ></textarea>
                <button
                  className="py-3 px-6 text-white bg-purple-600 rounded w-1/2 mx-auto border-2 border-black transition-all   hover:scale-105"
                  type="submit"
                >
                SAVE
                </button>
            </form>
            <div>                                                           {/* Creamos el div que contendrá nuestra lista */}
                <h1 className="text-2xl uppercase underline font-bold pb-4">
                fav movies
                </h1>
                <div className="flex justify-center items-center flex-wrap">
                    {favMovieListArray.map((movie, i) => (
                        <div
                          key={i}
                          className="w-1/4 bg-gray-500 flex flex-col justify-center items-center p-4 gap-6 border-2 border-black rounded self-stretch"
                        >
                            <h3 className="font-bold uppercase underline">{movie.name}</h3>
                            <p className="h-full flex items-center">{movie.desc}</p>
                            <button className="py-2 px-4 bg-red-600 rounded min-w-fit w-1/2 mx-auto border-2 border-black transition-all hover:scale-105">
                                DELETE
                            </button>
                        </div>
                    ))}
                </div>
            </div>
        </div>
    );
}

export default App;

Con esto hecho podemos agregar películas y ver como se actualiza en el momento.

Eliminar datos

Ya tenemos los datos creados y guardados, pero va a ser necesario poder eliminar los datos del array, para esto crearemos otro reducer de la siguiente manera.

[ ... ]             // Resto del código

export const favMovieListSlice = createSlice({

    name: 'favMovieList',

    initialState,

    reducers: {
        addMovie: (state, action) => {
            state.push(action.payload)
        },

        deleteMovie: (state, action) => {           // Le asignamos el nombre
            const movieInList = state.find(movie => movie.id === action.payload)        // Usamos ".find()" para buscar el id en el array
            state.splice(state.indexOf(movieInList), 1)         // Y buscamos el index para poder usar ".splice()"
        }
    }
})

export const {
    addMovie,
    deleteMovie,                // Lo importamos para usarlo luego
} = favMovieListSlice.actions
    
export default favMovieListSlice.reducer;

Ahora podemos llamarlo en nuestra App.jsx y asignarlo al botón correspondiente.

import "./App.scss";
import $ from "jquery";
import { useSelector, useDispatch } from "react-redux";
import { addMovie, deleteMovie } from "./features/movies/favMovieListSlice";        // Importamos el delete
import { v4 as uuid } from "uuid";

function App() {
    [ ... ]

    const handleDelete = (id) => {
        dispatch(deleteMovie(id));        // Creamos el dispatch y le pasamos el id
    }

    return (
        <div className="bg-gray-900 text-white min-h-screen h-fit text-center flex flex-col gap-8 p-4">
            [ ... ]
            <div className="flex justify-center items-center flex-wrap">
                {favMovieListArray.map((movie, i) => (
                    <div
                     key={i}
                     className="w-1/4 bg-gray-500 flex flex-col justify-center items-center p-4 gap-6 border-2 border-black  rounded self-stretch"
                        >
                        <h3 className="font-bold uppercase underline">{movie.name}</h3>
                        <p className="h-full flex items-center">{movie.desc}</p>
                        <button onClick={()=> handleDelete(movie.id)}             // Le pasamos el id de la película
                                className="py-2 px-4 bg-red-600 rounded min-w-fit w-1/2 mx-auto border-2 border-black transition-all hover:scale-105">
                            DELETE
                        </button>
                    </div>
                ))}
            </div>
        </div>
    );
}

export default App;

Editar datos

El proximo paso para completar nuestro CRUD con Redux será la posibilidad de editar nuestras películas, más allá de que luego no lo usemos en el producto final. Para ello crearemos nuestro reducer llamado updateMovie de la siguiente manera.

[ ... ]

export const favMovieListSlice = createSlice({

    name: 'favMovieList',

    initialState,

    reducers: {
        [ ... ]

        updateMovie: (state, action) => {
            const { id, name, desc } = action.payload                       // Hacemos un destructuring de los datos que enviaremos
            const movieToUpdate = state.find(movie => movie.id === id)      // Buscamos por el ID la película
            movieToUpdate.name = name                                       // Cambiamos los datos que existen por los que enviamos
            movieToUpdate.desc = desc
        },

        [ ... ]
    }
})

export const {
    addMovie,
    updateMovie,                // Lo exportamos para su uso luego
    deleteMovie,
} = favMovieListSlice.actions
export default favMovieListSlice.reducer;

Luego de esto será necesario crear el componente Edit.jsx para poder pasar los datos, quedando el mismo de la siguiente manera.

import $ from "jquery";
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, useNavigate, useParams } from "react-router-dom";
import { updateMovie } from "../../features/movies/favMovieListSlice";

const Edit = () => {
    const [movieToEdit, setMovieToEdit] = useState({});         // Creamos el estado que guardará nuestra película
    const { id } = useParams();                             // Tomamos el id que pasamos por parámetro
    const dispatch = useDispatch();
    const navigate = useNavigate();                                     // Usamos el useNavigate para poder redireccionar la página
    const moviesArray = useSelector((state) => state.favMovieList);     // Nos traemos la película

    const handleUpdate = (id, name, desc) => {              // Creamos la función para poder hacer el update
        dispatch(updateMovie({ id, name, desc }));          // Le pasamos los datos para el update 
        navigate("/");                                      // Y al final redirigimos al inicio
    };

    useEffect(() => {
        setMovieToEdit(moviesArray.find((movie) => movie.id === id));           // Buscamos la película dentro del array
    }, []);

    return (
        <div className="bg-zinc-900 text-white min-h-screen h-fit text-center flex flex-col gap-8 p-4">
            <Link
                to={"/"}
                className="py-3 px-6 text-white bg-blue-600 rounded w-1/2 mx-auto border-2 border-black transition-all hover:scale-105"
                    type="submit"
            >
                ⬅ BACK
            </Link>             {/* Creamos el link para volver al inicio */}
            <form className="flex flex-col w-1/4 mx-auto text-black gap-4">
                <input
                className="rounded p-2"
                name="nameUpdate"
                type="text"
                id="movieNameUpdate"
                defaultValue={movieToEdit.name}         {/* Le pasamos el dato que obtuvimos como valor por defecto */}
                />
                <textarea
                className="rounded p-2"
                name="descUpdate"
                id="movieDescUpdate"
                cols="30"
                rows="5"
                defaultValue={movieToEdit.desc}
                ></textarea>
                    <button
                        onClick={() =>
                            handleUpdate(
                                id,
                                $("#movieNameUpdate").val(),
                                $("#movieDescUpdate").val()
                            )                   {/* Le pasamos los datos de los input como valores a cambiar */}
                        }
                        className="py-3 px-6 text-white bg-purple-600 rounded w-1/2 mx-auto border-2 border-black transition-all hover:scale-105">
                        SAVE ➕
                    </button>
            </form>
        </div>
    );
};

export default Edit;

Todo este componente se puede modularizar, pero para el resultado final se eliminará, por lo que no es completamente necesario

Con esto hecho tenemos completado nuestro CRUD con Redux.

Mantener el estado

Lo proximo que debemos hacer es mantener el estado, algo similar como lo que se hace con el localstorage, para ello usaremos un paquete llamado redux-persist, instalando de la siguiente manera.

npm install redux-persist

redux-persist nos ayuda a la hora de crear un storage persistente, ya que no es posible usar localstorage dentro de redux. Para comenzar con la configuración debemos ir a nuestro store.js, el cual cambiaremos como nos indica la documentación, quedando de la siguiente manera

import {
    combineReducers,
    configureStore
} from '@reduxjs/toolkit'            // Importamos lo necesario de redux

import {
    persistReducer,             // Importamos la función principal de redux-persist
    FLUSH,
    REHYDRATE,
    PAUSE,
    PERSIST,
    PURGE,
    REGISTER,                   // Así como ciertas configuraciones para evitar errores
} from 'redux-persist'

import storage from 'redux-persist/lib/storage'         // Y el storage

import favMovieListSlicer from '../features/movies/favMovieListSlice';

const persistConfig = {             // Creamos la función
    key: 'moviesInStorage',         // Le pasamos el nombre de la key
    storage,                        // Y el storage como parámetros
}

const reducer = combineReducers({           // Usamos el combineReducers y lo guardamos en una constante
    favMovieList: favMovieListSlicer
})

const persistedReducer = persistReducer(persistConfig, reducer)         // Y paramos ambos como parámetros de la función

const store = configureStore({
    reducer: persistedReducer,          // Le pasamos el persistedReducer que creamos como reducer
    middleware: getDefaultMiddleware => getDefaultMiddleware({          // Y creamos el middleware que evita los errores
        serializableCheck: {
            ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],        // Pasando lo que pedimos desde redux-persist
        },
    }),
})

export default store;

Hecho esto podemos pasar a nuestro main.jsx y terminar de configurar lo necesario para que funcione de la siguiente manera.

import "./index.scss";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "react-redux";
import store from "./app/store";
import { BrowserRouter } from "react-router-dom";
import { PersistGate } from "redux-persist/integration/react";      // Importamos PersistGate desde redux-persist para React
import { persistStore } from "redux-persist";           // Y la función persistStore

const persistor = persistStore(store)           // Utilizamos la función y le pasamos como parámetro el mismo store que utilizamos para el Provider

ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <Provider store={store}>
            <BrowserRouter>
                <PersistGate persistor={persistor}>  {/* Envolvemos nuestro componente principal con el PersistGate que importamos anteriormente, pasando el persistor como prop */}
                    <App />
                </PersistGate>
            </BrowserRouter>
        </Provider>
    </React.StrictMode>
);

Con esto configurado tenemos nuestro estado persistente.