Paquete react-redux

A estas alturas ya queda claro que Redux no está acoplado a React en absoluto. Puedes construir una aplicación completa usando Redux con cualquier librería, incluyendo Vue. Sin embargo, Redux fue creado pensando en React, y hay ciertos pasos que podemos dar en nuestra aplicación para que la experiencia entre ambas tecnologías sea más fluida.

Esta publicación está registrada en el commit#a5f33e8a con los cambios que describimos.

Connect y Provider

Como has visto, Redux no tiene ninguna relación directa con React. React es otra librería de UI que puede aprovechar Redux para un manejo de estado más predecible. Si observamos nuestra propia implementación de React y Redux juntos, la clave real es que estamos pasando nuestro store como prop y utilizándolo para obtener el estado, hacer dispatch() o subscribe() cuando sea necesario. El objetivo principal de nuestra abstracción debería ser hacer que las tres responsabilidades del store funcionen de la manera más fluida posible con React.

Cómo llega el Store a cada Componente

Lo primero que queremos mejorar es cómo cada componente accede al store. Si a un componente le resulta difícil acceder al store —ya sea para obtener el estado, escucharlo o actualizarlo— nada de lo demás importará. Ahora mismo simplemente pasamos el store como una prop. Funciona bien en una aplicación pequeña, pero ¿qué pasa si tenemos una app más grande con más capas? Pasar el store a través de diez componentes no escalaría bien. Uno de los principales beneficios de tener un store es evitar pasar props a través de un montón de componentes.

Una de las razones por las que React —hablando de React por un momento, no de Redux— es tan popular es porque es muy eficiente. Es eficiente manteniendo el estado localizado en un componente, eficiente confinando la UI a un componente, y eficiente detectando cambios y re-renderizando solo ese componente. Así que lo segundo que debemos resolver es cómo re-renderizar componentes únicamente si los datos que dependen del store cambian. Actualmente, resolvemos esto llamando a getState() en la raíz de la aplicación y luego pasando los datos hacia abajo. De nuevo, esto no escalará bien a medida que nuestra app crezca.

Si podemos encontrar una abstracción adecuada para que el store llegue a cualquier componente que lo necesite y solo re-renderizar los componentes cuando los datos exactos que necesitan cambien, mejoraremos todos los aspectos de nuestro código actual.

Afortunadamente, desde React v16.3 podemos usar la React Context API. Context proporciona una forma de pasar datos a través del árbol de componentes sin tener que pasar props manualmente en cada nivel.

En una aplicación típica de React, los datos se transmiten de arriba abajo (de padre a hijo) mediante props, pero esto puede volverse engorroso para ciertos tipos de props (por ejemplo, preferencia de idioma, tema de UI) que son requeridos por muchos componentes dentro de una aplicación. Context ofrece una manera de compartir valores como estos entre componentes sin tener que pasar explícitamente una prop por cada nivel del árbol.

Context

Antes de agregar la API de Context a nuestra aplicación, asegurémonos de que estamos alineados sobre cómo funciona.

Recordemos que en los primeros enfoques de React teníamos que pasar datos de componente a componente:

import React from 'react';
import { render } from 'react-dom';

function Parent ({ name }) {
    return (
        <div>
            <h1>Parent</h1>
            <Child name={name}/>
        </div>
    );
}

function Child ({ name }) {
    return (
        <div>
            <h1>Child</h1>
            <Grandchild name={name}/>
        </div>
    );
}

function Grandchild ({ name }) {
    return (
        <div>
            <h1>Grandchild</h1>
            <h3>Name: {name}</h3>
        </div>
    );
}

class App extends React.Component {
    render() {
        const name = 'Tyler';

        return (
            <Parent />
        );
    }
}

render(<App />, document.getElementById('root'));

La variable name se pasa desde <App /> hasta <Grandchild /> a través de <Parent /> y <Child />. Sin embargo, <Parent /> y <Child /> nunca utilizan la propiedad name.

Este proceso de “encadenar props” para enviar datos a un componente hijo puede ser tedioso y propenso a errores. Afortunadamente, podemos evitarlo con la API de Context de React. Para empezar, usaremos la función createContext() de React que devuelve un objeto con un Provider y un Consumer. El siguiente fragmento es el código usando la API de Context.

import React from 'react';
import { render } from 'react-dom';

const Context = React.createContext();

function Parent () [
    return (
        <div>
            <h1>Parent</h1>
            <Child />
        </div>
    )
]

function Child () [
    return (
        <div>
            <h1>Child</h1>
            <Grandchild />
        </div>
    )
]

function Grandchild ({name}) [
    return (
        <Context.Consumer>
            { () => (
                <div>
                    <h1>Grandchild</h1>
                    <h3>Name: {name}</h3>
                </div>
            )}
        </Context.Consumer>
    )
]

function App extends React.Component {
    render () {
        const name = 'Sergio'

        return (
            <Context.Provider value={name}>
                <Parent />
            </Context.Provider>
        )
    }
}

render(<App />, document.getElementById('root'))

Context.Provider

El componente Provider se utiliza en el nivel superior del árbol de componentes; es decir, el componente que contiene los datos que se van a pasar. En nuestro caso, este era el componente App. Pasamos el dato name como valor del prop value de Provider. ¡Observa que el componente Provider simplemente envuelve a todo el componente que se va a renderizar!

Context.Consumer

En el extremo receptor (es decir, un componente “debajo” del Provider en la jerarquía), usamos el componente Consumer. En nuestro ejemplo, pasamos a Consumer una función como hijo. Esta función acepta un valor y devuelve JSX. Como resultado, pudimos renderizar el componente Grandchild con el dato name correcto sin tener que pasar ese dato a través de toda la cadena de componentes. ¡Eso es mucho menos código que la forma anterior! La API de Context de React proporciona una manera concisa y accesible de comunicar información de un componente a otro.

Ahora, pongamos en práctica Context en la aplicación de todos.

Patrón: Componentes Conectados y Componentes de Presentación

Para aplicar la API de Context en la app de todos debemos seguir el patrón de Componentes Conectados y de Presentación, cuya estructura abstracta es:

<ConnectedComponent>
    <PresentationalComponent>
</ConnectedComponent>

Estos nombres ayudan a explicar cuál es el propósito de cada componente:

  • Un Connected Component (Componente Conectado) está conectado al store de Redux y es responsable de obtener datos del store.
  • Un Presentational Component (Componente de Presentación) no debe acceder al store. Debe recibir toda la información que necesita como props y simplemente renderizar la UI.

connect(): Bajo el Capó

El propósito es abstraer toda la conexión entre Redux y React como lo haría una librería. Con el patrón de Componentes Conectados y de Presentación tenemos solo media solución. Falta lograr que el usuario pueda obtener la información del Context sin tener que usar <Context.Consumer>. Por eso, tenemos una función de abstracción llamada connect().

La función connect() es un componente de alto orden (higher-order component) que tiene tres responsabilidades:

  • Saber cuándo cambia el store — usar el subscribe del store
  • Saber qué estado se necesita — usar mapStateToProps
  • Obtener el store desde Context

El siguiente fragmento ilustra la lógica de la función connect():

const ConnectedComponent = connect((state) => ({
    stateProp: state.prop
}))(PresentationalComponent)

function connect (mapStateToProps) {
    return (Component) => {
        class Receiver extends React.Component {
            componentDidMount () {
                const { subscribe } = this.props.store

                this.unsubscribe = subscribe(() => {
                    this.forceUpdate();
                })
            }

            componentWillMount () {
                this.unsubscribe()
            }

            render () {
                const { dispatch, getState } = this.props.store
                const state = getState
                const stateNeeded = mapStateToProps(state)

                return <Component {...stateNeeded} dispatch={dispatch} />

            }
        }

        class ConnectedComponent extends React.Component {
            render () {
                return (
                    <Context.Consumer>
                        {(store) => <Receiver store={store} />}
                    </Context.Consumer>
                )
            }
        }

        return ConnectedComponent;
    }
}

Acabamos de construir el Provider, Context y la función connect(). Esta función es tan común que se ha convertido en una librería llamada react-redux oficialmente soportada por React. Así que, en lugar de usar nuestros propios métodos, podemos importar la librería y usar sus funciones.

Los Bindings de react-redux

Tomemos un momento para recapitular los cambios que hemos hecho en nuestra aplicación en esta lección, ¡porque hemos actualizado bastante!

  1. Aprovechamos la librería estándar redux para construir nuestra app. Esto nos permitió crear un store de Redux con la función createStore(), dándonos una API para escuchar (subscribe()) y obtener actualizaciones (getState()).
  2. Creamos nuestro propio componente Provider para pasar eficientemente el store a los componentes que lo necesitaban, así como nuestra propia función connect() para que los componentes pudieran acceder a “porciones” del estado como props.

Podemos construir una aplicación funcional de React y Redux sin Provider o connect(), pero dado que simplifican enormemente la interacción de los componentes de React con el store de Redux, los creadores de Redux los han incluido en el paquete react-redux.

Provider

Con react-redux, en lugar de crear y usar nuestro propio Provider que se ve así:

const Context = React.createContext(

class Provider extends React.Component {
    render () {
        return (
            <Context.Provider value={this.props.store}>
                {this.props.children}
            </Context.Provider>
        );
}
}

ReactDOM.render(
    <Provider store={store}>
        <ConnectedApp />
    </Provider>,
    document.getElementById('app')
);

…podemos simplemente usar el componente Provider definido por el paquete react-redux. Esto nos permite envolver toda nuestra aplicación con Provider, pasando el store incluso a los componentes más profundamente anidados.

ReactDOM.render(
    <ReactRedux.Provider store={store}>
        <ConnectedApp />
    </ReactRedux.Provider>,
    document.getElementById('app')
);

connect()

De manera similar, también podemos aprovechar la función connect() de react-redux directamente. connect() es una función de alto orden que recibe dos argumentos (además de algunos argumentos opcionales) y devuelve una función. Observa su firma a continuación:

const buildConnectedComponent = connect(mapStateToProps, mapDispatchToProps);

Lo vital es entender que buildConnectedComponent es una función. buildConnectedComponent tomará un componente React normal (de presentación) y devolverá un componente nuevo “conectado”.

const ConnectedComponent = buildConnectedComponent(MyComponent);

ConnectedComponent renderiza MyComponent, pasándole las props definidas por mapStateToProps y mapDispatchToProps.

Podemos evitar la variable intermedia buildConnectedComponent y simplemente llamar a las funciones una tras otra:

const ConnectedComponent = connect(mapStateToProps, mapDispatchToProps)(MyComponent)

¡Observa el doble paréntesis!

El método connect() conecta el Store con el Componente. Además, mapStateToProps es una función que le indica a connect() cómo mapear el state en la lista de props del componente.

React a menudo aprovecha Redux para un manejo de estado más predecible mediante los bindings de react-redux. Estos bindings nos proporcionan una API que simplifica las interacciones más comunes entre React y Redux.

Provider hace posible que Redux pase datos del store a cualquier componente de React que los necesite. Utiliza la característica de context de React para lograr esto.

connect() conecta un componente de React con el store de Redux. La función mapStateToProps() nos permite especificar qué estado del store queremos que se pase a nuestro componente de React, mientras que mapDispatchToProps() nos permite vincular dispatch a los creadores de acciones (action creators) antes de que lleguen al componente.

Estructura de Carpetas

Actualmente, la aplicación está toda en un solo archivo. Esto es buena idea para fines de aprendizaje, pero está lejos de una aplicación del mundo real. Usemos el paquete Create App para organizar la estructura de carpetas de la aplicación de todos/goals.

Organización “estilo Rails”

Para recapitular, hemos organizado los elementos individuales de nuestra aplicación con un enfoque “estilo Rails”. Es decir, los activos se agrupan por “tipo” o “capacidad”: cualquier acción se encontrará en la carpeta Actions, cualquier reducer en Reducers, y así sucesivamente. De hecho, el ejemplo del “mundo real” de Redux en GitHub estructura la aplicación exactamente así. Bajo esta estructura de directorios, si queremos importar todas las acciones a un componente, ¡podemos obtenerlas todas en una sola importación!

Frontend
    - Components
        - component1.js
        - component2.js
        - component3.js
    - Actions
        - action1.js
        - action2.js
    - Reducers
        - reducer1.js
    - Util
    - Store

Otros Patrones

Además del “estilo Rails” para organizar la estructura de carpetas, puedes encontrar otros enfoques que los desarrolladores utilizan para construir su directorio. Una forma alternativa de estructurar la misma aplicación es por funcionalidad:

├── dashboard
│ ├── actions.js
│ ├── index.js
│ └── reducer.js
└── nav
    ├── actions.js
    ├── index.js
    └── reducer.js

Esta forma de organización agrupa los activos por su funcionalidad o “concepto” común. Es decir, todos los activos relacionados con un componente de navegación están juntos en una sola carpeta modular. Es una excelente manera de expresar visualmente de qué trata la aplicación. Sin embargo, si la aplicación contiene varios cientos de componentes, puede volverse más difícil de navegar.

Es más: incluso puedes ver que algunos desarrolladores prefieren un enfoque “duck”, donde los archivos de Redux y manejo de estado están completamente separados de los archivos que renderizan la UI.

En última instancia, la elección es tuya. Sea cual sea la forma que elijas para organizar la estructura de tu directorio, asegúrate de que tenga sentido para tu aplicación y con la que te sientas cómodo.

Resumen

Esta sección no logró nada nuevo con React o Redux. Todo lo que hicimos fue mejorar la estructura y organización de nuestra aplicación moviendo cada parte a una estructura de carpetas específica.

Para decirlo una vez más, no hay una forma “correcta” de construir la estructura de carpetas de tu aplicación. Sin embargo, hacerlo de esta manera es práctico porque estamos usando la estructura proporcionada por Create React App. Usando esta estructura, es fácil convertir una aplicación React simple en una que incluya Redux. Otro beneficio es que otros desarrolladores de React ya estarán familiarizados con esta organización de archivos y carpetas.