Uso con React
Comencemos enfatizando que Redux no tiene relación alguna con React. Puedes escribir aplicaciones Redux con React, Angular, Ember, jQuery o vanilla JavaScript.
Dicho esto, Redux funciona especialmente bien con librerías como React y Deku porque te permiten describir la interfaz de usuario como una función de estado, y Redux emite actualizaciones de estado en respuesta a acciones.
Usaremos React para crear nuestra una aplicación sencilla de asuntos pendites (To-do).
Instalando React Redux
React Redux no está incluido en Redux de manera predeterminada. Debe instalarlo explícitamente:
npm install --save react-redux
Si no usas npm, puedes obtener la distribución UMD (Universal Module Definition) más reciente desde unpkg (ya sea la distribución de desarrollo o la de producción). La distribución UMD exporta una variable global llamada window.ReactRedux
por si la añades a tu página a través de la etiqueta <script>
.
Componentes de Presentación y Contenedores
Para asociar React con Redux se recurre a la idea de separación de presentación y componentes contenedores. Si no está familiarizado con estos términos, lea sobre ellos primero, y luego vuelva. ¡Son importantes, así que vamos le vamos a esperar!
¿Ha terminado de leer el artículo? Repasemos sus diferencias:
Componentes de Presentación | Componentes Contenedores | |
---|---|---|
Propósito | Como se ven las cosas (markup, estilos) | Como funcionan las cosas (búsqueda de datos, actualizaciones de estado) |
Pertinente a Redux | No | Yes |
Para leer datos | Lee datos de los props | Se suscribe al estado en Redux |
Para manipular datos | Invoca llamada de retorno (callback) desde los props | Envia acciones a Redux |
Son escritas | Manualmente | Usualmente generados por React Redux |
La mayoría de los componentes que escribiremos serán de presentación, pero necesitaremos generar algunos componentes contenedores para conectarlos al store que maneja Redux. Con esto y el resumen de diseño que mencionaremos a continuación no implica que los componentes contenedores deban estar cerca o en la parte superior del árbol de componentes. Si un componente contenedor se vuelve demasiado complejo (es decir, tiene componentes de presentación fuertemente anidados con innumerables devoluciones de llamadas que se pasan hacia abajo), introduzca otro contenedor dentro del árbol de componentes como se indica en el FAQ.
Técnicamente usted podría escribir los componentes contenedores manualmente usando store.subscribe()
. No le aconsejamos que haga esto porque React Redux hace muchas optimizaciones de rendimiento que son difíciles de hacer a mano. Por esta razón, en lugar de escribir los componentes contenedores, los generaremos utilizando el comando connect()
, función proporcionada por React Redux, como verá a continuación.
Diseño de la jerarquía de componentes
Recuerda cómo diseñamos y dimos forma al objecto del estado raíz? Es hora de diseñar la jerarquía de la interfaz de usuario para que coincida con este objeto del estado. Esto no es una tarea específica de Redux. Thinking in React es un excelente tutorial que explica el proceso.
Nuestro breve resumen del diseño es simple. Queremos mostrar una lista de asuntos pendientes. Al hacer clic, un elemento de la lista se tachará como completado. Queremos mostrar un campo en el que el usuario puede agregar una tarea nueva. En el pie de página, queremos mostrar un toggle para mostrar todas las taras, sólo las completadas, o sólo las activas.
Diseño de componentes de presentación
Podemos ver los siguientes componentes de presentación y sus props surgir a través de esta breve descripción:
TodoList
es una lista que mostrará las tareas pendientes disponibles.todos: Array
es un arreglo de tareas pendientes que contiene la siguiente descripción{ id, text, completed }
.onTodoClick(id: number)
es un callback para invocar cuando un asunto pendientes es presionado.
Todo
es un asunto pendiente.text: string
es el texto a mostrar.completed: boolean
indica si la tarea debe aparecer tachada.onClick()
es un callback para invocar cuando la tarea es presionada.
Link
es el enlace con su callback.onClick()
es un callback para invocar cuando el enlace es presionado.
Footer
es donde dejamos que el usuario cambie las tareas pendientes visibles actualmente.App
es el componente raíz que representa todo lo demás.
Cada artículo describe la apariencia pero no conoce de donde vienen los datos, o cómo cambiarlos. Sólo muestran lo que se les da. Si migras de Redux a otra cosa, podrás mantener todos estos componentes exactamente iguales. No dependen de Redux en absoluto.
Diseño de componentes contenedores
También necesitaremos algunos componentes contenedores para conectar los componentes de presentación a Redux. Por ejemplo, el componente de presentación TodoList
necesita un contenedor comoVisibleTodoList
que se suscribe al store de Redux y debe saber cómo aplicar el filtro de visibilidad. Para cambiar el filtro de visibilidad, proporcionaremos un componente contenedor FilterLink
que renderiza un Link
que distribuye la debida acción al hacer clic:
VisibleTodoList
filtra los asuntos de acuerdo a la visibilidad actual y renderiza elTodoList
.FilterLink
obtiene el filtro de visibilidad actual y renderiza unLink
.filter: string
es el tipo del filtro de visibilidad.
Diseño de otros componentes
A veces es difícil saber si un componente debe ser componente de presentación o contenedor. Por ejemplo, a veces la forma y la función están realmente entrelazadas, como en el caso de este pequeño componente:
AddTodo
es un campo de entrada con un botón "Añadir tarea"
Técnicamente podríamos dividirlo en dos componentes, pero podría ser demasiado pronto en esta etapa. Está bien mezclar presentación y lógica en un componente que sea muy pequeño. A medida que crece, será más obvio cómo dividirlo, así que lo dejaremos en uno solo.
Implementación de componentes
Vamos a escribir los componentes! Comenzaremos con los componentes de presentación por lo que no es necesario pensar en la relación con Redux todavía.
Implementación de componentes de presentación
Todos estos son componentes normales de React, por lo que no los examinaremos en detalle. Escribiremos componentes funcionales sin-estado a menos que necesitemos usar el estado local o los métodos del ciclo de duración. Esto no significa que los componentes de presentación tengan que ser funciones - es solo que es más fácil definirlos de esta manera. Si, y cuando necesites agregar un estado local, métodos de ciclo de duración u optimizaciones de rendimiento, puede convertirlos a clases.
components/Todo.js
import React, { PropTypes } from 'react'
const Todo = ({ onClick, completed, text }) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
)
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}
export default Todo
components/TodoList.js
import React, { PropTypes } from 'react'
import Todo from './Todo'
const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
)
TodoList.propTypes = {
todos: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired).isRequired,
onTodoClick: PropTypes.func.isRequired
}
export default TodoList
components/Link.js
import React, { PropTypes } from 'react'
const Link = ({ active, children, onClick }) => {
if (active) {
return <span>{children}</span>
}
return (
<a href="#"
onClick={e => {
e.preventDefault()
onClick()
}}
>
{children}
</a>
)
}
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}
export default Link
components/Footer.js
import React from 'react'
import FilterLink from '../containers/FilterLink'
const Footer = () => (
<p>
Show:
{" "}
<FilterLink filter="SHOW_ALL">
Todos
</FilterLink>
{", "}
<FilterLink filter="SHOW_ACTIVE">
Activo
</FilterLink>
{", "}
<FilterLink filter="SHOW_COMPLETED">
Completado
</FilterLink>
</p>
)
export default Footer
components/App.js
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'
const App = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
)
export default App
Ahora es el momento de conectar los componentes de presentación a Redux mediante la creación de algunos contenedores. Técnicamente, un componente contenedor es sólo un componente de React que utiliza store.subscribe ()
para leer una parte del árbol de estado en Redux y suministrar los props a un componente de presentación que renderiza. Puedes escribir un componente contenedor manualmente, pero sugerimos generar los componentes contenedores con la función connect()
de la librería React Redux, ya que proporciona muchas optimizaciones útiles para evitar re-renders innecesarios. (Un beneficio de utilizar esta librería es que usted no tiene que preocuparse por la implementación del método shouldComponentUpdate
recomendado por React para mejor rendimiento.)
Para usar connect()
, es necesario definir una función especial llamada mapStateToProps
que indiqua cómo transformar el estado actual del store Redux en los props que desea pasar a un componente de presentación. Por ejemplo, VisibleTodoList
necesita calcular todos
para pasar a TodoList
, así que definimos una función que filtra el state.todos
de acuerdo con el state.visibilityFilter
, y lo usamos en su mapStateToProps
:
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
Además de leer el estado, los componentes contenedores pueden enviar acciones. De manera similar, puede definir una función llamada mapDispatchToProps()
que recibe el método dispatch()
y devuelve los callback props que deseas inyectar en el componente de presentación. Por ejemplo, queremos que VisibleTodoList
inyecte un prop llamado onTodoClick
en el componente TodoList
, y queremos que onTodoClick
envíe una acción TOGGLE_TODO
:
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
Finalmente, creamos VisibleTodoList
llamando connect()
y le pasamos estas dos funciones:
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
Estos son los conceptos básicos de la API de React Redux, pero hay algunos atajos y opciones avanzadas por lo que le animamos a revisar su documentación en detalle. En caso de que que te preocupe el hecho que mapStateToProps
esté creando objetos nuevos con demasiada frecuencia, quizás desees aprender acerca de computar datos derivados con reselect.
El resto de los componentes contenedores están definidos a continuación:
containers/FilterLink.js
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter))
}
}
}
const FilterLink = connect(
mapStateToProps,
mapDispatchToProps
)(Link)
export default FilterLink
containers/VisibleTodoList.js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
Implementación de otros componentes
containers/AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'
let AddTodo = ({ dispatch }) => {
let input
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ''
}}>
<input ref={node => {
input = node
}} />
<button type="submit">
Añadir tarea
</button>
</form>
</div>
)
}
AddTodo = connect()(AddTodo)
export default AddTodo
Transferir al store
Todos los componentes contenedores necesitan acceso al store Redux para que puedan suscribirse a ella. Una opción sería pasarlo como un prop a cada componente contenedor. Sin embargo, se vuelve tedioso, ya que hay que enlzar store
incluso a través del componentes de presentación ya que puede suceder que tenga que renderizar un contenedor allá en lo profundo del árbol de componentes.
La opción que recomendamos es usar un componente React Redux especial llamado <Proveedor>
para mágicamente hacer que el store esté disponible para todos los componentes del contenedor en la aplicación sin pasarlo explícitamente. Sólo es necesario utilizarlo una vez al renderizar el componente raíz:
index.js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
Siguientes pasos
Lea el código fuente completo de este tutorial para internalizar mejor el conocimiento que ha adquirido. Luego, dirígete directamente al tutorial avanzado para aprender a manejar los network requests y el routing!