Carlos Eduardo Bracho Ramírez
Josué Hernández
Ivan Camilo Buitrago Buitrago
Efrén Quintero Salazar
Yoan Estrada
César Palma
Juan Camilo Lentino Villalba
Camilo Andrés Granda Cortés
Mariangelica Useche
Team PlatziCayo Legal
Miguel Angel Reyes Moreno
Geovanny Sneyder Paez Pabon
Juan Diego Mejia Maestre
César Augusto Cortés Labrada
David Rangel
David Rangel
Jair Neri
Daniel Alberto Vega Bejarano
Andrés Serafin Rene Jerez Camargo
Harold Zurita Simon
Daniel Arenas
German Cavani
Mariangelica Useche
Team PlatziAndrés Felipe Eslava Zuluaga
Sebastián Andrés Sanhueza Tapia
Sebastian Carballo
Kengya Moncada
Michael Merchan
En rootReducer.js, recuerden importar lo siguiente:
import { combineReducers } from "redux-immutable", desde redux-immutable.
Si importan combineReducers from "redux", les va a arrojar error y perderán 15 minutos de sus vidas.
Máquina, héroe, mastodonte, fiera, tifón Gracias
la profe lo explica en la clase
Se supone que JS fuera mas fácil pero veo que cada vez se vuelve mas y mas difícil de entender que triste
mano pero eso es para tener mejores experiencias de usaurios dale duro que poco a poco vas ganando experiencia y despues se hace facil
solo estas viendo una perspectiva de como montarlo.
npm install redux-immutable
Uso de shallowEqual vs. Comparación a Profundidad en JavaScript
Supongamos que tenemos dos objetos de Pokémon:
const pokemon1 = { nombre: "Pikachu", tipo: "Eléctrico" }; const pokemon2 = { nombre: "Pikachu", tipo: "Eléctrico" };
Ahora, si utilizamos shallowEqual para compararlos:
import { shallowEqual } from 'react-redux'; console.log(shallowEqual(pokemon1, pokemon2)); // true
En este caso, shallowEqual compara las propiedades de nivel superior de los objetos pokemon1 y pokemon2, que son nombre y tipo. Como ambas propiedades tienen los mismos valores, la comparación devuelve true.
Sin embargo, si comparamos los mismos objetos directamente con una comparación a profundidad utilizando el operador ===:
console.log(pokemon1 === pokemon2); // false
La comparación a profundidad verifica si los objetos son idénticos en estructura y valor, incluyendo todas las propiedades anidadas. A pesar de que las propiedades nombre y tipo tienen los mismos valores en ambos objetos, la comparación a profundidad devuelve false porque los objetos en sí no son idénticos en estructura. son dos objetos diferentes
.
shallowEqual es útil cuando solo necesitas comparar las propiedades de nivel superior de objetos, evitando renders innecesarios en componentes de React al utilizar Redux. Si necesitas comparar objetos en su totalidad, incluyendo propiedades anidadas, una comparación a profundidad es necesaria.
Excelente aporte :clap:
No entendí lo de shallowEqual. Cuando hay que usarlo?
Avanzado
Cuándo usar reducers combinados
Combinar reducers es opcional y puede mejorar la experiencia de desarrollo. Sin embargo, eres libre de manejar todo con 1 solo reducer (aunque no es recomendable).
Usaremos redux-immutable
Reducers Combinados
primero se crea una carpeta de reducer, en esa creamos el archivo index o la raiz de todos los reducer combinados
reducer independientes
//pokemons.jsx import { SET_ERROR, SET_POKEMONS, SET_FAVORITE } from "../actions/types"; import { fromJS, get, setIn } from "immutable"; const initialState = fromJS({ pokemons: [], loading: true, }); export const pokemonsReducer = (state = initialState, action) => { switch (action.type) { case SET_POKEMONS: const pokemonsUpdated = setIn( state, ["pokemons"], fromJS(action.payload) ); return pokemonsUpdated.set(["loading"], false); case SET_FAVORITE: const pokemonFoundIndex = get(state, "pokemons").findIndex((pkm) => { const pkmon = pkm.toJS().id; return pkmon == action.payload.id; }); console.log(pokemonFoundIndex, action.payload.id); if (pokemonFoundIndex < 0) return state; const isFavorite = get(state, [ "pokemons", pokemonFoundIndex, "isFavorite", ]); return setIn( state, ["pokemons", pokemonFoundIndex, "isFavorite"], !isFavorite ); default: return state; } };
//iu.jsx
import { SET_ERROR, SET_POKEMONS,SET_FAVORITE } from "../actions/types"; import {fromJS, get, set, setIn} from 'immutable' const initialState = fromJS({ error:false }); export const uiReducer = (state = initialState, action) => { switch (action.type) { case SET_ERROR: return state.set(['error'],action.payload); default: return state; } }; // const
IndexReducers o el rootReducer
import {combineReducers} from 'redux-immutable'; import { uiReducer } from './iu'; import { pokemonsReducer } from './pokemons'; //como estamos utilicemmos la libreria de immutable, para // usar reducer combinados, necesitamos instalar redux-immutable const rootReducer= combineReducers({ data:pokemonsReducer, ui: uiReducer }) export default rootReducer;
ahora pasamos al index de nuestra app y mandamos el reducer que combina el resto
import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import { Provider } from "react-redux"; import thunk from 'redux-thunk' import { applyMiddleware, compose, legacy_createStore as createStore } from "redux"; import { loger } from "./middlewares"; import rootReducer from "./reducers/rootReducer"; const composeAlt = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const componoseEnjencer = composeAlt(applyMiddleware(thunk,loger)); const store = createStore(rootReducer,componoseEnjencer); ReactDOM.createRoot(document.getElementById("root")).render( <Provider store={store}> <React.StrictMode> <App /> </React.StrictMode> </Provider> );
como estamos usando immutable js necesitamos reacomodar en donde utilizamos nuestro estado
import { Searched } from "./components/Searched"; import { shallowEqual, useDispatch, useSelector } from "react-redux"; import { Col, Row } from "antd"; import "./App.css"; import { getPokemonWithDetails, setPokemons } from "./actions"; import { PokemonList } from "./components/PokemonList"; // import { usePokeApi } from './hooks/usePokeApi'; import { PokeSkeleton } from "./components/PokeSkeleton"; import { useEffect } from "react"; import { getPokemonDetails, getPokemons } from "./services/PokeApi"; import { get } from "immutable"; function App() { const state = useSelector((state) => state.get('data',shallowEqual).toJS()); //la funcion shadow equal ayuda a verificar si el estado ha cambiado // y si no cambio no hara render innecesarios, es decir, no hara rerender const dispatch = useDispatch(); const { pokemons,loading} = state useEffect(() => { getPokemons() .then(pkmos=>dispatch(getPokemonWithDetails(pkmos.results))) .catch(err=>console.log(err)); }, []); return ( <div className="app"> <Col span={4} offset={10} style={{ marginBottom: 30 }}> <img src="https://static.platzi.com/media/tmp/class-files/github/curso-redux/curso-redux-01-pokeapi/src/statics/logo.svg" alt="pokedux" /> </Col> <Col span={8} offset={8}> <Searched /> </Col> <Row style={{ marginTop: 50 }} justify={"center"} gutter={20}> {loading ? <PokeSkeleton /> : <PokemonList pokemons={pokemons} />} </Row> </div> ); } export default App;
🔒 Guía Maestra: Inmutabilidad con ImmutableJS
La inmutabilidad asegura que el estado de tu aplicación sea predecible y eficiente. Al no modificar los datos originales, facilitas el rastreo de cambios y optimizas el renderizado en frameworks como React.
🚀 1. Conceptos y Configuración
npm install immutable🛠️ 2. Estructuras y Conversión
Map: Para objetos {}.List: Para arreglos [].fromJS(): Conversión profunda de JS plano a inmutable.toJS(): Conversión de inmutable a JS plano (usar con moderación por su costo).🎯 3. Manipulación de Datos
getIn(['path', 'to', 'key']): Acceso seguro a datos anidados. Si la ruta no existe, devuelve undefined en lugar de romper la app.setIn(['path'], value): Crea una nueva versión con el dato actualizado.findIndex(): Localiza la posición de un elemento en una List.⚡ 4. Conceptos Avanzados y Ejemplos
A. Comparación por Valor con Immutable.is()
A diferencia de ===, que compara si dos objetos están en la misma dirección de memoria, is() compara si el contenido es idéntico.
import { Map, is } from 'immutable'; const mapA = Map({ a: 1, b: 2 }); const mapB = Map({ a: 1, b: 2 }); console.log(mapA === mapB); // false (instancias diferentes) console.log(is(mapA, mapB)); // true (contenido idéntico) // Caso de uso: Evita re-renders innecesarios en React.
B. Optimización Masiva con withMutations()
Si aplicas muchos cambios seguidos, hacer copias intermedias es lento. withMutations crea un estado "temporalmente mutable" para agrupar todo en una sola operación final.
import { List } from 'immutable'; const listaOriginal = List([1, 2, 3]); const listaOptimizada = listaOriginal.withMutations(mutable => { for (let i = 4; i <= 100; i++) { mutable.push(i); // No crea copias intermedias aquí } });
🏗️ 5. Arquitectura: Combine Reducers & Responsabilidades
A medida que la app crece, un solo "monolito" de estado es inmanejable. Aplicamos "Divide y Vencerás".
data para pokémones, ui para loaders).redux-immutable: Cuando usas ImmutableJS, debes usar el combineReducers de esta librería para que el estado global sea un Mapa Inmutable y no un objeto JS plano.import { combineReducers } from 'redux-immutable'; const rootReducer = combineReducers({ data: pokemonsReducer, ui: uiReducer, });
📉 6. Rendimiento con useSelector
El hook useSelector lanza un re-render si el valor retornado cambia (por referencia).
===): Es la que viene por defecto. Si el selector devuelve una nueva estructura (como un .toJS()), el componente siempre se volverá a renderizar.shallowEqual: Realiza una comparación superficial de las propiedades.import { useSelector, shallowEqual } from 'react-redux'; // Uso correcto con shallowEqual para evitar renders infinitos const pokemons = useSelector(state => state.getIn(['data', 'pokemons']), shallowEqual);
✅ 7. ¿Por qué usar este stack?
Immutable.is y shallowEqual, tus componentes solo se pintan cuando la información realmente cambia.Es más una reflexión según lo que entendí de esta clase...
Antes en la clase 18 cuando usamos por primera vez la librería immutable, también debimos usar el shallowEqual debido a que estamos regresando una referencia distinta cada vez que consultamos el estado con useSelector ya que si el estado cambiaba o no cambiaba, la referencia es siempre distinta. Sin immutable no había problema porque a pesar de regresar referencias distintas Redux aquí compara valores, no referencias. Por supuesto, sin immutable habría que desestructurar los niveles de profundidad que pudieran existir en alguna de las propiedades de nuestro estado.
Así queda con TS:
// app/types/reducerTypes.ts import { Map } from 'immutable'; import type { SET_LOADING, SET_POKEMONS, SET_FAVOURITE } from './actionTypes'; import type { PokeDetail } from './pokeTypes'; export interface PokemonSchema { pokemons: PokeDetail[]; } export type PokemonState = Map<keyof PokemonSchema, any>; export interface LoadingSchema { loading: boolean; } export type LoadingState = Map<keyof LoadingSchema, any>; export interface SetPokemonAction { type: typeof SET_POKEMONS; payload: PokeDetail[]; } export interface SetLoadingAction { type: typeof SET_LOADING; payload: boolean; } export interface SetFavouriteAction { type: typeof SET_FAVOURITE; payload: number; } export type PokemonAction = SetPokemonAction | SetFavouriteAction; // app/reducers/pokemonsReducer.ts import { fromJS } from 'immutable'; import { SET_LOADING, SET_POKEMONS, SET_FAVOURITE } from '~/types/actionTypes'; import type { PokemonAction, PokemonState } from '~/types/reducerTypes'; const initialState: PokemonState = fromJS({ pokemons: [], }); export const pokemonsReducer = ( state: PokemonState = initialState, action: PokemonAction, ) => { switch (action.type) { case SET_POKEMONS: return state.setIn(['pokemons'], fromJS(action.payload)); case SET_FAVOURITE: const currenPokeIndex = state .get('pokemons') .findIndex((pokemon: any) => pokemon.get('id') === action.payload); if (currenPokeIndex === -1) return state; const isFavourite = state.getIn([ 'pokemons', currenPokeIndex, 'favourite', ]); return state.setIn( ['pokemons', currenPokeIndex, 'favourite'], !isFavourite, ); default: return state; } }; // app/reducers/uiReducer.ts import { fromJS } from 'immutable'; import { SET_LOADING } from '~/types/actionTypes'; import type { LoadingState, SetLoadingAction } from '~/types/reducerTypes'; const initialState: LoadingState = fromJS({ loading: false, }); export const uiReducer = ( state: LoadingState = initialState, action: SetLoadingAction, ) => { switch (action.type) { case SET_LOADING: return state.update('loading', (value) => !value); default: return state; } }; // app/reducers/rootReducer.ts import { combineReducers } from '@reduxjs/toolkit'; import { pokemonsReducer } from './pokemonsReducer'; import { uiReducer } from './uiReducer'; export const rootReducer = combineReducers({ data: pokemonsReducer, ui: uiReducer, }); export type RootState = ReturnType<typeof rootReducer>; // app/welcome/welcome.tsx import { useEffect } from 'react'; import { Col, Spin } from 'antd'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import logo from '../statics/logo.svg'; import { PokeList } from '~/components/PokeList'; import { Searcher } from '~/components/Searcher'; import { getPokemon } from '~/api/getPokemon'; import { getPokemonDetails } from '~/api/getPokemonDetails'; import { type AppDispatch } from '~/store/store'; import { getPokemonsWithDetails, setLoading } from '~/actions'; import { type RootState } from '~/reducers/rootReducer'; export function Welcome() { const pokemons = useSelector( (state: RootState) => state.data.get('pokemons'), shallowEqual, ).toJS(); const loading = useSelector((state: RootState) => state.ui.get('loading')); const dispatch = useDispatch<AppDispatch>(); useEffect(() => { const fetchPokemons = async () => { dispatch(setLoading(true)); const pokeRes = await getPokemon(); if (pokeRes) { dispatch(getPokemonsWithDetails(pokeRes)); } dispatch(setLoading(false)); }; fetchPokemons(); }, [getPokemon, getPokemonsWithDetails, getPokemonDetails]); return ( <main> <h1>Pokedux</h1> <Col span={4} offset={10} className='my-3'> <img src={logo} alt='Pokedux' /> </Col> <Col span={8} offset={8} className='mt-3'> <Searcher /> </Col> {loading ? ( <Col offset={12}> <Spin spinning size='large' className='mt-3' /> </Col> ) : ( pokemons && <PokeList pokemons={pokemons} /> )} </main> ); }
Me faltó:
// app/store/store.ts import { legacy_createStore as createStore, compose, applyMiddleware, type Action, } from '@reduxjs/toolkit'; import { thunk, type ThunkDispatch } from 'redux-thunk'; import { logger } from '~/middlewares'; import { rootReducer, type RootState } from '~/reducers/rootReducer'; const composeAlt = (typeof window !== 'undefined' && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose; const composedEnhancers = composeAlt(applyMiddleware(logger, thunk)); export const store = createStore(rootReducer, composedEnhancers); export type AppDispatch = ThunkDispatch<RootState, undefined, Action>;
les falto comentar que se tenia que instalarr una dep.
No, ella lo menciona en el video
5:10
npm i redux-immutable
📦 Manejo del Estado Global dividiendo Reducers (con Immutable.js y Redux-Immutable)
A medida que una aplicación crece, es mejor dividir el estado por responsabilidades.
Para esto, Redux ofrece el helper combineReducers, pero cuando usamos Immutable.js, necesitamos una variante especial: redux-immutable.
🐱👤 Reducer de Pokemons
Creamos un archivo pokemons.js donde se maneja todo lo relacionado a los pokemons.
import { fromJS } from "immutable"; import { SET_FAVORITE, SET_POKEMONS } from "../actions/types"; const initialState = fromJS({ pokemons: [], }); export const pokemonsReducer = (state = initialState, action) => { switch (action.type) { case SET_POKEMONS: return state.setIn(["pokemons"], fromJS(action.payload)); case SET_FAVORITE: const currentPokemonIndex = state .get("pokemons") .findIndex((pokemon) => pokemon.get("id") === action.payload.pokemonId); if (currentPokemonIndex < 0) return state; const isFavorite = state.getIn([ "pokemons", currentPokemonIndex, "favorite", ]); return state.setIn( ["pokemons", currentPokemonIndex, "favorite"], !isFavorite ); default: return state; } };
🔄 Reducer de UI (loading)
El reducer de la parte visual va en ui.js:
import { fromJS } from "immutable"; import { SET_LOADING } from "../actions/types"; const initialState = fromJS({ loading: false, }); export const uiReducer = (state = initialState, action) => { switch (action.type) { case SET_LOADING: return state.setIn(["loading"], action.payload); default: return state; } };
🧩 Root Reducer con Redux-Immutable
Como usamos estructuras inmutables, NO podemos usar el combineReducers normal.
Debemos usar la versión compatible: redux-immutable.
⚠️ ¡Aquí surgió un problema de versiones!
Inicialmente probé con:
"immutable": "^5.1.4"Pero redux-immutable no es compatible con immutable 5, lo que generaba errores al combinar reducers.
✅ Solución de versionado que funcionó 🎉
Tuve que instalar versiones específicas:
"immutable": "^4.0.0", "redux-immutable": "^4.0.0"
El comando que resolvió todo:
npm install immutable@4.0.0
Con esa combinación, redux-immutable funciona correctamente y ya no aparecen errores.
🏗️ RootReducer.js
Una vez resuelto el conflicto de versiones, importamos el combineReducers de redux-immutable:
import { combineReducers } from "redux-immutable"; import { uiReducer } from "./ui"; import { pokemonsReducer } from "./pokemons"; const rootReducer = combineReducers({ data: pokemonsReducer, ui: uiReducer, }); export default rootReducer;
🧠 Uso en un custom hook
Cuando usamos seletores con estructuras inmutables, debemos usar getIn para acceder a niveles anidados.
Además, useSelector hace comparación estricta, por lo que es importante usar shallowEqual para evitar renders innecesarios.
import { shallowEqual, useDispatch, useSelector } from "react-redux"; import { getPokemonsWithDetails, setLoading } from "../actions"; import { getPokemon } from "../api"; import { useEffect } from "react"; export const usePokemons = () => { const pokemons = useSelector( (state) => state.getIn(["data", "pokemons"]), shallowEqual ).toJS(); const loading = useSelector((state) => state.getIn(["ui", "loading"])); const dispatch = useDispatch(); const fetchPokemons = async () => { dispatch(setLoading(true)); const pokemonsRes = await getPokemon(); dispatch(getPokemonsWithDetails(pokemonsRes)); dispatch(setLoading(false)); }; useEffect(() => { fetchPokemons(); }, []); return { pokemons, loading, fetchPokemons }; };
he estado teniendo errores con la librería immutable, les recomiendo instalar la versión 4.0.0.
npm i immutable@4.0.0
Para trabajar con la versión del curso
A alguien más le ha pasado que con todo lo que hemos puesto la aplicación termina cargando más lento? . No sé si tendrá algo que ver, pero vengo arrastrando este error en la consola desde hace algunas clases:
inpage.js:1 Uncaught (in promise) TypeError: Cannot assign to read only property 'selectedAddress' of object '#<l>' at l._handleAccountsChanged (inpage.js:1:39788) at l._handleUnlockStateChanged (inpage.js:1:39993) at l._initializeState (inpage.js:1:37982) at l._initializeStateAsync (inpage.js:1:46258)
Hola, Jeff. ¿Tienes el link a github? Y le echo una revisada :eye
Parcero, revisa los métodos que tienes del API. Son el sitio donde estamos haciendo llamadas asíncronas. Luego, verifica cada part donde las importas y revisa con debugger el paso a paso, identificando el llamado de las funciones.
Te ayudará
$ npm install redux-immutable --force
My english version branch & commit -
Branch name
feat/modularize-reducers
Commit message
Refactor: Modularize reducers by concerns
Split reducers into domain-specific files (pokemons and ui) for better organization and maintainability.
Combine reducers using combineReducers in rootReducers.js.
Update store creation in index.js to use the rootReducers.
Adjust component state access in App.js to reflect the nested reducer structure. This change improves the codebase structure by separating concerns within the Redux reducers, making it easier to understand and scale.
amé esta clase ♥
ShallowEqual: Evalua la diferencia de un objeto en su version anterior y actual para evitar el rerendering.