¡Bienvenida! Este es un curso especial de React Hooks

1

¿Qué aprenderás en el Curso Profesional de React Hooks?

2

¿Qué son los React Hooks y cómo cambian el desarrollo con React?

Introducción a React Hooks

3

useState: estado en componentes creados como funciones

4

useEffect: olvida el ciclo de vida, ahora piensa en efectos

5

useContext: la fusión de React Hooks y React Context

6

useReducer: como useState, pero más escalable

7

¿Qué es memoization? Programación funcional en JavaScript

8

useMemo: evita cálculos innecesarios en componentes

9

useRef: manejo profesional de inputs y formularios

10

useCallback: evita cálculos innecesarios en funciones

11

Optimización de componentes en React con React.memo

12

Custom hooks: abstracción en la lógica de tus componentes

13

Third Party Custom Hooks de Redux y React Router

Configura un entorno de desarrollo profesional

14

Proyecto: análisis y retos de Platzi Conf Store

15

Git Hooks con Husky

16

Instalación de Webpack y Babel: presets, plugins y loaders

17

Configuración de Webpack 5 y webpack-dev-server

18

Configuración de Webpack 5 con loaders y estilos

19

Loaders de Webpack para Preprocesadores CSS

20

Flujo de desarrollo seguro y consistente con ESLint y Prettier

Estructura y creación de componentes para Platzi Conf Store

21

Arquitectura de vistas y componentes con React Router DOM

22

Maquetación y estilos del home

23

Maquetación y estilos de la lista de productos

24

Maquetación y estilos del formulario de checkout

25

Maquetación y estilos de la información del usuario

26

Maquetación y estilos del flujo de pago

27

Integración de íconos y conexión con React Router

Integración de React Hooks en Platzi Conf Merch

28

Creando nuestro primer custom hook

29

Implementando useContext en Platzi Conf Merch

30

useContext en la página de checkout

31

useRef en la página de checkout

32

Integrando third party custom hooks en Platzi Conf Merch

Configura mapas y pagos con PayPal y Google Maps

33

Paso a paso para conectar tu aplicación con la API de PayPal

34

Integración de pagos con la API de PayPal

35

Completando la integración de pagos con la API de PayPal

36

Paso a paso para conectar tu aplicación con la API de Google Maps

37

Integración de Google Maps en el mapa de checkout

38

Creando un Custom Hook para Google Maps

Estrategias de deployment profesional

39

Continuous integration y continuous delivery con GitHub Actions

40

Compra del dominio y despliega con Cloudflare

Optimización de aplicaciones web con React

41

Integración de React Helmet para mejorar el SEO con meta etiquetas

42

Análisis de performance con Google Lighthouse

43

Convierte tu aplicación de React en PWA

Bonus: trabaja con Strapi CMS para crear tu propia API

44

Crea una API con Strapi CMS y consúmela con React.js

¿Qué sigue en tu carrera profesional?

45

Próximos pasos para especializarte en frontend

No tienes acceso a esta clase

¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera

useReducer: como useState, pero más escalable

6/45
Recursos

Aportes 72

Preguntas 21

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

🧠⚛️ useReducer no es una alternativa a Redux, solo “toma prestado” el uso de reducers para actualizar nuestro estado local.

De esta clase, esperaba que nos hablaran un poco sobre los casos de uso de useReducer, ventajas y desventajas diferencias entre useState y useReducer, cuando usar uno y cuando usar el otro. Implementar el useReducer porque se puede hacer no me explica todo lo que esperaba de esta clase.

Apuntes

  • Como useState, pero más escalable
  • Implementa una forma más amigables y llena de caracteristicas para trabajar con el estado
  • useReducer a menudo es preferible a useState cuando:
    • se tiene una lógica compleja que involucra múltiples subvalores
    • el próximo estado depende del anterior.

Ejemplo de useReducer

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

Ideas/conceptos claves

useReducer.- Esta basada en la idea de Redux de tener una función pura que devuelve estados pero que se puede aplicar de una manera local en componentes

Recursos

Referencia de la API de los Hooks - React

Así va la app

UFFF… carece de pedagogía este curso. a mi modo de ver està muy mal estructurado.

Una cosa es dominar un tema y otra bien diferente saber enseñarlo…

Los quiero bye

/* Super complicado pero lo vas a usar muchisimas veces
1. agrega el useReducer
2. crea un estado inicial: la lista de favoritos vacia
3. crea el reducer, es una funcion que usa switch identificar el metodo a usar
	  recive state y action:
		state: el estado actual.
		action: objeto con el metodo que quieres ejecutar junto con el contenido.
		action.type: el metodo a ejecutar.
		action.payload: el contenido nuevo que quieres manejar.
		Ej: ADD_TO_FAVORITE toma el estado actual y le agrega el contenido de payload
4. crea el use reducer:
		favorite: es el nombre el valor de lectura.
		dispatch: el nombre de la funcion para llamar a los metodos.
		useReducer toma dos datos, el primero es el reducer, contenedor del switch de metodos
		el segundo parametro es el estado inicial, que por lo regular es vacio
5. Metodo que llama al dispatch / la funcion para acceder a un metodo del reducer
		contiene el type que es el nombre del metodo
		y el payload que es el contenido que se manejara al correr el metodo
		el contenido del payload en este caso se obtiene del tag que lo llamo con el onclick
6. Onclick que manda a llamar al dispatch
		es un onclick que al ejecutarse manda la data del caracter
		en la funcion esta info sera mandada al reducer y de ahi al state final en favorite
7. map al contenido de favorite, listado del contenido de este array, 
		si no hay pues no se ve nada
*/
import React, { useState, useEffect, useReducer } from 'react'; // 1

const initialState = { // 2
  favorites: [],
}
  
const favoriteReducer = (state, action) => { // 3
  switch(action.type) {
    case 'ADD_TO_FAVORITE':
      return {
        ...state,
        favorites: [...state.favorites, action.payload]
      };
      default: return state;
  }
}

const Characters = () => {
  const [characters, setCharacters] = useState([]);
  const [favorite, dispatch] = useReducer(favoriteReducer, initialState); // 4

  useEffect(() => {
    fetch('https://rickandmortyapi.com/api/character/')
    .then(response => response.json())
    .then(data => setCharacters(data.results))
  }, [])

  const handleClick = favorite => {
    dispatch({ type: 'ADD_TO_FAVORITE', payload: favorite })
  }
  
  return (
    <div className='characters'>

      {favorite.favorites.map((favorite) => ( //7
        <li class='item' key={favorite.id}>
          {favorite.name}
        </li>
      ))}
      {characters.map((character) => (
        <div class='item' key={character.id}>
          <h2>{character.name}</h2>
          <button type='button' onClick={() => handleClick(character)}>add</button> //6
        </div>
      ))}
    </div>
  );
}

export default Characters;

No fue muy clara la clase, el profesor asume que conocemos la lógica de Redux y omite explicaciones de ciertas partes, me tocó buscar información externa para entender lo que él asume que sabemos.

Agregue un boton para eliminar de favoritos:

Aqui les dejo mi codigo:

Este es el componente de Characters:

import { useState, useEffect, useReducer } from 'react';
import Card from './Card';
import '../styles/Characters.css'

const initialState = {
    favorites: []
}

const favoriteReducer = (state, action) => {
    switch (action.type) {
        case 'ADD_TO_FAVORITE':
            const isExist = state.favorites.find(item => item.id === action.payload.id)
            if (isExist) return { ...state }
            return {
                ...state,
                favorites: [...state.favorites, action.payload]
            }

        case 'REMOVE_FAVORITE':
            return {
                ...state,
                favorites: state.favorites.filter(items => items.id !== action.payload)
            };

        default:
            return state;
    }
}

const Characters = () => {
    const [characters, setCharacters] = useState([]);
    const [favorites, dispatch] = useReducer(favoriteReducer, initialState);

    useEffect(() => {
        fetch('https://rickandmortyapi.com/api/character/')
            .then(response => response.json())
            .then(data => setCharacters(data.results))
    }, []);

    const handleClick = (favorite) => {
        dispatch({ type: 'ADD_TO_FAVORITE', payload: favorite })
    }

    const handleClickRemove = (id) => {
        dispatch({ type: 'REMOVE_FAVORITE', payload: id })
    }

    return (
        <div className="container">

            {favorites.favorites.length > 0 &&
                <div>
                    <h2>Favoritos</h2>
                    <ul className="row Characters">
                        {favorites.favorites.map(favorite =>
                            <li className="col-6 col-md-3" key={favorite.id}>
                                <Card {...favorite} isFavorite={true} handleClickRemove={handleClickRemove} />
                            </li>
                        )}
                    </ul>
                </div>
            }


            <h2>Personajes</h2>
            <ul className="row Characters">
                {characters.map(character =>
                    <li className="col-6 col-md-3" key={character.id}>
                        <Card {...character} handleClick={handleClick} />
                    </li>
                )}

            </ul>
        </div>
    )
}

export default Characters;

Y este es mi componente Card:

import '../styles/Card.css';


const Card = (props) => {

    const { id, name, status, species, gender, image, isFavorite, handleClick, handleClickRemove } = props;

    return (
        <div className="Card">
            <img src={image} alt={name} />
            <h5>Name: {name}</h5>
            <h5>Status: {status}</h5>
            <h5>Species: {species}</h5>
            <h5>Gender: {gender}</h5>
            {!isFavorite ?
                <button
                    type="button"
                    className='btn btn-primary'
                    onClick={() => handleClick({ id, name, status, species, gender, image })}
                >
                    Agregar a favoritos
                </button>
                :
                <button
                    type="button"
                    className='btn btn-danger'
                    onClick={() => handleClickRemove(id)}
                >
                    Eliminar de favoritos
                </button>
            }
        </div>
    )
}

export default Card

Aqui mi funcionalidad de favoritos:

Para el que le interese la lógica del reducer:

const initialState = {
	favorites: [],
};

const favoriteReducer = (state, action) => {
	switch (action.type) {
		case 'ADD_TO_FAVORITES':
			return {
				...state,
				favorites: [...state.favorites, action.payload],
			};
		case 'REMOVE_FROM_FAVORITES':
			return {
				...state,
				favorites: [
					...state.favorites.filter((favorite) => favorite !== action.payload),
				],
			};
		default:
			return state;
	}
};

const Characters = () => {
	const [characters, setCharacters] = useState([]);
	const [favorites, dispatchFavorites] = useReducer(
		favoriteReducer,
		initialState
	);

	useEffect(() => {
		fetch('https://rickandmortyapi.com/api/character/')
			.then((response) => response.json())
			.then((data) => setCharacters(data.results));
	}, []);

	const handleFavorite = (favorite) =>
		dispatchFavorites({
			type: !!findCharacterInFavorites(favorite)
				? 'REMOVE_FROM_FAVORITES'
				: 'ADD_TO_FAVORITES',
			payload: favorite,
		});

	const findCharacterInFavorites = (favorite) =>
		favorites.favorites.find((character) => character.id === favorite.id);

	console.log(favorites);
	return (
		<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
			{characters.map((character) => (
				<Card
					props={{
						...character,
						handleFavorite: () => handleFavorite(character),
						favorite: !!findCharacterInFavorites(character),
					}}
				/>
			))}
		</div>
	);
};

Les comparto como va quedando mi plataforma.

Quería validar que no se pudiera seleccionar el mismo item para el listado de favoritos asi que realicé esta condición en el reducer por si alguien le interesa.

const favoriteReducer = (state, action) =>{
  switch(action.type){
    case 'ADD_TO_FAVORITE':
      const validation = state.favorites.filter((favorite)=> action.payload.id === favorite.id)

      if(validation.length === 0){
        return {
          ...state,
          favorites:[...state.favorites, action.payload]
        }
      }
      else{
        return {...state}
      }
    default:
      return state
  }
}

Siento que el las explicaciones estan siendo muy superficiales… No todo se aprende a puro ejemplo

Asi esta quedando la APP, utilizando Tailwind CSS.

Realmente haber aprendido Redux anteriormente, me ayudó mucho a comprender esta clase!! Estructure el proyecto de una manera distinta, creando una carpeta **‘Character’ ** dentro de context, y esa carpeta contiene ‘CharacterContext.js’ que solamente contiene el createContext de Characters, luego creé CharacterReducer, que se encarga de manejar la lógica de los dispatchs, y por último, CharacterState, la que contiene el initialState y retorna el context de character con su provider y value, ademas agregue un custom hook con useContext, para no tener que importar siempre el contexto y el useContext, sino que solamente importo el custom hook donde necesite hacer uso del contexto.
Me esta quedando de la siguiente manera:

Así es como esta estructurado cada archivo dentro de la carpeta Character de context, luego solamente necesite encapsular el componente characters dentro de CharacterState, de la siguiente manera

Y luego dentro de Characters.jsx, ya tengo acceso al estado, atributos y funciones, pasadas en el provider del context. Utilizando el custom hook que creé.

Y así se ve gráficamente 😄

Hola, tengo una duda o pregunta…

const initialState = {
  favorites: [],
};
const favoriteReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TO_FAVORITE':
      return {
        ...state,
        favorites: [...state.favorites, action.payload],
      };

    default:
      return state;
  }
};

el código anterior no debería ir en un archivo aparte?

Les dejo un aporte:
-Evitar agregar dos personajes iguales

  const handleClick_AddFavourite = favourite =>{
    if (!favourites.favourites.includes(favourite)){
      dispatch( { type:'ADD_TO_FAVOURITE', payload : favourite})
    }else{
      alert('Ya has agregado este Personaje a tus Favoritos.')
    }

  };

Dejo un tutorial a lo visto en está clase:

Pasos para Instanciar un nuevo useReducer

https://www.linkedin.com/public-profile/settings?trk=d_flagship3_profile_self_view_public_profile

En los anteriores apartados hemos examinado la manera en la que iniciamos el estado inicial de nuestro reducer y la función que se encargará de establecer el cambio del estado de acuerdo al estado con el que ejecutemos la función reductora.

Ahora es momento de establecer la manera en la que llamaremos esta función reductora y como le indicamos que parte de nuestro estado se afecta.

  1. Importar el useReduce de la libreria de React
import { useReducer } from 'react'

Los pasos 2 y 3 los podemos definir por fuera del componente o en un archivo.js externo debido a que son lógica del programa.

  1. Definir el valor inicial de nuestro Reducer . Idealmente se usa para manejar múltiples estado a la vez, por lo tanto, lo encontraras como un objeto que contiene todos los estados que se requieren manejar. En este ejemplo, manejaremos solo una variable en su interior.
//Estado inicial del reducer
const initialState = {
  favorites:[],
}
  1. Debemos instanciar una función que se encargue de modificar el estado de nuestras variables.
const reducer = (state, action)=>{
  switch(action.type){
    case 'ADD_TO_FAVORITE':
      return{
        ...state, 
        favorites:[...state.favorites, action.payload]
      }
      default:
        return state;
  }
}
  • La función requiere un state y un action
  • El state sera el objeto con las variables que nosotros definamos como parte del objeto. En este caso solo tiene una variable.
  • La acción servira como la bandera que ejecute una u otra acción. En este caso, tenemos dos posibles acciones ‘ADD_TO_FAVORITE’ o un estado por defecto
  • El switch evalua el tipo de acción que mandado ha llamar la función reducer
  • Cada case debe retornar como primer elemento el anterior estado …state, y como segundo valor las variables que se modificaran en su llamado. En este ejemplo, solo se requiere modificar una sola variable, pero el estado puede contener múltiples variables.
  • Si tenemos una variable que tomara valores de manera dinámica este debera ser definido como action.payload
  • Las acciones dinámicas son cambios por una acción en el teclado (llenar un formulario), llamados a una api. En este ejemplo consumimos los valores del array según los datos de una api. Es por ello que se define un payload
  • Siempre debemos definir un caso por defecto en caso de que se llame la función reducer y no coincida con ninguna de las posibles acciones. Lo más aconsejable es regresar el mismo objeto de state sin ningún cambio retun state.

Los siguientes pasos requieren se ejecuten dentro del componente

  1. Dentro de nuestro componente debemos llamar instanciar el reducer
const [state, dispatch] = useReducer(reducer, initialState);
  • El state tomara como valor inicial el objeto que se definia dentro de initialState. Tambien podemos escribir directamente un objeto { favorites:[] } pero no es recomendable para mantener el orden en el código.
  • Por convensión se define como nombre de la función reductora dispatch. Cada vez que necesitemos modificar un estado mediante el reducer la llamaremos dispatch. Tomalo como un cambio del nombre de tu función reduce. El nombre dispatch no es obligatorio puedes llamarla nuevamente reducer u otro nombre que para tu lógica sea mas diciente.
  1. Si he explicado bien entiendes que cada variable dentro de tu state cambiara según una acción o evento diferente dentro de tú App. Cada una de estos eventos debe llamar una función que llame el reduce y le entregue los paramétros necesarios para que pueda interpretar que acción se produjo. Recuerda, en este caso solo tenemos una acción ‘ADD_TO_FAVORITE’ por lo tanto solo requerimos una función. Pero podemos tener muchas funciones más. La de este ejemplo se llama cuando damos click sobre el botón de agregar.
const hadleClick = character =>{
    dispatch({
      type: 'ADD_TO_FAVORITE',
      payload:character
    })
  }
  • Cuando damos click se ejecuta la función handleClick
  • Esta función tiene como parametro el objeto donde dimos click. En el ejemplo se lo llamo favorite, pero para mi es mas facil de entender si le paso todo el objeto character, puedes pasar solo el nombre u otra información que se adapte a tus necesidades.
  • Dentro de esta función llamaremos la función encargada de modificar el estado, es similar a llamar el setState pero por convención la llamamos dispatch
  • Ella debe recibir el type o tipo de acción que debe ejecutar y como en este caso la acción depende de los datos de una Api y es dinamico debemos enviar los datos como un payload.

En este punto ya tienes enlazada toda tu App mediante reducer. Ahora debes agregar un botón que llame a la función handleClick. Yo cambie un poco la manera en la que se hace el ejemplo en clases para crear cards con los personajes favoritos. Pero puedes adaptar la logica a tus propositos.

Dejo los códigos necesarios para mi ejemplo:

  1. Componente characters
mport React from 'react'
import { Character } from './Character';
import { useState, useEffect, useReducer } from 'react'

//Estado inicial del reducer
const initialState = {
  favorites:[],
}

//Funcion de manipulación del estado del reducer
const favoritesReducer = (state, action)=>{
  //Se valida que tipo de acción se ejecutara cuando se llame
  //El action puede tener diferentes valores
  switch(action.type){
    case 'ADD_TO_FAVORITE':
      return{
        ...state, 
        favorites:[...state.favorites, action.payload]
      }
      default:
        return state;
  }
}

function Characters() {
  const [characters, setCharacters] = useState([]);
 
  const [favorites, dispatch] = useReducer(favoritesReducer, initialState);

  const hadleClick = favorite =>{
    dispatch({
      type: 'ADD_TO_FAVORITE',
      payload:favorite
    })
  }
  useEffect(()=>{
    fetch('https://rickandmortyapi.com/api/character/')
        .then(response => response.json())
        .then(data => setCharacters(data.results)) 
  },[]);

  return (

    <div className='characters '>     

      {
        favorites.favorites.length>0?
        (
          <h2>Favorites</h2>
        ):
        (
          <h2>Not Faivorites</h2>
        )
      }

      {
      favorites.favorites.length>0? 
      (
        <div className='characters-container container'>
        {favorites.favorites.map((favorite, id) =>
         
          <Character 
            character = {favorite}
            key={id}
            hadleClick = {hadleClick}
          />)}
        </div>  
      )
      :
      (<h2>Sin favoritos</h2>) 
      
      }
      <h2>Characters</h2>
      <div className='characters-container container'>
      {

        characters.map((character, id) =>{
          return <Character 
            character = {character}
            key={id}
            hadleClick = {hadleClick}
          />
        })
      }
      </div>
    </div>
  )
}
export {Characters}
  1. Componente Character que crea las cards
import React from 'react'

export const Character = ({character, hadleClick}) => {
 
  return (

    <div className='character-card'>

      <figure className='figure-container'>
        <img src={character.image} alt="image character " />
      </figure>
      <div className="character-text">
          <h2>{character.name}</h2>
          <p><span>Location: </span>{character.location.name}</p>
          <p><span>Origin: </span>{character.origin.name}</p>
          <p><span>Status: </span>{character.status}</p>
      </div>        
      <button type='button'
        onClick={()=>hadleClick(character)}
      >
        Agregar a Favorito
      </button>
    </div>
    
  )
}
  1. Estilos
:root{
  --headin-principal: #04adc7;
  --headin-secundary: #abd268;
  --blue-dark: #1b1b41;
  --white:#FFFFFF;
  --black: #000000;
}
.App {
  background-color: #1b1b41;
}
body {
  margin: 0;
  padding: 0;
  background-color: #1b1b41;
}

html {
 box-sizing: border-box;
}

*, *:before, *:after {
 box-sizing: inherit;
}
img{
  max-width: 100%;
}
a{
  text-decoration: none;
}
li{
  list-style: none;
}
h2{
  color: var(--white);
  text-align: center;
}
.container{
  max-width: 968px;
  margin: 0 auto;
}

/* HEADER */
.header{
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 25px;

}
.headingPrincipal{
  text-align: center;
  color: var(--headin-principal);  
  font-size: 2rem;
  padding: 15px;
  margin: 0;
}
.button-container{
  display: flex;
  column-gap: 15px;
}
.button{
  padding: 5px 15px;
  font-size: 1rem;
  border-radius: 25px;  
  font-weight: 600;
}
.buttonHeader{
  text-align: center;
  border: 4px solid var(--headin-secundary);
  color: var(--blue-dark);
}

/*Characters*/

.characters{
  margin: 0px auto;
  padding:0 25px;
  
}
.characters-container{
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 25px;   
  margin: 20px;
}

@media(min-width: 650px){
  .characters-container{
    flex-direction: row;
    flex-wrap: wrap; 

    
  }
}

@media(min-width: 968px){
  .characters-container{
    grid-template-columns: repeat(3, 1fr);  
  }
}
.characters-container h2{
  display: block;
  
}

.character-card{  
  max-width: 320px;
  min-width: 200px;
  width: 280px;
  height: 550px;
  display: flex;
  flex-direction: column;
  align-items: center;
  border:1px solid var(--headin-secundary);
  border-radius: 15px;
  overflow: hidden;
}
.figure-container{
  margin: 0;  
  width: 100%;
  height: 100%;
  height: auto;
  overflow: hidden;
}
.character-text{
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: 10px;
}
.character-card h2{
  color: var(--headin-secundary);
  margin: 0;
  padding: 10px;
  text-align: center;
  align-self: center;
}
.character-card p{
  color: var(--white);
  margin: 0;
  padding: 5px;
}
.character-card span{
  color: var(--headin-secundary);
}

Básicamente, el useReducer es un eventHandler que, al ser llamado, realiza la acción que nosotros le decimos que haga; en esta clase fue añadir un elemento más al initialState que comienza vacío, y que cada que le damos click, llama al “eventHandler” (reducer), le indíca que la acción será ADD_TO_FAVORITE.
El hook useState es simplemente el estado clásico de un componente.

Realmente, si no entendiste lo suficiente esta clase no es algo de lo que te debes de preocupar demasiado, ya que Redux y useReducer comprenden varios conceptos de programación funcional inmutable y pura. Junto a los conceptos de reducer. actions, store, etc., y es mejor comenzar entendiendo eso primero.

he leído varios comentarios y tengo una opinión similar. L as explicaciones se quedan cortas para explicar los hooks

Les comparto mi implementación del use reducer ya con CSS y un poco mas de lógica para agregar y quitar personajes de los favoritos

El codigo con algunas notas lo pueden encontrar en mi repositorio https://github.com/Hiddenberg/react-hooks-platzi/tree/v0.1

Este es mi reducer, le agregué una decisión para evitar agregar el mismo personaje favorito más de una vez. Lo hice con filter… Agradecería me hagan saber si hay una forma mejor de evitar que se agregue un personaje favorito más de una vez.

<code> 
const initialState = {
  myFavorites: []
};

const favoriteReducer = (state, action) => {
  switch (action.type) {
    case "ADD_TO_FAVORITES":{
      if(state.myFavorites.filter(fav => fav.id === action.payload.id).length){
        return state;
      } else {
        return {
          ...state,
          myFavorites: [...state.myFavorites, action.payload]
        }
      }
    }
    default:
      return state;
  }
}

function App() {
   const [personajes, setPersonajes] = useState([]);
   const [favorites, dispatch] = useReducer(favoriteReducer, initialState);

  useEffect(() => {
     /************
      * Se cargan los datos de los personajes de la api 
      * Rick and Morty
      * ...
      * ...
      * }, []);
      *************/
  
    const handleAddToFavorite = (favorite) => {
    dispatch({ type: "ADD_TO_FAVORITES", payload: favorite});
  }

  return (
    <div className="personajes">
      <h1>Personajes de Rick and Morty</h1>
      <hr />
      {favorites &&
        <div className="favoritos">
          <h2>Personajes favoritos</h2>
          <ul>
            {favorites.myFavorites.map(favorite => {
              return (
                <li key={favorite.id}>
                  {favorite.name}
                </li>
              )
            })}
          </ul>
          <hr />
        </div>
      }
      
      {personajes.map(personaje => (
        <h2 key={personaje.id}>
          {`${personaje.name} - ${personaje.type ? personaje.type : "Unknown"} `}
          <button type="button" onClick={() => handleAddToFavorite(personaje)}>
            Agregar a favoritos
          </button>
        </h2>
      ))}
    </div>
  );
}

export default App;
</code>

No explican casos o usos mas comunes. muy basico!! para ser un curso profesional

Hasta ahora me va así (No soy diseñador :c, se que se ve mal)

si les pasó como a mi que es la primera vez que ven useReducer, action, dispatch etc este video complementara un poco esta clase.

video

espero les ayude 😃

Levante la mano a quien le pareció más sencillo el useContext ✋

Este video me ayudó a entender un poco más.
Al parecer useState es para cosas más simples y useReducer es para cosas más complejas

Muy interesante la clase realmente ! si quieren profundizar un poco mas en este Hook aqui les dejo el link a la documentación.

useReducer: como useState, pero más escalable


useReducer implementa una nueva forma más amigable de usar el estado. Si haz trabajado con Redux te vas a familiarizar con el contexto ya que tiene:

  • Estado inicial
  • Reducer
  • Dispatch (para enviar la información)

no lo se me parece mejor usar redux

Por muy por encima que esté haciendo los estilos y funciones básicas para que nada se rompa, me está gustando cómo va quedando


Lo malo es que recién ahora me di cuenta que me volvió a pasar que trabajo en el diseño para que todo ande y esté bonito (Para lo que suelo hacer), y luego resulta que he estado trabajando todo el tiempo con el zoom modificado 😅

useReducer es un hook que permite agregar un reducer a un componente.

La sintaxis de useReducer es:

const [state, dispatch] = useReducer(reducer, initialState);

Donde:

  • state: es el valor actual del estado que mantiene el hook.
  • dispatch: es una función que nos permite actualizar el estado que mantiene el hook. Recibe como parámetro el action.
  • initialState: es el valor inicial del estado. Puede ser de cualquier tipo.

Un reducer es una función que toma el estado actual y una acción como argumentos, y devuelve el estado modificado u original según indique la acción.

Una acción o action es un objeto que describe que ha ocurrido en la app y como debe cambiar el estado. Es una acción del usuario en la UI. Puede tener cualquier forma, pero normalmente tiene dos propiedades:

  • type: Es una propiedad obligatoria. Indica el tipo de acción.
  • payload: Es una propiedad opcional. Añade información adicional para modificar el estado.

El flujo de useReducer se puede describir así:

  1. El usuario interactúa con el componente y provoca un evento especial, como hacer clic en un botón.
  2. El evento llama a una función que envía una acción al dispatch.
  3. El dispatch recibe la acción y la pasa al reducer.
  4. El reducer ejecuta la lógica correspondiente al tipo de acción (propiedad type) y devuelve el nuevo estado. El nuevo estado reemplaza al estado anterior en el hook useReducer.
  5. React detecta el cambio de estado y vuelve a renderizar el componente con el nuevo estado. Finalmente, el componente muestra el contenido actualizado en la pantalla.

Llegamos, implementamos useReducer y chimpún. Es que no se explica bien en ningún momento porqué. Qué es. Qué problema viene a solucionar. Por qué no puedo hacer esto mismo con las herramientas que ya tenía antes. Así no se aprende, tan sólo hacemos cosas porque sí.

¿Qué es un reducer?
Son una herramienta que nos permite declarar todos los posibles estados de nuestra App para llamarlos de forma declarativa. Necesitan 2 objetos esenciales: los estados compuestos y las acciones.

Los estados compuestos:

Son un objeto donde van a vivir como propiedades todos nuestros estados

Acciones:

Responsables, al ser disparados, de pasar de un estado a otro.
Este objeto tiene 2 propiedades: action type y action payload.

Action type:

Define el nombre clave para encontrar el nuevo estado.

Action payload:

Es opcional e importante con estados dinámicos. Un estado es dinamico cuando depende del llamado de un API, de lo escrito por el usuario en un input, etc. Estos son estados dinámicos y los recibimos con un payload.
Flujo de trabajo:

Definimos distintos tipos de acciones con type y payload.
Enviamos estas acciones a nuestro reducer.
El reducer define los posibles estados por donde pasara nuestra App.

Con action type elegimos cual de esos estados queremos disponer con el cambio o evento del usuario.

Con action payload damos dinamismo a dicho estado. Será el mismo estado pero le daremos características especiales

El hook de useReducer es una excelente manera de administrar objetos de estado complejos y transiciones de estado, para manejar estados más simples se usa useState

GENTE QUE PIENSA QUE EL PROFESOR NO EXPLICA:

Este es un curso nivel avanzado de react hooks, practicamente se está dando un repaso de todo lo que se vió en la ruta de React.js, para entender mejor, YA deben saber los conceptos de cursos anteriores, no esperen un curso “profesional” si no tienen la capadidad de comprenderlo.

Entendí un poco mejor con este video:
.
https://www.youtube.com/watch?v=Qisq6I_buBo&list=PLvq-jIkSeTUZ5XcUw8fJPTBKEHEKPMTKk&index=84
.
Explican el paso a paso y como funciona.

No se entiende, no tiene mucho sentido ver una clase en donde no se explique en detalle para que sirve cada Hook. Falta mucha profundizacion, teniendo en cuenta casos de usos.

las buenas practicas sugiere crear carpetas para separar el reducer, las actions y opcionalmente los types.

Ademas, si has trabajo con Vuex de Vue, useReducer se hace muy facil ya que recuerda a las actions de Vuex

Qué buena clase, super clara!

Aquí podrían revisar mas a fondo useReducer, implementando best practices:

https://youtu.be/kK_Wqx3RnHk

Me gusta, pero lo siento muy desordenado. Con Redux queda más claro el código o es mi percepción?

En este Curso: https://platzi.com/clases/react-estado/
Se puede ver a detalle este Hook. Les recomiendo de la clase 12 a la 15. 👌

Hasta el momento voy asi, le añadi las funcionalidad de no repetir favorito y de eliminarlo.
![](

Para los que no entendieron que es useReducer() les dejo este ejemplo de como se puede hacer lo mismo que con useState():
Este en un custom hook donde empleo useReducer() para simular a useState()

import React, {useReducer} from "react"

function reducer( state, action ) {
  switch( action.type ) {
    case 'toggle': return !state
    default: throw new Error()
  }
}

export function useMyState( initialState ){
  const [ state, dispatch ] = useReducer( reducer, initialState )
  return{ state, dispatch }
}

Este es el componente donde haré uso del custom hook anterior. Su funcionamiento es básico, al presionar el botón quiero cambiar el estado para mostrar el saludo o quitarlo.

import React from "react"
import { useMyState } from "../hooks/useMyState"

export function MyComponent(){
  const { state: sayHello, dispatch: setSayHello } = useMyState( false )

  return(
    <div>
      <p>{ sayHello ? "Hello" : "" }</p>
      <button onClick={ () => setSayHello( { type: 'toggle'} ) }>Greeting</button>
    </div>
  )

}

useReducer() maneja el estado como lo hace useState() y retorna un array con el estado y la función que modifica a ese estado, la cosa está en que a esa función a la que llamamos dispatch, solo se encarga de entregar un objeto action a la función reducer() que es la que va a modificar el estado.

onClick={ () => setSayHello( { type: 'toggle'} ) }

useReducer() recibe como parámetros la función reducer() y un valor inicial del estado(como mismo haciamos con useState() ):

 const [ state, dispatch ] = useReducer( reducer, initialState )

La función reducer se declara afuera del componente o hook, separando el manejo del estado de la lógica de renderizado del componente, esta es la gran diferencia con useState(). En esencia esta función no modifica el estado anterior, si no que crea uno nuevo y React compara ambos y si hay un cambio re-renderiza el componente (inmutable)
La lógica es la de un switch/case que va a regresar un valor de estado en función de los valores de las propiedades del objeto action.


Una analogía rapida sería:
-el evento o fetch a una api es el que selecciona una carta para enviar
-el objeto action es la carta( pueden haber más sobre el escritorio )
-el dispatch es el mensajero
-la función reducer es a quien va dirigida la carta y quien la lee ( al modificar el estado demuestra que la leyó )

Tremendo, nunca lo usé a este hook pero ahora lo voy a empezar a implementar! 😄

Se agregan y se elimina favoritos y se valida no más de 12 favoritos.

import React, { useState, useEffect, useContext, useReducer} from 'react'
import ThemeContext from '../context/ThemeContext';

const initialState = {
    favorites: []
}

const  favoriteReducer = (state, action) =>{
    switch (action.type) {
        case 'ADD_TO_FAVORITE':
            const validation = state.favorites.filter((favorite)=> action.payload.id === favorite.id)
            if(validation.length === 0){
                if (state.favorites.length < 12){
                    return{
                        ...state,
                        favorites: [ ...state.favorites, action.payload ],
                    };
                }
            }
            else{
                return {
                    ...state,
                    favorites: [
                        ...state.favorites.filter( favorite => favorite.id !== action.payload.id )
                    ]
                };
            }break
        case 'DELETE_TO_FAVORITE':
            return{
                ...state,
                favorites: [
                    ...state.favorites.filter( favorite => favorite.id !== action.payload.id )
                ]    
                };
        default:
            return state;
    }
}




const Characters = () => {
    const [characters, setCharacters] = useState([]);
    const [favorites, dispatch] = useReducer(favoriteReducer, initialState);

    useEffect(() => {
        fetch("https://rickandmortyapi.com/api/character/")
        .then(response => response.json())
        .then(data => setCharacters(data.results))

    },[]);
    const {darkMode} = useContext(ThemeContext);
 
    const handleClick = favorite => {
        dispatch({ type: 'ADD_TO_FAVORITE', payload: favorite})

    }
    
    const removeFavs = (favorite) => {
        dispatch({ type: "DELETE_TO_FAVORITE", payload: favorite });
    };

    return (
        <div className="container mt-5">
            <div className="row">
                {favorites.favorites.map(favorite =>(
                    <>
                    <div className="col-1 mb-5" key={favorite.id} >
                        <img className="rounded-circle favoriteCircle" src={ favorite.image } alt="" />
                        <button type="button" onClick= {() => removeFavs(favorite)} >Eliminar</button>
                    </div>
                    </>
                ))}
            </div>
            <div className="row">
                {characters.map(character => (
                    <>
                    <div className="col-4 mb-5">
                    <div className={`border p-3 rounded cardLight ${darkMode ? "border-warning": "border-dark"}`} key={character.id}>
                        <img src={ character.image } alt="" />
                        <h4>{ character.name }</h4>
                        <h6>{ character.status } - { character.species }<small> ({ character.gender }) </small></h6>
                        <button type="button" onClick= {() => handleClick(character)} >{handleClick ? "Agregar a favoritos" : "Eliminar"}</button>
                    </div>
                    </div>
                    </>
                ))}
            </div>
        </div>
    )
}

export default Characters



useReducer

💡 INFO
Función alternativa a useState que se vincula a un reducer, de Redux, retornando una referencia al state y a la función dispatch.

.
useReducer se suele preferir por sobre useState cuando el state presenta una lógica compleja:

  • Un encadenamiento de estados dependientes uno, secuencialmente, del otro.
  • Inyectar o hidratar un state local al state manejado por Redux.

.

Estructura básica

const [state, dispatch] = useReducer(reducer, initialArg, funcInit);

Su API, expone 3 argumentos donde:

  1. reducer - el cuál deberá ser del tipo (state, action) => newState
  2. inicitalArg - ya sea proporcionado por el mismo componente o heredado por el Redux
  3. funcInit - función para delegar el proceso de inicialización, el comportamiento será Lazy

.

Redux Tool Kit (RTK)

RTK es una nueva generación de herramientas proporcionadas por Redux para actualizar su API abstraída para las aplicaciones modernas de React.
Para nuestro ejercicio, podemos utilizar la siguiente estructura de código:

// store/favorites.jsx
import { createSlice } from "@reduxjs/toolkit";

export const initialFavorites = [];

const slice = createSlice({
  name: "favorites",
  initialState: initialFavorites,
  reducers: {
    addFavorites: (state, action) => {
      state.push(action.payload);
    }
  }
});

export const { addFavorites } = slice.actions;
export const favoritesReducer = slice.reducer;

⚡️NOTA
Es necesario instalar react-redux y @reduxjs/toolkit

.
De lo anterior podemos decir que nuestro estado no será global, solamente hidratado desde el estado local del componente.

Una vez creado lo anterior, obtenemos las referencias a nuestro componente:

import React, { useState, useEffect, useReducer } from "react";

import {
  favoritesReducer,
  initialFavorites,
  addFavorites
} from "../store/favorites.jsx";

Utilizarlo como local store:

const [favorites, dispatch] = useReducer(favoritesReducer, initialFavorites);
 
const handleClick = (favorite) => {
    dispatch(addFavorites(favorite));
  };

  return (
      {characters.length > 0 &&
        characters.map((character) => (
          <div key={character.id}>
            <h2>{character.name}</h2>
            <button type="button" onClick={() => handleClick(character)}>
              Agregar a Favoritos
            </button>
          </div>
        ))}
    </div>
  );

Link de códigos

Para que no agregue 2 veces el mismo personaje(Character):

const favoriteReducer = (state, action) => {
    switch (action.type) {
        case 'ADD_TO_FAVORITE':
            return {
                ...state,
                favorites: [...state.favorites.filter(item => item.id !== action.payload.id),
                            action.payload],
            };
        default:
            return state;
    }
}

Entonces usamos use Reducer cuando queremos manejar state dentro del componente de una mejor forma, es decir este no es como Redux que con connect se pude traer a los props, este se declara y se usa dentro del componente y sus hijos, no?

Les dejo un aporte de Juan David Castro Gallego en donde explica con bastante claridad como funciona. También abre el espectro mostrando diferentes posibilidades.
No se si ya da algún curso pero creo que sería un excelente profesor.
https://platzi.com/blog/usestate-vs-usereducer/

Diferencias entre useReduce y Redux: https://www.robinwieruch.de/redux-vs-usereducer
En pocas palabras con useReducer estamos limitados a emitir actions solo a un reducer, además de que no nos permite realizar side-effects, como lo es el data-fetching, y tambien no nos es posible seguir el comportamiento de nuestro estado antes o despues de emitir una acción. Todo lo anterior si es posible con Redux. 😉

Asi voy:

Hermoso!!!

por que es una mala práctica usar el index? hay casos que no tenemos este id presente en la api que otra alternativa hay?

No me quedó muy claro el uso de esta linea de codigo, que se está haciendo? que es el payload?
...state,favorites:[...state.favorites,action.payload]

Saludos!

Así lo hice yo, usando un poco de la lógica de Alexis Ulises y agregandole algunas validaciones por pura estética. Intenté tambien pasar todo el HTML a un componente presentacional pero se me hizo un lio de this y de props, si alguien lo hizo comparta el codigo!

import React, { useState, useEffect, useReducer } from "react";

import "./styles/Characters.css";

const initialState = {
  favorites: [],
};

const favoriteReducer = (state, action) => {
  switch (action.type) {
    case "ADD_TO_FAVORITE":
      return {
        ...state,
        favorites: [...state.favorites, action.payload],
      };

    case "REMOVE_FROM_FAVORITE":
      return {
        ...state,
        favorites: [
          ...state.favorites.filter((favorite) => favorite !== action.payload),
        ],
      };

    default:
      return state;
  }
};

const Characters = () => {
  const [characters, setCharacters] = useState([]);
  const [favorites, dispatch] = useReducer(favoriteReducer, initialState);

  useEffect(() => {
    fetch("https://rickandmortyapi.com/api/character/")
      .then((response) => response.json())
      .then((data) => setCharacters(data.results))
      .catch((error) => console.warn("Error in fetch:", error));
  }, []);

  const handleFavorite = (favorite) => {
    dispatch({
      type: !!isCharacterInFavorites(favorite) ? "REMOVE_FROM_FAVORITE" : "ADD_TO_FAVORITE",
      payload: favorite,
    });
  };

  const isCharacterInFavorites = (favorite) =>
    favorites.favorites.find((character) => character.id === favorite.id);

  var favIcon = "https://icons.iconarchive.com/icons/hopstarter/soft-scraps/256/Button-Favorite-icon.png";
  var delIcon = "https://uxwing.com/wp-content/themes/uxwing/download/01-user_interface/red-x.png";
  console.log("Favorites: ", favorites);

  return (
    <>
      <div className="Favorites">
        <h2>{ (favorites.favorites.length === 0) ?  "Add your favorite characters here!" : "Favorites:"}</h2>
          {favorites.favorites.map((favorite) => (
            <li
              key={favorite.id}
              className="FavList"
            >
              <button type="button" onClick={() => handleFavorite(favorite)}>
                <img
                    src={delIcon}
                    alt=""
                    className="Icon delete"
                  />
              </button>
              {favorite.name}
            </li>
          ))}
      </div>

      <div className="Characters">
        {characters.map((character) => (
          <div className="Character" key={character.id}>
            <h2> {character.name} </h2>

            <img src={character.image} alt="Character" />

            <h4> Type: {character.species} </h4>
            <h4> Gender: {character.gender} </h4>
            <h4> Origin: {character.origin.name} </h4>

            <button type="button" onClick={() => handleFavorite(character)}>
              <img
                className="Icon"
                src={!!isCharacterInFavorites(character) ? delIcon : favIcon}
                alt=""
              />
            </button>
          </div>
        ))}
      </div>
    </>
  );
};

export default Characters;

Así va quedando, me gusta mucho el curso!

El useReducer es una alternativa al useState, sirven para lo mismo. La recomendación es que cuando vamos a usar estados simples, que pocas cosas van a cambiar, está bien usar el useState. Pero cuando vamos hacer operaciones más complejas con muchas acciones, es mejor usar un reducer.

No entiendo del todo este del reducer pero intenté hacer que a la lista no se pueda ingresar un elemento que ya esté en la lista… si alguien lo ve y me una sugerencias si está bien o no se lo agradecería

import { useState, useEffect, useReducer } from 'react'

const initialState = {
    favorites: []
}

const favoriteReducer = (state, action) => { //agregará a favoritos
    console.log(state.favorites, action)
    debugger
    switch (action.type) {
        case 'ADD_TO_FAVORITE':
            if (state.favorites.length === 0) {
                return {
                    ...state,
                    favorites: [...state.favorites, action.payload]
                }
            } else if (state.favorites.some(favorite => favorite === action.payload)) {
                return state
            } else {
                return {
                    ...state,
                    favorites: [...state.favorites, action.payload]
                }
            }

        case 'REMOVE_TO_FAVORITE':
            return {
                ...state,
                favorites: [...state.favorites.filter(favorite => favorite !== action.payload),]
            }
        default:
            return state
    }
}

const Characters = () => {

    const [characters, setCharacters] = useState([])
    const [favorites, dispatch] = useReducer(favoriteReducer, initialState)

    useEffect(() => {
        fetch('https://rickandmortyapi.com/api/character')
            .then(response => response.json())
            .then(data => { setCharacters(data.results) })
    }, [])

    const handleClick = favorite => {
        dispatch({ type: 'ADD_TO_FAVORITE', payload: favorite })
    }

    const handleQuitarClick = favorite => {
        dispatch({ type: 'REMOVE_TO_FAVORITE', payload: favorite })
    }

    return (
        <div className="Characters">

            {favorites.favorites.map(favorite => (
                <li key={favorite.id} >
                    {favorite.name}
                    <button type="button" onClick={() => handleQuitarClick(favorite)}>Quitar</button>
                </li>
            ))}

            {characters.map(item => (
                <div key={item.id} className="CharacterTarget">
                    <h2>{item.name}</h2>
                    <img src={item.image} alt="" />
                    <h3>{item.species}</h3>
                    <button type="button" onClick={() => handleClick(item)}>Agregar a Favoritos</button>
                </div>
            ))}

        </div>
    )
}

export default Characters

En el minuto 7:41 menciona que es mala práctica utilizar el index del arreglo, ¿Porqué? ¿Es mala práctica usar el segundo parámetro de la función map?

Aquí mi avance

Interesante su funcionamiento, muy similar a Redux
Para los que hicimos el curso de Redux esto es piece of cake

Si alguien usa WSL con Ubuntu y no les detecta los cambios cuando corren npm run start deben agregar en la raiz del proyecto un archivo llamado .env y poner CHOKIDAR_USEPOLLING=true y listo pueden volver a correr npm run start 😄

Oscar simplemente 20/10 y mas porque es de Coahuila, siuuuu

Resaltar items seleccionados como favoritos
Agrega y remueve favoritos

//Parte inicial de nuestro archivo Cryptos.jsx, mostramos los reducers

import React, { useState, useEffect, useReducer } from "react";
import _ from "lodash";

import CyptoCard from "./Crypto";

const initialState = {
    favorites: [],
};
const favoritesReducer = (state, action) => {
    switch (action.type) {
        case "ADD_TO_FAVS":
            return {
                ...state,
                favorites: [...state.favorites, action.payload],
            };
        case "REMOVE_FROM_FAVS":
            const newFavorites = _.pull(state.favorites, action.payload);
            return {
                ...state,
                favorites: newFavorites,
            };
        default:
            return state;
    }
};
// continuación de Cryptos.jsx
const Cryptos = () => {
    const [cryptos, setCryptos] = useState([]);
    const [state, dispatch] = useReducer(favoritesReducer, initialState);
    useEffect(() => {
        fetch("https://api.nomics.com/v1/currencies/ticker?key=123456789&per-page=50&page=1")
            .then((res) => res.json())
            .then((data) => setCryptos(data));
    }, []);

    const addToFavs = (newFavorite) => {
        dispatch({ type: "ADD_TO_FAVS", payload: newFavorite });
    };
    const removeFromFavs = (favoriteToBeRemoved) => {
        dispatch({ type: "REMOVE_FROM_FAVS", payload: favoriteToBeRemoved });
    };
    const { favorites } = state
    return cryptos.map((crypto) => {
        const isFav = _.includes(favorites, crypto.symbol);
        return <CyptoCard
            isFav={isFav}
            {...crypto}
            key={crypto.id}
            addToFavs={addToFavs}
            removeFromFavs={removeFromFavs}
        />;
    });
};
export default Cryptos;

Y ahora miraremos el componente que renderiza cada elemento

import React, { useContext } from "react";
import ThemeContext from "../context/ThemeContext";

const Crypto = ({ logo_url, currency, name, symbol, price, isFav, addToFavs, removeFromFavs }) => {
    const { darkModeActive } = useContext(ThemeContext);
    return (
        <div className={`card crypto-card m-3 ${isFav ? "is-fav" : ""}`}>
            <div className="card-content">
                <div className="media">
                    <div className="media-left">
                        <figure className="image is-48x48">
                            <img src={logo_url} alt={`${currency} logo`} />
                        </figure>
                    </div>
                    <div className="media-content">
                        <p className="title is-4">{name}</p>
                        <p className="subtitle is-6">{symbol}</p>
                    </div>
                </div>

                <div className="content is-flex is-flex-direction-row is-justify-content-space-between">
                    <span className={`tag mr-1 ${darkModeActive ? "is-dark" : "is-white"}`}>USD {parseFloat(price).toFixed(2)}</span>
                    <button
                        onClick={() => (isFav ? removeFromFavs(symbol) : addToFavs(symbol))}
                        className={`button  is-small is-danger  ${darkModeActive ? "is-outlined" : ""}`}
                    >
                        <span>{isFav ? `Remover de` : `Agregar a`} Favoritos</span>
                        <span className="icon is-small">
                            <i className="mdi mdi-heart"></i>
                        </span>
                    </button>
                </div>
            </div>
        </div>
    );
};

export default Crypto;

Resultado

Dejo mi aporte de ir un poco más y realizar un botón que elimine al que se encuentra en la lista de favoritos. NOTA: NO TIENE VALIDACIÓN DE VERIFICAR SI EL SELECCIONADO YA SE ENCONTRABA EN FAVORITOS.

import React, { useState, useEffect, useReducer } from 'react'

const initialState = {
    favoritesCharacters: []
};

const favoriteReducer = (state, action) => {
    switch(action.type) {
        case 'ADD_TO_FAVORITES':
            return {
                ...state,
                favoritesCharacters: [...state.favoritesCharacters, action.payload]
            };

        case 'DELETE_FROM_FAVORITES':
            return {
                ...state,
                favoritesCharacters: [
                    ...state.favoritesCharacters.filter( item => item.id !== action.payload.id )
                ]
            };
        
        default:
            return state;
    }
}

const Characters = () => {
    const [characters, setCharacters] = useState([]);
    const [favorites, dispatch] = useReducer(favoriteReducer, initialState)

    const getCharacters = async () => {
        const response = await fetch('https://rickandmortyapi.com/api/character/');
        const data = await response.json();
        const results = data.results;
        setCharacters(results);
    }

    useEffect(() => {
        getCharacters();
    }, [])

    const handleClickOnFavorite = favorite => {
        dispatch({type: 'ADD_TO_FAVORITES', payload: favorite});
    }

    const handleClickOnDeleteFavorite = character => {
        dispatch({type: 'DELETE_FROM_FAVORITES', payload: character})
    }

    return (
        <div className="">
            <div className="Characters">
                {
                    favorites.favoritesCharacters.length > 0 && (
                        <>
                            <div className="Characters-title">
                                <h2>Favorites Characters</h2>
                            </div>
                            <div className="Characters-list">
                                {
                                    favorites.favoritesCharacters.map( favoriteCharacter => (
                                        <div className="Character" key={favoriteCharacter.id}>
                                            <h2>{ favoriteCharacter.name}</h2>
                                            <figure>
                                                <img src={favoriteCharacter.image} alt={favoriteCharacter.species} />
                                                <figcaption>
                                                    <p><b>Gender: </b> {favoriteCharacter.gender}</p>
                                                    <p><b>Origin: </b> {favoriteCharacter.origin.name}</p>
                                                    <p><b>Location: </b> {favoriteCharacter.location.name}</p>
                                                    <p><b>Specie: </b> {favoriteCharacter.species}</p>
                                                    <p><b>Status: </b> {favoriteCharacter.status}</p>
                                                    <p><button type="button" onClick={() => handleClickOnDeleteFavorite(favoriteCharacter)}>Delete</button></p>
                                                </figcaption>
                                            </figure>
                                        </div>
                                    ))
                                }
                            </div>
                        </>
                    )
                }
            </div>
            <div className="Characters">
                <div className="Characters-title">
                    <h2>Characters</h2>
                </div>
                <div className="Characters-list">
                    {
                        characters.map( (character) => (
                            <div className="Character" key={character.name}>
                                <h2>{ character.name}</h2>
                                <figure>
                                    <img src={character.image} alt={character.species} />
                                    <figcaption>
                                        <p><b>Gender: </b> {character.gender}</p>
                                        <p><b>Origin: </b> {character.origin.name}</p>
                                        <p><b>Location: </b> {character.location.name}</p>
                                        <p><b>Specie: </b> {character.species}</p>
                                        <p><b>Status: </b> {character.status}</p>
                                        <p><button type="button" onClick={() => handleClickOnFavorite(character)}>Add to favorites</button></p>
                                    </figcaption>
                                </figure>
                            </div>
                        ))
                    }
                </div>
            </div>
        </div>
    );
};

export default Characters;

ESPERO LES PUEDA AYUDAR

Vaya, este curso lo necesitaba hace 2 años cuando inicie mi aplicación de React. Excelente!

Aquí mi reducer con la función de eliminar:

const favoritesReducer = (state, action) => {
    switch (action.type) {
        case 'ADD_TO_FAVS':
            return {
                ...state,
                favorites: [...state.favorites, action.payload]
            }
        case 'REMOVE_FROM_FAVS':
            return {
                ...state,
                favorites: [...state.favorites.filter(c => c.id !== action.payload.id)]
            }
        default:
            return state;
    }
}

Hola, estoy siguiendo este proyecto en Nextjs. Les dejo mi repositorio:

https://github.com/danyel117/platzi-conf-store