Conceptos claves para empezar
¿Ya tomaste el Curso Básico de Redux?
Conceptos claves de Redux
Ciclo de vida de Redux
Diferencias entre Redux y Context
Introducción a nuestro proyecto
Creemos una Pokedux
Iniciando nuestro proyecto
¡Atraparlos ya!
Introducción a PokeAPI
React.js + Redux
Integrando Redux
Hooks vs. Connect
Redux DevTools
Middlewares
Middlewares
Peticiones asíncronas
Redux Thunk
Middlewares alternativos: Redux Saga
Avanzando la ui
Agreguemos un loader
Agreguemos favoritos
Inmutabilidad
¿Qué es inmutabilidad?
Agregando Inmutabilidad a nuestra Pokedux
Avanzado
Cuándo usar reducers combinados
Redux Toolkit: creando nuestro primer Slice
Redux Toolkit: createAsyncThunk
Despedida del curso
Conclusiones
No tienes acceso a esta clase
¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera
Has llegado a un momento crucial en tu aprendizaje sobre ReWiz: aplicar todo lo aprendido para agregar funcionalidad a una caja de búsqueda en tu aplicación. El reto consiste en permitir que el usuario ingrese un texto y que la lista de Pokémones se filtre según coincidencias de nombres. Asegúrate de compartir tu solución con la comunidad en la caja de aportes para fomentar el aprendizaje colectivo. Ahora, veamos los conceptos clave que te ayudarán en esta tarea.
ReWiz es una librería que ofrece un enfoque innovador para gestionar estados en aplicaciones React. Entender su ciclo de vida es esencial para implementar la funcionalidad de búsqueda de manera eficaz. A través del curso, has aprendido a usar esta herramienta de manera efectiva, comparándola incluso con Context API para tener una perspectiva más amplia sobre sus ventajas y limitaciones.
Los Miralwares son componentes clave en ReWiz que permiten manipular acciones o procesos antes de que lleguen al reducer. Has tenido la oportunidad de crear tu propio Miralware, lo que te da la habilidad de interceptar y modificar acciones en la aplicación, haciéndolos perfectos para tareas como la autenticación o el registro.
Redux Tone es una herramienta que complementa ReWiz al ampliar las capacidades de middleware. Te ofrece mayor flexibilidad al manejar procesos asíncronos. Implementar Redux Tone en tu proyecto te puede ayudar a estructurar complejas cadenas de acciones, muy útil en aplicaciones a gran escala.
La inmutabilidad es un principio fundamental al manejar el estado con Redux. Aprender a mantener los estados inmutables te asegura que las actualizaciones se realicen de forma controlada y predecible. Además, con Redux Toolkit, te has familiarizado con los Slices, que simplifican la definición de reducers y actions, reduciendo significativamente la cantidad de código necesario.
Aunque el curso se ha centrado en Redux, debes saber que hay otras herramientas emergentes que pueden ser más adecuadas para ciertos proyectos. Sin embargo, Redux sigue siendo ampliamente utilizado en el ámbito laboral debido a su estabilidad y la familiaridad de muchos desarrolladores con su ecosistema.
Es importante no detenerse aquí. Te animamos a tomar el examen final para consolidar tus conocimientos y dejar una reseña si el curso fue de tu agrado. Además, continúa explorando este emocionante mundo tecnológico y comparte tus experiencias e ideas en las redes sociales. Esto no solo te ayudará a crecer profesionalmente, sino también a conectarte con otros entusiastas de la tecnología.
Aportes 48
Preguntas 3
Muchas gracias por el curso, excelente!
Para el buscador modifiqué el initialState agregándole un campo llamado pokemonsFiltered. También modifiqué el dataSlice, donde en setPokemons se cargan ambos campos y en setFavorite se lee pokemonsFiltered. Y agregué la función setFiltered
const initialState = {
pokemons: [],
pokemonsFiltered: [],
};
/*..... */
export const dataSlice = createSlice({
name: "data",
initialState,
reducers: {
setPokemons: (state, action) => {
state.pokemons = action.payload;
state.pokemonsFiltered = action.payload;
},
setFavorite: (state, action) => {
const currentPokemonIndex = state.pokemonsFiltered.findIndex(
(pokemon) => pokemon.id === action.payload.pokemonId
);
if (currentPokemonIndex >= 0) {
const isFavorite = state.pokemonsFiltered[currentPokemonIndex].favorite;
state.pokemonsFiltered[currentPokemonIndex].favorite = !isFavorite;
}
},
setFilter: (state, action) => {
const pokemonsFiltered = state
.pokemons
.filter( pokemon => pokemon.name.includes( action.payload ) )
state.pokemonsFiltered = pokemonsFiltered;
}
},
});
El componente App lee pokemonsFiltered
const pokemons = useSelector((state) => state.data.pokemonsFiltered, shallowEqual);
Finalmente, el componente Searcher dispara la función setFilter para modificar la lista.
export const Searcher = () => {
const dispatch = useDispatch();
const handleOnChange = (e) => {
dispatch(setFilter(e.target.value));
}
return <Input.Search
placeholder="Buscar..."
onChange={handleOnChange}
style={{marginBottom: '10px'}} />
}
Que importante es conocer el flujo de redux en versiones anteriores o “the old way”, he aplicado redux toolkit en trabajo pero nunca la manera anterior a este. Muchas gracias por el curso!
Comparto el resultado del proyecto final:
Repositorio: https://github.com/jhcueva/Pokedex
Web desplegada: https://pokedex-tau.vercel.app
Gran curso, rapido y conciso, que gran facilidad para explicar las cosas ❤
esta es mi pokedex construida con react y tailwind
todavia no la termino, porque estaba con el tema del diseño, ya que soy pesimo en eso xD,
pero implemente una paginación simple 😃 , modo oscuro 🌗 y un spinner de pokeball simulando la captura 😜
más adelante implementaré rutras (wouter), detallles del pokemon y fetchs por generaciones
la captura de pantalla la realice con esta app 😃
WebScreenshots
en esta utilize jotai para el manejo del estado 😄
Me fascino este curso! siempre le temo a React sin embargo ya estoy pronto de acabar la ruta con pasos firmes en mi aprendizaje.
¡Qué bien me encantó este curso!!
Tomó su tiempo, la ultima parte, pero finalmente se consiguió. Quería mas de una vez hacerlo sin redux por lo pequeño de la app 😄
Excelente curso! Hace falta un curso de Redux Toolkit puramente, donde profundicemos mas en su uso.
Por fin entendi Redux.
Gracias!!!
estuvo buenisimo el curso
Dejo mi solucion con estilos propios y el buscador funcional. Lo hice creando un nuevo metodo en el slice de data mas otro estado que verifica el estado del imput y en base a eso actualiza la UI en caso que no encuentre ningun resultado
web deployed: https://pokedux-delegadodev.netlify.app/
codigo: https://github.com/Delegado007/pokedux/tree/styles_component
Así quedó mi sitio con la función de buscar
Buen cursoo Gracias profa.
Muy buen curso, me gusto el enfoque, se aprendieron muchas cosas. para dejar mi aporte nomas para evitar que se guarde tanta data en el state
Puse esto en API para solamente mandar al state las propiedades que utilizamos
export const getPokemonDetail = (pokemon) => {
return axios
.get(pokemon.url)
.then(res => {
const data = res.data
return {
id: data.id,
name: data.name,
sprites: data.sprites,
types: data.types,
// favorite: false,
}
})
.catch(err => console.log(err))
}
Por otro lado muy de acuerdo con todos los comentarios que necesitamso un curso a profundidad de Redux Toolkit
Muy bueno el curso, lo que más me gustó es el uso de redux para proyectos legacy y luego redux-toolkit como alternativa super fácil. Esto fue como la evolución de vuex a pinia en Vue.js
Me gusto mucho el curso estuvo genial, debo leer la Doc de Redux para realmente sacarle provecho a todo lo que tiene.
Comparto mi PokeRedux final, agregué la vista de detalles y el menú de favoritos(la estrella grande amarilla), las búsquedas filtran tipos de pokemon y nombres de pokemon, se puede consultar pokemons por regiones. El diseño lo realicé con tailwind y algunas imágenes son de PNGEggs y WikiDex.
Page: https://kortexcode.github.io/Pokedex-Redux/
Repo: https://github.com/KortexCode/Pokedex-Redux
Teacher 5 stars !!
Excelente curso 😄
Este curso estuvo buenísimo, sin duda alguna lo mejor de lo mejor, muchas gracias profe.
Excelente curso y que gran profesora, muy bien explicado todo
Tan bueno el curso que me lo devoré en una tarde ♥ me servirá muchísimo
Lo que hice para evitar tener un doble listado de elementos fue en principio en nuestra función fetchPokemonWithDetails() hice un ciclo adicional el cual solo corre la primera vez para agregar una propiedad adicional llamada visible la cual por defecto estará en true para que se muestren todos los pokemons en la primera carga
export const fetchPokemonWithDetails = createAsyncThunk(
"data/fetchPokemonWithDetails",
async (_, {dispatch}) => {
dispatch( setLoading(true));
const pokemonsResponse = await getPokemons();
const pokemonsDetailed = await Promise.all( pokemonsResponse.map( (pokemon) => getPokemonDetails(pokemon) ));
pokemonsDetailed.forEach( (pokemon) => {
pokemon.visible = true;
});
dispatch( setPokemons(pokemonsDetailed) );
dispatch( setLoading(false));
}
);
Esta propiedad la envio desde PokemonList a PokmeonCard
const PokemonList = ({ pokemons }) => {
return <div className="PokemonList">
{pokemons.map( (pokemon) => {
return <PokemonCard
name={pokemon.name}
image={pokemon.sprites.front_default}
types={pokemon.types}
id={pokemon.id}
favorite={pokemon.favorite}
visible={pokemon.visible}
key={pokemon.name}
/>
})}
</div>
}
En PokmeonCard creo una lógica sencilla para mostrar o no mostrar el card dependiendo lo que venga en visible
return visible ? <Card
title={name}
cover={<img src={image} alt="Charmander" />}
extra={<StarButton isFavorite={favorite} onClick={ handleOnFavorite } />}
>
<Meta description={typesString} />
</Card> : null
En el reducer de data creo la lógica para tener el estado del valor buscado, para lo cual creo una variable llamada searchedValue en. el initialState, y agrego el respectivo reducer que se encarga de procesar la lógica para activar o no el visible basado en el contenido del estado de searchedValue
setSearchedValue: (state, action) => {
state.searchedValue = action.payload;
if( state.searchedValue !== "" ) {
state.pokemons.forEach( (pokemon) => {
pokemon.visible = pokemon.name.toLowerCase().includes(state.searchedValue.toLowerCase())
});
}
else {
state.pokemons.forEach( (pokemon) => pokemon.visible = true )
}
}
Pongo todo en lowercase por para garantizar que siempre se evaluen todos los nombres sin importar si vienen en mayusculas o minusculas, vicio realmente de crear otros buscadores en el pasado.
–
Finalmente hago el dispatch de setSearchedValue desde el mismo componente de Searcher en donde tengo almacenada la lógica correspondiente a este input, al igual que agregué un label para mostrar el valor buscado, aunque realmente lo hice fue para probar que me estuviere conservando el estado de forma correcta.
import { Input } from 'antd';
import { useDispatch, useSelector } from 'react-redux';
import { setSearchedValue } from '../slices/dataSlice';
const Searcher = () => {
const searchedValue = useSelector( (state) => state.data.searchedValue );
const dispatch = useDispatch();
const handleChangeSearchedValue = (input) => {
dispatch( setSearchedValue(input.target.value) );
}
return (
<Input.Group className='searchBoxGroup'>
<Input.Search placeholder="Buscar" value={searchedValue} onChange={handleChangeSearchedValue} />
<label>
{searchedValue !== "" ? ("Searching (" + searchedValue + ")") : null}
</label>
</Input.Group>
)
}
export default Searcher;
De tener en cuenta que en este aso dado que estamos usando AntDesign el evento en el Input Search para capturar correctamente el valor es onChange el cual se encuentra en la documentación.
Esa fue mi solución
Muchas gracias profesora por este curso tan bien explicado.
Entre al curso con ideas generales y ahora me voy teniendo toda la base clara y solida además de poder adaptarme a las diferentes herramientas relacionadas con Redux, aunque claro yo propondre toolkit sin dudarlo jajaja. Hasta un proximo curso, espero encontrarla en otro curso de React.
El buscador de pokemons que hemos incluido en la UI, sin duda lo más útil de este curso.
Excelente!! Gracias por todo lo aprendido.
Muy buen curso! Gracias!
Me gustó mucho el curso y pude practicar lo aprendido.
Mi solución al buscador.
Agregue dos variables al estado del dataSlide para poder realizar la búsqueda y que al agregar a favoritos pudiera guardar la información, luche mucho en encontrar una solución para o de favoritos pero al final esto fue lo mejor que se me ocurrió.
Super este curso, al entender el flujo de redux desde lo básico y luego pasar a reduxjs/toolkit pude entender a fondo cómo es que estaban manejando en el proyecto en el que me encuentro ya si poder implementar mis nuevos funcionalidades que necesitaba sin perderme.
Nice course Mariangelica. I struggled in certain parts, but overall, it was neat.
Habia hecho la solucion sin usar Redux y haciendo el filter localmente con useState, pero como es un curso de Redux lo cambie agregando un property “searchText” en el slice ui
const initialState = {
loading: false,
searchText: '',
};
Me parecio la mejor solucion sin tener que agregar otroa copia de todos los pokemons en un property “filtered” o algo similar.
En mi componente App.js hago el filtrado usando los valores del state y uso “useMemo” para que la operacion de filtrado no se haga en cada renderizado y solo se haga cuando cambie el valor del texto a filtrar o la lista total de pokemons:
const { loading, searchText } = useSelector(
(state) => state.ui,
shallowEqual
);
const filteredPokemons = useMemo(() => {
const lowerCaseSearchText = searchText.toLowerCase();
return pokemons.filter(({ name }) =>
name.toLowerCase().includes(lowerCaseSearchText)
);
}, [searchText, pokemons]);
Y por ultimo en mi componente Searcher solo agregue mi funcion “handleOnSearch” y la pase al componente en la propiedad “onSearch” en lugar de “onChange” para que la busqueda se haga la presionar enter o al hacer click en el boton de busqueda, y no mientras se escribe:
import { Input } from 'antd';
import { useDispatch } from 'react-redux';
import { setSearchText } from '../slices/uiSlice';
const Searcher = () => {
const dispatch = useDispatch();
const handleOnSearch = (searchText) => dispatch(setSearchText(searchText));
return <Input.Search placeholder='Buscar...' onSearch={handleOnSearch} />;
};
export default Searcher;
un curso que me costo un poco de trabajo entender algunos conceptos, pero sin duda necesario para mis actividades diarias en tech
Gracias @musarte.dev!
que buen curso
Me encantó el curso, pero tengo que estudiarlo más, aún me cuesta bastante 😅
¡Excelente curso! Pondré en práctica lo aprendido en mi siguiente proyecto con React. ¡Muchas felicidades a la maestra!
⭐Me enfoque en una solución de mejorar el UX del usuario guardando los favoritos en otra vista con React Router ya que al navegar entre varias paginas se pierden los pokemones.
https://pokedux-one.vercel.app/
https://github.com/Diegosotomayor1/pokedux
Hola, les comparto mi solución
https://alexguaman.github.io/pokedux/
Mi solución al reto utilizando vite + typeScript:
(Cualquier duda o aporte de ts lo dejan en los comentarios)
// dataSlice.tsx
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { getPokemonDetails, getPokemons, PokemonDetailType, PokemonType } from "../api";
import { setLoading } from "./uiSlice";
type InitialState = {
pokemons: PokemonDetailType[],
searched : string
}
const initialState: InitialState = {
pokemons: [],
searched: ''
}
export const fetchPokemonsWithDetails = createAsyncThunk(
'data/fetchPokemonsWithDetails',
async (_,{dispatch}) =>{
try {
dispatch(setLoading(true))
const pkmns = await getPokemons();
const pkmnsDetailed = await Promise.all((pkmns as PokemonType[]).map(pkmn=>getPokemonDetails(pkmn)))
dispatch(setPokemons(pkmnsDetailed))
dispatch(setLoading(false))
} catch (err) {
console.error(err)
dispatch(setLoading(false))
}
}
)
export const dataSlice = createSlice({
name: 'data',
initialState,
reducers:{
setPokemons: (state,action:PayloadAction<PokemonDetailType[]>)=>{
state.pokemons = action.payload
},
setFavorite: (state, action:PayloadAction<{pokemonId:number}>)=>{
const currentPokemonIndex = state.pokemons.findIndex(
(pkmn:PokemonDetailType) => {
return pkmn.id === action.payload.pokemonId
}
)
if(currentPokemonIndex >= 0){
const isFavorite = state.pokemons[currentPokemonIndex].favorite
state.pokemons[currentPokemonIndex].favorite = !isFavorite
}
},
setSearched: (state, action:PayloadAction<string>)=>{
state.searched = action.payload
}
}
})
export const dataReducer = dataSlice.reducer;
export const { setPokemons, setFavorite, setSearched } = dataSlice.actions;
// App.tsx
import { useEffect } from 'react'
import { Search } from './components/Search'
import { PokemonList } from './components/PokemonList'
import { Col, Spin } from 'antd'
import { useAppDispatch, useAppSelector } from './slices'
import { fetchPokemonsWithDetails } from './slices/dataSlice'
import logo from './statics/logo.svg'
import './App.css'
function App() {
const pokemons = useAppSelector((state)=>state.data.pokemons)
const searched = useAppSelector(state=>state.data.searched)
const pokemonsSearched = pokemons.filter(pokemon=>pokemon.name.toLowerCase().includes(searched.toLowerCase()))
const loading = useAppSelector((state)=>state.ui.loading)
const dispatch = useAppDispatch();
useEffect(()=>{
dispatch(fetchPokemonsWithDetails())
},[])
return (
<div className="App">
<Col span={4} offset={10}>
<img src={logo} alt="Pokedux" />
</Col>
<Col span={8} offset={8}>
<Search />
</Col>
{loading?
<Col offset={12} >
<Spin spinning size='large' />
</Col>
:
<PokemonList pokemons={pokemonsSearched} />
}
</div>
)
}
export default App;
// Search.tsx
import { Input } from "antd";
import { useAppDispatch, useAppSelector } from "../slices";
import { setSearched } from "../slices/dataSlice";
const Search = ()=>{
const searched = useAppSelector(state=>state.data.searched)
const dispatch = useAppDispatch();
const handler = (event:any)=>{
console.log(event.target.value)
dispatch(setSearched(event.target.value))
}
return <Input.Search value={searched} onChange={handler} placeholder="Buscar..." style={{marginBottom: 10}}/>
}
export {
Search
}
para el buscador, añadí una nueva propiedad search al estado
const initialState = {
pokemons: [],
search: []
}
luego añadí una nueva acción
export const dataSlice = createSlice({
name: 'data',
initialState,
reducers: {
setPokemons: (state,action) => {
....
setSearch: (state, action) => {
state.search = state.pokemons.filter(poke => poke.name.includes(action.payload))
}
}
})
desde el componente App con el hook useSelector, obtuve el valor de esa nueva propiedad
const search = useSelector(state => state.data.search)
y cree un estado local con valor por defecto de false
const [isSearch,setIsSearch] = useState(false)
todo esto lo hice para poder pasar props de una manera dinámica dependiendo el valor del estado local a el componente PokemonList desde el componente App
<PokemonList pokemons={isSearch ? search : pokemons} />
la función para cambiar el valor del estado la pase por prop a el componente Searcher
<Searcher setIsSearch={setIsSearch} />
quedando de esta forma App.js:
const App = () => {
const dispatch = useDispatch()
const pokemons = useSelector(state => state.data.pokemons, shallowEqual)
const search = useSelector(state => state.data.search)
const loading = useSelector(state => state.ui.loading)
const [isSearch,setIsSearch] = useState(false)
useEffect(()=> {
dispatch(fetchPokemonsWithDetails())
}, [])
return (
<div className="App">
<Col span={4} offset={10}>
<img src={pokedux} alt='pokedux' />
</Col>
<Col span={8} offset={8}>
<Searcher setIsSearch={setIsSearch} />
</Col>
{
loading ?
<Col offset={12}>
<Spin spinning size='large' />
</Col> :
<PokemonList pokemons={isSearch ? search : pokemons} />
}
</div>
);
}
export default App
desde el componente Searcher solo recibí la prop y cree la lógica añadiendo eventos para poder invocar la función que esperamos en la prop y su valor cambie a true cuando se detecte un cambio en el input, y disparar la acción que anteriormente habíamos creado pasándole como parámetro el valor que recibimos en el input
const Searcher = ({ setIsSearch })=> {
const dispatch = useDispatch()
const handleChange = (e)=> {
if (e.target.value === ''){
setIsSearch(false)
}
dispatch(setSearch(e.target.value))
setIsSearch(true)
}
return <Input.Search placeholder="Buscar.." style={{marginBottom: 10}} onChange={handleChange} />
}
Comparto el resultado final del proyecto del curso con el buscador: Proyecto pokedux
Aca dejo mi solución al buscador https://github.com/kincasasbuenas/pokedux/commit/0b032d221d4d3f6d9820015b6a3ab94c11f171c8
¿Quieres ver más aportes, preguntas y respuestas de la comunidad?