Cómo usar Redux para administrar el estado

Cómo usar Redux como una tienda global para administrar el estado de la aplicación. Aprenda a interactuar y administrar su tienda Redux en una interfaz de usuario basada en React utilizando componentes basados ​​en clases y componentes funcionales a través de ganchos.

Primeros pasos

Para este tutorial, usaremos CheatCode Next.js Boilerplate como punto de partida. Las rutas que se muestran arriba de los bloques de código a continuación se asignan al repositorio de este tutorial en Github. Para acceder a ese repositorio, haga clic en el botón "Ver en Github" arriba (nota:se requiere una suscripción a CheatCode Pro para acceder a los repositorios de tutoriales sobre CheatCode).

Para comenzar, clone una copia de Next.js Boilerplate de Github:

git clone [email protected]:cheatcode/nextjs-boilerplate.git

Y luego ejecuta:

cd nextjs-boilerplate && npm install

A continuación, opcionalmente, si se salta el modelo estándar o se construye como parte de otra aplicación, puede instalar redux y react-redux :

npm i react react-redux

Comprender el flujo de datos en Redux

El propósito de Redux es crear una tienda (un lugar para guardar sus datos) a la que se puede acceder a través de toda su aplicación. Por lo general, Redux se usa para crear un global tienda, o una tienda a la que puede acceder toda su aplicación (a diferencia de una página o componente específico).

const store = createStore();

Cuando se crea una tienda usando el createStore() función exportada desde el redux paquete que instalamos arriba, pasó otra función conocida como reductor . Un reductor es responsable de decidir cómo modificar el estado actual contenido en una tienda en respuesta a alguna acción que se lleva a cabo.

const store = createStore((state = {}, action) => {
  switch (action.type) {
    case "LOGIN":
      return {
        ...state,
        authenticated: true,
        user: action.user,
      };
    case "LOGOUT":
      return {
        ...state,
        authenticated: false,
        user: null,
      };
    default:
      return {
        ...state,
      };
  }
}, {});

Aquí, hemos pasado una función reductora de ejemplo a createStore() . Hay algunas cosas a las que prestar atención aquí.

Primero, queremos notar que una función reductora toma dos argumentos:state y action (el state = {} la sintaxis aquí es que establezcamos un valor predeterminado para state en el caso de que su valor sea nulo o indefinido).

El state argumento aquí contiene el actual estado de la tienda Redux. El action El argumento contiene la acción actual que se está enviando y que hará cambios en el estado de la tienda.

Ahora, donde las cosas se ponen interesantes, y probablemente confusas, es cuando empezamos a modificar nuestro estado en función de una acción. La sintaxis que probablemente se ve rara aquí es switch() {} part (conocido técnicamente en JavaScript como declaración de cambio de caso):

(state = {}, action) => {
  switch (action.type) {
    case "LOGIN":
      return {
        ...state,
        authenticated: true,
        user: action.user,
      };
    case "LOGOUT":
      return {
        ...state,
        authenticated: false,
        user: null,
      };
    default:
      return {
        ...state,
      };
  }
}

Aquí, hemos extraído la función reductora de arriba en aras de la claridad (el mismo código exacto). La primera parte que queremos ver es el switch (action.type) {} . Lo que esto está diciendo es "toma el action.type e intente encontrar una coincidencia en esta declaración".

Así es como funciona una declaración de cambio de caso. La idea es que, dado algún valor (action.type en este caso), intente encontrar un case sentencia cuyo propio valor es igual al valor pasado al switch .

Entonces, aquí, si asumimos que el valor almacenado en action.type es igual a "LOGOUT" , el segundo case declaración aquí—case "LOGOUT" —coincidirá con el código que sigue al : dos puntos después del case será ejecutado.

En este ejemplo, devolvemos un objeto JavaScript que representará la copia actualizada del estado. Decimos que está actualizado porque el valor que devolvemos de nuestro interruptor, y en última instancia, nuestra función de reducción, es una copia del estado original (recuerde, este es el primer argumento pasado a nuestra función reductora). Decimos que es una copia porque aquí estamos usando el ...state sintaxis que se conoce como sintaxis extendida en JavaScript.

const state = { food: 'Apple', animal: 'Red Panda' };

console.log(state);

// { food: 'Apple', animal: 'Red Panda' }

const newState = {
  ...state,
  animal: 'Turkey',
};

console.log(newState);
// { food: 'Apple', animal: 'Turkey' }

console.log(state);
// { food: 'Apple', animal: 'Red Panda' }

La sintaxis extendida nos permite "desempaquetar" un objeto sobre otro. Una buena analogía para esto es cuando llevas una maleta con tu ropa a un hotel y la desempacas en los cajones de tu habitación de hotel. Aquí, la maleta es state y el ... antes de que seamos nosotros "abriendo la cremallera, desempacando y moviendo nuestra ropa a los cajones del hotel".

El resultado final de esto es que obtenemos un nuevo objeto (en el que estamos desempaquetando nuestro objeto existente). A partir de ahí, podemos modificar valores específicos en el objeto agregando propiedades adicionales debajo del ...state .

Entonces, lo que logramos aquí es tomar lo que teníamos antes, crear una copia y luego modificar propiedades específicas en ese objeto en relación con la acción que se está tomando.

Al alejarnos, podemos ver que el objetivo de nuestra función de reducción en Redux es modificar el estado en respuesta a alguna acción . Si nuestro action.type era LOGOUT , sabemos que queremos modificar el estado para reflejar que el usuario actual (como se representa en el estado actual de la tienda) está desconectado.

En el ejemplo anterior, creamos una copia del state actual y luego establecer authenticated a false y user a null . Porque estamos devolviendo un objeto aquí, como parte del switch() comportamiento de la declaración, ese valor devuelto "burbujeará" en el cuerpo de nuestra función reductora y será devuelto desde la función reductora. Cualquier cosa que sea devuelta por la función reductora, entonces, se convierte en el nuevo estado de la tienda.

Definición de una tienda para el estado global

Seamos un poco más concretos con esto. A continuación, vamos a crear una tienda global para nuestra aplicación que contendrá algunos artículos para un carrito de compras. Más tarde, crearemos un componente React para el carrito desde donde enviaremos los eventos a la tienda global.

Para comenzar, creemos nuestra tienda global dentro del modelo que clonamos antes:

/lib/appStore.js

import { createStore } from "redux";

const appStore = createStore((state = {}, action) => {
  // We'll define the functionality for our reducer here.
}, {
  cart: [],
});

export default appStore;

Similar a lo que aprendimos anteriormente, estamos creando una tienda Redux para nuestra aplicación usando el createStore() método importado de redux paquete (incluido en el repetitivo que clonó o, si optó, instaló manualmente antes).

Aquí, en lugar de usar el nombre genérico store para la variable que almacena nuestra tienda, estamos usando el nombre appStore para reflejar su contenido (estado global para toda nuestra aplicación). Si saltamos hasta el final del archivo, veremos que export default appStore . Esto será útil más adelante cuando conectemos nuestra tienda a nuestro <App /> principal. componente.

Un gran cambio que hicimos en el código que vimos anteriormente es que estamos pasando otro argumento a nuestro createStore() llamar. Como segundo argumento (además de nuestra función de reducción), estamos pasando un objeto JavaScript que representa el predeterminado Estado de nuestra tienda. Aunque no tenemos que hacer esto, esta es una forma conveniente de inicializar su tienda con datos.

Definición de un reductor para su tienda de estado global

A continuación, debemos desarrollar nuestra función de reducción para decidir qué sucede cuando nuestra tienda recibe una acción:

/lib/appStore.js

import { createStore } from "redux";

const appStore = createStore(
  (state = {}, action) => {
    switch (action.type) {
      case "ADD_TO_CART":
        return {
          ...state,
          cart: [...state.cart, action.item],
        };
      case "REMOVE_FROM_CART":
        return {
          ...state,
          cart: [...state.cart].filter(({ _id }) => {
            return _id !== action.itemId;
          }),
        };
      case "CLEAR_CART":
        return {
          ...state,
          cart: [],
        };
      default:
        return {
          ...state,
        };
    }
  },
  {
    cart: [],
  }
);

export default appStore;

Tomando lo que aprendimos anteriormente y aplicándolo, aquí presentamos una declaración de cambio de caso que toma un action.type y define una serie de case declaración para decidir qué cambios haremos (si los hay).

Aquí, hemos definido cuatro case declaraciones y uno default caso:

  • ADD_TO_CART el type de la acción cuando un usuario agrega un artículo a su carrito.
  • REMOVE_FROM_CART el type de la acción cuando un usuario elimina un artículo de su carrito.
  • CLEAR_CART el type de la acción cuando un usuario borra todos los elementos de su carrito.

Para cada case , estamos usando un patrón similar al que vimos antes. Devolvemos un objeto JavaScript que contiene una copia de nuestro state existente y luego haga las modificaciones necesarias.

Debido a que estamos creando un carrito de compras, el valor en el que nos enfocamos es items que contiene, como era de esperar, los artículos actualmente en el carrito.

Mirando el ADD_TO_CART caso, creamos una copia de nuestro estado y luego establecemos el cart propiedad igual a una matriz que contiene el state.cart existente (si lo hay) a la matriz. A continuación, anticipamos que nuestro action pasará un item además de nuestro tipo y concatenar o agregar ese elemento al final de la matriz. El resultado final aquí es que tomamos los artículos existentes en el carrito y agregamos uno nuevo al final.

Aplicando esta misma lógica al REMOVE_FROM_CART caso, podemos ver que se está tomando un enfoque similar, sin embargo, esta vez, nuestro objetivo no es agregar un artículo al cart array, sino para eliminar o filtrar uno. Primero, creamos una copia de nuestros elementos existentes en una nueva matriz y luego usamos el método de filtro de JavaScript para decir "mantener solo el elemento que estamos recorriendo actualmente si es _id propiedad no igual al itemId anticipamos pasar con el action ."

Para el CLEAR_CART caso, las cosas son un poco más simples; todo lo que nos importa hacer aquí es vaciar completamente el cart formación. Para hacerlo, dado que no nos importa conservar ninguno de los elementos, podemos sobrescribir cart con una matriz vacía.

Uso de un proveedor de Redux para acceder al estado en su aplicación React

Ahora que tenemos nuestra tienda Redux configurada y nuestro reductor planeado, ahora, necesitamos poner nuestra tienda en uso.

La primera opción que veremos para hacer esto es usar el <Provider /> componente del react-redux paquete. Este es un paquete oficial que ofrece ayudantes para usar Redux en una interfaz de usuario basada en React.

Para usar el <Provider /> , debemos colocarlo en la parte superior de nuestro árbol de componentes. Por lo general, este es el componente que se pasa a nuestra llamada a ReactDOM.render() o ReactDOM.hydrate() . Para este tutorial, debido a que estamos usando CheatCode Next.js Boilerplate, lo colocaremos en el pages/_app.js que es el componente principal representado por Next.js y representa la "parte superior" de nuestro árbol de componentes.

/pages/_app.js

import React from "react";
import PropTypes from "prop-types";
import Head from "next/head";
import { Provider as ReduxProvider } from "react-redux";
import { ApolloProvider } from "@apollo/client";
import Navigation from "../components/Navigation";
import loginWithToken from "../lib/users/loginWithToken";
import appStore from "../lib/appStore";
import client from "../graphql/client";

import "../styles/styles.css";

class App extends React.Component {
  state = {
    loading: true,
  };

  async componentDidMount() {
    [...]
  }

  render() {
    const { Component, pageProps } = this.props;
    const { loading } = this.state;

    if (loading) return <div />;

    return (
      <React.Fragment>
        <Head>
          <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0"
          />
          <title>App</title>
        </Head>
        <ReduxProvider store={appStore}>
          <ApolloProvider client={client}>
            <Navigation />
            <div className="container">
              <Component {...pageProps} />
            </div>
          </ApolloProvider>
        </ReduxProvider>
      </React.Fragment>
    );
  }
}

App.propTypes = {
  Component: PropTypes.object.isRequired,
  pageProps: PropTypes.object.isRequired,
};

export default App;

Algunas notas aquí. Primero, CheatCode Next.js Boilerplate usa Redux como una tienda global por defecto. También utiliza el <Provider /> componente para entregar la tienda al árbol de componentes.

Aquí, para dejar claro nuestro trabajo, vamos a cambiar dos cosas importantes:

  1. Reemplace el import store from '../lib/store' con import appStore from '../lib/appStore' .
  2. Abajo en el render() método del <App /> componente, reemplace el nombre de la variable que se pasa al store apoyo en el <ReduxProvider /> componente para ser appStore .

Cabe destacar que cuando importamos el <Provider /> componente del react-redux paquete, también lo renombramos a <ReduxProvider /> para ayudarnos a comprender mejor qué tipo de proveedor es (uso del nombre Provider es común en las bibliotecas de React, por lo que hacer esto nos ayuda a evitar colisiones de espacios de nombres y comprender la intención de cada Provider ).

Al hacer esto, aunque no parezca mucho, lo que hemos logrado es dar acceso a cualquier componente de nuestra aplicación al appStore que pasamos como el store apoyo en el <ReduxProvider /> componente. Si no hacer esto, la única forma en que podríamos acceder a la tienda sería importarlo directamente a nuestros archivos de componentes (veremos este patrón más adelante).

<ReduxProvider store={appStore}>
  [...]
</ReduxProvider>

A continuación, veremos cómo acceder a la tienda desde dentro un componente en nuestro árbol usando tres métodos diferentes:accediendo a la tienda en un componente a través del react-redux connect HOC (componente de orden superior), a través de ganchos de componentes funcionales y a través del método de importación directa que acabamos de insinuar.

Acceder a su tienda en un componente React basado en clases con Redux Connect

Como discutimos anteriormente, nuestro objetivo es crear un carrito de compras para demostrar nuestra tienda global. Sin embargo, antes de construir nuestro carrito, necesitamos algunos artículos que podamos agregar a nuestro carrito. Para mostrar el uso del connect HOC de react-redux , construiremos nuestro escaparate como un componente React basado en clases.

Para empezar, modifiquemos el /pages/index.js componente en CheatCode Next.js Boilerplate para darnos una lista simple de elementos que podemos agregar o eliminar de nuestro carrito:

/páginas/index.js

import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";

import StyledStorefront from "./styles";

const storefrontItems = [
  {
    _id: "turkey-sandwich",
    image: "https://loremflickr.com/640/480/turkeysandwich",
    title: "Turkey Sandwich",
    price: "$2.19",
  },
  {
    _id: "potato-chips",
    image: "https://loremflickr.com/640/480/potatochips",
    title: "Potato Chips",
    price: "$1.19",
  },
  {
    _id: "soda-pop",
    image: "https://loremflickr.com/640/480/popcan",
    title: "Soda Pop",
    price: "$1.00",
  },
];

class Index extends React.Component {
  render() {
    const { cart, addToCart, removeFromCart } = this.props;

    return (
      <StyledStorefront>
        <ul>
          {storefrontItems.map((item) => {
            const { _id, image, title, price } = item;
            const itemInCart =
              cart && cart.find((cartItem) => cartItem._id === _id);

            return (
              <li key={_id}>
                <img src={image} alt={title} />
                <header>
                  <h4>{title}</h4>
                  <p>{price}</p>
                  <button
                    className="button button-primary"
                    onClick={() =>
                      !itemInCart ? addToCart(item) : removeFromCart(_id)
                    }
                  >
                    {!itemInCart ? "Add to Cart" : "Remove From Cart"}
                  </button>
                </header>
              </li>
            );
          })}
        </ul>
      </StyledStorefront>
    );
  }
}

Index.propTypes = {
  cart: PropTypes.array.isRequired,
  addToCart: PropTypes.func.isRequired,
  removeFromCart: PropTypes.func.isRequired,
};

export default connect(
  (state) => {
    return {
      cart: state.cart,
    };
  },
  (dispatch) => {
    return {
      addToCart: (item) => dispatch({ type: "ADD_TO_CART", item }),
      removeFromCart: (itemId) =>
        dispatch({ type: "REMOVE_FROM_CART", itemId }),
    };
  }
)(Index);

Mucho que ver aquí, pero comencemos desde abajo con el connect() llamar. Este connect() El método se está importando en la parte superior de nuestro /pages/index.js expediente. Como su nombre lo indica, el connect() método conecta el componente que estamos escribiendo en la tienda Redux. Más específicamente, toma la tienda que le pasamos al <ReduxProvider /> y asigna su estado y método de envío al componente que estamos empaquetando.

En este ejemplo, estamos envolviendo nuestro <Index /> componente con el connect() para que podamos conectar la interfaz de usuario de nuestra tienda a la tienda Redux.

Si miramos un poco más de cerca, el connect() El método toma dos argumentos:

  1. Primero, una función que se conoce como mapStateToProps lo que nos permite acceder al estado actual de la tienda Redux y asignar su contenido a los accesorios del componente que estamos empaquetando (es decir, nos permite seleccionar de forma selectiva a qué datos del estado queremos dar acceso a nuestro componente).
  2. Segundo, una función que se conoce como mapDispatchToProps que nos permite acceder al dispatch método para la tienda Redux dentro de nuestro componente.

Mirando mapStateToProps , la idea aquí es bastante sencilla:definir una función que reciba el state actual de la tienda Redux como argumento y luego devolver un objeto JavaScript que contiene los nombres de los accesorios que queremos exponer a nuestro componente. Ahora, mira de cerca. Lo que estamos haciendo aquí es decir "queremos tomar el state.cart valor y mapearlo al cart prop en nuestro componente.

Al hacer esto, ahora, dentro de nuestro render() (y otros métodos de ciclo de vida en el componente), podemos decir this.props.cart , o, si estamos usando la desestructuración const { cart } = this.props; .

Lo bueno de esto es que a medida que nuestra tienda se actualiza, ahora, this.props.cart también se actualizará. La ventaja aquí es que lo que obtenemos es esencialmente una actualización en tiempo real en nuestra interfaz de usuario.

Mirando el segundo argumento pasado a connect() , nuevamente, tenemos otra función llamada mapDispatchToProps . Esto es casi idéntico al mapStateToProps función, excepto que toma un único argumento dispatch que es una función en sí misma. Esta función se utiliza para enviar acciones (¿las recuerdas?) a nuestra tienda.

Recuerde antes cómo teníamos la declaración de cambio de caso con cosas como case "ADD_TO_CART" ? Aquí es donde conectamos esas cosas con nuestra interfaz de usuario. Aquí, en nuestro mapDispatchToProps función, lo que estamos haciendo es tratar de pasar accesorios a nuestro componente (el que está envuelto por nuestra llamada a connect() ) que representan las diferentes acciones que intentamos enviar.

Aquí, estamos pasando dos accesorios:addToCart y removeFromCart . Estamos configurando estos accesorios como una función que espera que se le pase un item o un itemId (respectivamente).

Cuando el addToCart la función se llama como this.props.addToCart({ _id: '123', title: 'Item Title', ... }) lo que pasa es que el objeto pasó a addToCart se devuelve a esta función que se establece en addToCart prop y luego entregado a una llamada al dispatch en nuestra tienda Redux.

Si echamos un vistazo a esa llamada a dispatch() , podemos ver que también pasamos un objeto aquí, pero esta vez agregamos un type propiedad. ¿Parecer familiar? Sí, el type: "ADD_TO_CART" se asigna de nuevo al case "ADD_TO_CART" que vimos en nuestra función reductora en /lib/appStore.js !

¿Tiene sentido?

Lo mismo se aplica aquí con removeFromCart , sin embargo, cuando lo llamamos, en lugar de pasar un artículo completo para agregarlo al carrito, simplemente pasamos el itemId o el _id del objeto del artículo.

Para que esto quede más claro, echemos un vistazo al render() método de nuestro componente.

/páginas/index.js

class Index extends React.Component {
  render() {
    const { cart, addToCart, removeFromCart } = this.props;

    return (
      <StyledStorefront>
        <ul>
          {storefrontItems.map((item) => {
            const { _id, image, title, price } = item;
            const itemInCart =
              cart && cart.find((cartItem) => cartItem._id === _id);

            return (
              <li key={_id}>
                <img src={image} alt={title} />
                <header>
                  <h4>{title}</h4>
                  <p>{price}</p>
                  <button
                    className="button button-primary"
                    onClick={() =>
                      !itemInCart ? addToCart(item) : removeFromCart(_id)
                    }
                  >
                    {!itemInCart ? "Add to Cart" : "Remove From Cart"}
                  </button>
                </header>
              </li>
            );
          })}
        </ul>
      </StyledStorefront>
    );
  }
}

Esto debería tener más sentido. Observe que en la parte superior de este archivo estamos usando la desestructuración para "arrancar" el cart (que mapeamos desde el estado en mapStateToProps ), addToCart (que agregamos a los accesorios en mapDispatchToProps ) y removeFromCart (que agregamos a los accesorios en mapDispatchToProps ).

Poniendo todo eso en uso, primero, usamos la matriz estática de storefrontItems que vimos arriba y mapear sobre él (estos son solo elementos inventados que imitan lo que podríamos obtener de una base de datos).

A medida que mapeamos cada artículo, queremos hacer la pregunta "¿ya se agregó este artículo al carrito?"

Aquí es donde la variable itemInCart entra en juego dentro de nuestro .map() método. Aquí, estamos asignando la variable a una llamada a cart.find() . .find() es una función nativa de JavaScript que nos permite llamar a una función que intenta encontrar un elemento coincidente en alguna matriz.

Aquí, queremos ver si podemos encontrar un objeto JavaScript en nuestro cart matriz con un _id propiedad igual al _id del elemento de escaparate que se está reproduciendo actualmente en nuestro mapa.

Si encontramos una coincidencia? ¡Eso significa que el artículo está en nuestro carrito!

Luego, utilizando este valor, hacemos dos cosas que involucran el botón "Agregar al carrito" a continuación. Primero, asignamos un onClick handler diga "cuando se haga clic en este botón, agregue este artículo al carrito o, si ya está en el carrito, elimínelo". Tenga en cuenta que aquí llamamos al addToCart() y removeFromCart() funciones que asignamos a accesorios en nuestro mapDispatchToProps funcionar antes.

Recuerde que dependiendo de lo que estemos haciendo (agregando un artículo al carrito o eliminando uno existente), vamos a pasar diferentes datos a dispatch .

¡Eso es una parte menos! Ahora, si hace clic en el botón "Agregar al carrito" para cada artículo, debería verlo cambiar a "Eliminar del carrito" y viceversa si lo vuelve a hacer clic.

Acceder a su tienda en un componente funcional de React con Redux Hooks

Otro método para acceder a una tienda Redux en React es usar una de las implementaciones de ganchos incluidas en el react-redux paquete. Los ganchos son una convención en React para manejar el estado dentro de los componentes funcionales o responder a los efectos secundarios de los cambios en los accesorios o el estado en un componente funcional.

En react-redux , uno de los ganchos disponibles para usar se llama useSelector() . Nos permite "seleccionar" directamente un valor (o valores) de nuestra tienda Redux.

Como ejemplo, vamos a actualizar el <Navigation /> en CheatCode Next.js Boilerplate para incluir un recuento de artículos del carrito (con un enlace a la página del carrito que crearemos a continuación) que se actualiza automáticamente a medida que se agregan o eliminan artículos de nuestro carrito.

/componentes/Navegación/index.js

import React, { useState, useEffect } from "react";
import { useRouter } from "next/router";
import { useSelector } from "react-redux";
import NavigationLink from "../NavigationLink";
import Link from "next/link";

import StyledNavigation from "./styles";

const Navigation = () => {
  const cart = useSelector((state) => state.cart);
  const router = useRouter();
  const [navigationOpen, setNavigationOpen] = useState(false);

  const handleRouteChange = () => {
    setNavigationOpen(false);
  };

  useEffect(() => {
    router.events.on("routeChangeStart", handleRouteChange);

    return () => {
      router.events.off("routeChangeStart", handleRouteChange);
    };
  }, []);

  return (
    <StyledNavigation className={`navigation ${navigationOpen ? "open" : ""}`}>
      <div className="container">
        <Link href="/" passHref>
          <a className="brand">BigBox</a>
        </Link>
        <i
          className="fas fa-bars"
          onClick={() => setNavigationOpen(!navigationOpen)}
        />
        <div className="navigation-items">
          <ul>
            <NavigationLink href="/">Storefront</NavigationLink>
          </ul>
          <p className="cart" onClick={() => router.push("/cart")}>
            <i className="fas fa-shopping-cart" /> {(cart && cart.length) || 0}{" "}
            Cart
          </p>
        </div>
      </div>
    </StyledNavigation>
  );
};

Navigation.propTypes = {};

export default Navigation;

Esto se ve un poco diferente. El gran cambio que estamos haciendo aquí es que en lugar de usar un componente basado en clases, estamos usando un componente funcional. Esta es una técnica para definir un componente React que es de naturaleza más simple. Los componentes funcionales son componentes que no necesitan los métodos de ciclo de vida ni la estructura de una clase de JavaScript.

Para llenar el vacío entre los métodos de ciclo de vida que faltan y la necesidad ocasional de acceso al estado, en la versión 16, React introdujo ganchos. Una forma de obtener acceso al estado de nivel de componente sin tener que introducir todo el peso de un componente basado en clases.

Nuestra navegación se ajusta bastante bien a esta necesidad. Se basa en una configuración de estado simple y en la obtención de datos, pero no necesita mucho más que eso; un gran ajuste para componentes funcionales y ganchos.

Aquí, a lo que queremos prestar atención es a nuestra llamada a useSelector() cerca de la parte superior de nuestro componente. Esto se está importando desde el react-redux paquete y es responsable de ayudarnos a extraer algo de valor de nuestro estado (un concepto similar a lo que vimos con mapStateToProps en nuestra tienda).

La forma en que funciona el enlace es que toma una función como argumento y cuando nuestro componente se procesa, se llama a esa función y se recibe el estado actual de nuestra tienda Redux.

¿Esperar? ¿Qué tienda Redux? El que pasamos a través de nuestro <ReduxProvider /> . Aunque no podemos verlo, detrás de escena, el useSelector() gancho aquí comprueba si hay una tienda Redux existente en los accesorios de nuestro árbol de componentes. Si encuentra uno, la llamada tiene éxito y se nos devuelve el valor que solicitamos de state (suponiendo que exista en el estado).

Si lo hiciéramos no tener nuestro <ReduxProvider /> más arriba en nuestro árbol de componentes, obtendríamos un error de React diciendo que el useSelector() hook requiere acceso a una tienda y que necesitamos configurar un proveedor.

A partir de aquí, las cosas se explican por sí mismas. Tomamos el state.cart recuperado valor, colocándolo en nuestro cart variable y luego hacia la parte inferior de nuestro componente, represente el length actual del cart matriz.

¡Eso es todo! Aunque no parezca mucho, vuelva a la página de la tienda y agregue algunos artículos al carrito. Tenga en cuenta que aunque estamos enviando nuestro addToCart o removeFromCart acciones desde el escaparate, los cambios en la tienda Redux se propagan a cualquier otro componente de nuestra aplicación que recupera y escucha los cambios en los datos de nuestra tienda Redux.

Esta es la magia de Redux en juego. Puede cambiar los datos desde un lugar y hacer que esos cambios se reflejen automáticamente en otro lugar. Con una característica como un carrito de compras, esta es una excelente manera de agregar comentarios visuales a los usuarios de que la acción que realizaron tuvo éxito sin la necesidad de cosas como alertas emergentes u otros elementos discordantes de la interfaz de usuario.

Acceder a su tienda directamente en un componente React basado en clases

Ahora que hemos visto los dos métodos más comunes para acceder a una tienda Redux, veamos uno más. En nuestro ejemplo final, vamos a conectar una página para nuestro carrito, mostrar los artículos en el carrito y permitirnos eliminar un artículo a la vez o borrar el carrito por completo.

/páginas/carro/index.js

import React from "react";
import appStore from "../../lib/appStore";

import StyledCart from "./styles";

class Cart extends React.Component {
  state = {
    cart: [],
  };

  componentDidMount() {
    this.handleStoreStateChange();
    this.unsubscribeFromStore = appStore.subscribe(this.handleStoreStateChange);
  }

  componentWillUnmount() {
    this.unsubscribeFromStore();
  }

  handleStoreStateChange = () => {
    const state = appStore.getState();
    this.setState({ cart: state && state.cart });
  };

  render() {
    const { cart } = this.state;

    return (
      <StyledCart>
        <header>
          <h1>Cart</h1>
          <button
            className="button button-warning"
            onClick={() =>
              appStore.dispatch({
                type: "CLEAR_CART",
              })
            }
          >
            Clear Cart
          </button>
        </header>
        {cart && cart.length === 0 && (
          <div className="blank-state bordered">
            <h4>No Items in Your Cart</h4>
            <p>To add some items, visit the storefront.</p>
          </div>
        )}
        {cart && cart.length > 0 && (
          <ul>
            {cart.map(({ _id, title, price }) => {
              return (
                <li key={_id}>
                  <p>
                    <strong>{title}</strong> x1
                  </p>
                  <div>
                    <p className="price">{price}</p>
                    <i
                      className="fas fa-times"
                      onClick={() =>
                        appStore.dispatch({
                          type: "REMOVE_FROM_CART",
                          itemId: _id,
                        })
                      }
                    />
                  </div>
                </li>
              );
            })}
          </ul>
        )}
      </StyledCart>
    );
  }
}

export default Cart;

A lo que queremos prestar atención aquí es que si miramos nuestras importaciones en la parte superior de nuestro archivo, ya no estamos importando ninguna función del react-redux paquete.

En cambio, aquí, estamos introduciendo nuestro appStore directamente.

Lo bueno de Redux es que es bastante versátil. Mientras podemos use herramientas útiles como el connect() método o el useSelector() ganchos, podemos acceder a nuestra tienda de todos modos directamente.

Las ventajas de este método son el control, la claridad y la simplicidad. Al acceder a su tienda directamente, no hay confusión sobre cómo la tienda está encontrando su camino hacia nuestro componente (por ejemplo, usando el <ReduxProvider /> ) y eliminamos la necesidad de código adicional para mapearnos a lo que queremos.

¡En su lugar, simplemente accedemos a él!

Arriba, una vez que hayamos importado nuestro appStore , queremos ver tres métodos definidos en nuestro Cart clase:componentDidMount() , componentWillUnmount() y handleStoreStateChange() .

Los dos primeros métodos, componentDidMount() y componentWillUnmount() son métodos de ciclo de vida integrados en React. Como sus nombres lo indican, estas son funciones que queremos llamar después nuestro componente se ha montado en el DOM (modelo de objeto de documento, o la representación en memoria de lo que se representa en pantalla para los usuarios), o justo antes de que nuestro componente se desmonte del DOM.

Dentro de componentDidMount() , estamos haciendo dos cosas:primero, estamos haciendo una llamada a this.handleStoreStateChange() . Ignoremos eso por un segundo.

A continuación, estamos asignando this.unsubscribeFromStore al resultado de llamar a appStore.subscribe() . ¿Qué es esto?

En Redux, una suscripción es una forma de registrar una función de devolución de llamada que se activa cada vez que se realiza un cambio en nuestra tienda. Aquí, estamos llamando a appStore.subscribe() pasando en this.handleStoreStateChange . Esa función es responsable de actualizar nuestro <Cart /> componente cada vez que se realiza un cambio en nuestra tienda.

Si nos fijamos en handleStoreStateChange() , veremos que hace dos cosas:primero llama al .getState() método en nuestro appStore store para obtener el estado actual de nuestra tienda Redux. Luego, debido a que lo único que nos importa en esta vista son los artículos en nuestro carrito, toma el state.cart y luego lo copia al estado de <Cart /> componente.

Esto nos permite lograr algo similar a lo que vimos en la sección anterior con useSelector() , pero en lugar de acceder directamente a los valores a través del gancho, primero accedemos al estado actual de toda la tienda con .getState() y entonces arrancar lo que queremos. Usamos el state del componente basado en la clase React (this.state ) como nuestro mecanismo para representar datos.

Al usar este método, hay un problema:¿cómo configuramos el inicial? this.state valor para nuestro <Cart /> componente. Aquí es donde la llamada a this.handleStoreStateChange() en componentDidMount() viene muy bien.

Aquí, estamos diciendo "cuando el componente se monte, vaya y obtenga el estado actual de la tienda y colóquelo en el <Cart /> estado del componente". Esto asegura que, ya sea que estemos cargando la página del carrito por primera vez o que estemos recibiendo cambios después el montaje, el estado de nuestro componente se actualiza correctamente.

Por el contrario, cuando nuestro componente se va a desmontar desde el DOM (lo que significa que estamos saliendo de la página), llamamos this.unsubscribeFromStore() que contiene la función que recibimos de nuestro appStore.subscribe() método anterior. Esta función, cuando se llama, detiene los oyentes de la tienda, eliminándolos de la memoria. Esto se conoce como "limpieza" para garantizar que no tengamos código innecesario ejecutándose en segundo plano para las páginas que ya no están en pantalla para el usuario.

Ahora que tenemos estas piezas, abajo en nuestro render() método, podemos cerrar el ciclo en todo esto:

/páginas/carro/index.js

[...]

class Cart extends React.Component {
  state = {
    cart: [],
  };

  [...]

  render() {
    const { cart } = this.state;

    return (
      <StyledCart>
        <header>
          <h1>Cart</h1>
          <button
            className="button button-warning"
            onClick={() =>
              appStore.dispatch({
                type: "CLEAR_CART",
              })
            }
          >
            Clear Cart
          </button>
        </header>
        {cart && cart.length === 0 && (
          <div className="blank-state bordered">
            <h4>No Items in Your Cart</h4>
            <p>To add some items, visit the storefront.</p>
          </div>
        )}
        {cart && cart.length > 0 && (
          <ul>
            {cart.map(({ _id, title, price }) => {
              return (
                <li key={_id}>
                  <p>
                    <strong>{title}</strong> x1
                  </p>
                  <div>
                    <p className="price">{price}</p>
                    <i
                      className="fas fa-times"
                      onClick={() =>
                        appStore.dispatch({
                          type: "REMOVE_FROM_CART",
                          itemId: _id,
                        })
                      }
                    />
                  </div>
                </li>
              );
            })}
          </ul>
        )}
      </StyledCart>
    );
  }
}

export default Cart;

Anteriormente, aprendimos sobre el envío de acciones a nuestra tienda Redux usando las funciones con nombre que creamos y asignamos a los accesorios de nuestro componente de escaparate con mapDispatchToProps .

Cuando llamamos al dispatch (el que recibimos del argumento pasado al mapDispatchToProps función), lo que técnicamente estábamos haciendo es llamar a nuestro appStore.dispatch método.

Tal como vimos antes, este método es responsable de despachar una acción a nuestra tienda Redux. El trabajo que hicimos con mapDispatchToProps fue puramente por conveniencia. La conveniencia es que pudimos crear una función con nombre que representaba la acción que se estaba tomando en lugar de pasar un dispatch genérico prop a nuestro componente (que es potencialmente más confuso).

Aquí, en lugar de usar un mapDispatchToProps , hacemos comando y solo usamos appStore.dispatch() directamente. Lo bueno aquí es que estamos pasando exactamente lo mismo a appStore.dispatch() como hicimos con addToCart() y removeFromCart() más temprano. La diferencia esta vez es que solo estamos llamando a dispatch directamente.

Si intentamos eliminar un artículo de nuestro carrito ahora haciendo clic en la "x" al lado del artículo, o haciendo clic en el botón "Borrar carrito" cerca de la parte superior de la página, nuestras acciones se envían y el cart ¡El valor en nuestra tienda Redux está actualizado!

Terminando

En este tutorial, aprendimos sobre tres métodos diferentes para interactuar con Redux, utilizando dos tipos diferentes de estilos de componentes en React:componentes basados ​​en clases y componentes funcionales.

Redux es una excelente manera de manejar el estado global en una aplicación y agregar un poco de estilo de "tiempo real" a su aplicación. Lo bueno de esto es su flexibilidad, como hemos visto aquí. No estamos limitados a una sola forma de hacer las cosas, lo que significa que Redux puede adaptarse a proyectos nuevos y existentes (basados ​​en React o de otro tipo) con facilidad.