Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
Reducción de Código Repetitivo
Redux está parcialmente inspirado en Flux, y la queja más común sobre Flux es la cantidad de código repetitivo que requiere. En esta guía, veremos cómo Redux nos permite elegir el nivel de detalle que queremos en nuestro código, dependiendo de nuestro estilo personal, preferencias del equipo, mantenibilidad a largo plazo, etc.
Acciones
Las acciones son objetos simples que describen lo que ocurre en la aplicación, y son la única forma de describir una intención de modificar los datos. Es importante entender que el hecho de que las acciones sean objetos que debes enviar no es código repetitivo, sino una de las decisiones de diseño fundamentales de Redux.
Existen frameworks que afirman ser similares a Flux, pero sin el concepto de objetos de acción. En términos de predictibilidad, esto es un retroceso respecto a Flux o Redux. Sin acciones serializables como objetos simples, es imposible grabar y reproducir sesiones de usuario, o implementar recarga en caliente con viaje en el tiempo. Si prefieres modificar datos directamente, no necesitas Redux.
Las acciones tienen este aspecto:
{ type: 'ADD_TODO', text: 'Use Redux' }
{ type: 'REMOVE_TODO', id: 42 }
{ type: 'LOAD_ARTICLE', response: { ... } }
Es convención común que las acciones tengan un tipo constante que ayude a los reductores (o Stores en Flux) a identificarlas. Recomendamos usar cadenas de texto y no Símbolos para los tipos de acción, porque las cadenas son serializables, y usar Símbolos complica innecesariamente la grabación y reproducción.
En Flux, tradicionalmente se considera que debes definir cada tipo de acción como una constante de cadena:
const ADD_TODO = 'ADD_TODO'
const REMOVE_TODO = 'REMOVE_TODO'
const LOAD_ARTICLE = 'LOAD_ARTICLE'
¿Por qué es beneficioso? A menudo se dice que las constantes son innecesarias, y para proyectos pequeños, esto puede ser cierto. Para proyectos más grandes, hay beneficios al definir tipos de acción como constantes:
-
Ayuda a mantener la coherencia en la nomenclatura porque todos los tipos de acción se agrupan en un solo lugar.
-
A veces quieres ver todas las acciones existentes antes de trabajar en una nueva funcionalidad. Puede que la acción que necesitas ya la haya añadido alguien del equipo, y no lo supieras.
-
La lista de tipos de acción añadidos, eliminados y modificados en una Pull Request ayuda a todo el equipo a seguir el alcance e implementación de nuevas funcionalidades.
-
Si cometes un error tipográfico al importar una constante de acción, obtendrás
undefined. Redux lanzará un error inmediatamente al enviar dicha acción, y descubrirás el fallo antes.
Depende de ti elegir las convenciones para tu proyecto. Puedes empezar usando cadenas inline, luego pasar a constantes, y quizá después agruparlas en un único archivo. Redux no opina al respecto, así que usa tu mejor criterio.
Creadores de Acciones
Otra convención común es que, en lugar de crear objetos de acción inline donde los envías, crees funciones que los generen.
Por ejemplo, en vez de llamar a dispatch con un objeto literal:
// somewhere in an event handler
dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
})
Puedes escribir un creador de acciones en un archivo separado e importarlo en tu componente:
actionCreators.js
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}
AddTodo.js
import { addTodo } from './actionCreators'
// somewhere in an event handler
dispatch(addTodo('Use Redux'))
Los creadores de acciones a menudo se han criticado por ser código repetitivo. ¡Pues no tienes que escribirlos! Puedes usar objetos literales si crees que se ajustan mejor a tu proyecto. Sin embargo, hay beneficios al escribir creadores de acciones que deberías conocer.
Imagina que un diseñador revisa nuestro prototipo y nos dice que debemos permitir máximo tres tareas. Podemos implementar esto reescribiendo nuestro creador de acciones en forma de callback con el middleware redux-thunk y añadiendo una salida temprana:
function addTodoWithoutCheck(text) {
return {
type: 'ADD_TODO',
text
}
}
export function addTodo(text) {
// This form is allowed by Redux Thunk middleware
// described below in “Async Action Creators” section.
return function (dispatch, getState) {
if (getState().todos.length === 3) {
// Exit early
return
}
dispatch(addTodoWithoutCheck(text))
}
}
Acabamos de modificar el comportamiento del creador de acciones addTodo, completamente invisible para el código que lo invoca. No tenemos que preocuparnos de revisar cada lugar donde se añaden todos para asegurarnos de que incluyan esta verificación. Los creadores de acciones te permiten desacoplar la lógica adicional relacionada con el despacho de una acción de los componentes reales que emiten esas acciones. Esto es muy útil cuando la aplicación está en pleno desarrollo y los requisitos cambian con frecuencia.
Generación de creadores de acciones
Algunos frameworks como Flummox generan constantes de tipo de acción automáticamente a partir de las definiciones de funciones creadoras de acciones. La idea es que no necesitas definir tanto la constante ADD_TODO como el creador de acciones addTodo(). Internamente, estas soluciones siguen generando constantes de tipo de acción, pero se crean implícitamente, lo que añade un nivel de indirección y puede causar confusión. Recomendamos crear explícitamente tus constantes de tipo de acción.
Escribir creadores de acciones simples puede ser tedioso y a menudo genera código repetitivo:
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}
export function editTodo(id, text) {
return {
type: 'EDIT_TODO',
id,
text
}
}
export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
}
}
Siempre puedes escribir una función que genere un creador de acciones:
function makeActionCreator(type, ...argNames) {
return function (...args) {
const action = { type }
argNames.forEach((arg, index) => {
action[argNames[index]] = args[index]
})
return action
}
}
const ADD_TODO = 'ADD_TODO'
const EDIT_TODO = 'EDIT_TODO'
const REMOVE_TODO = 'REMOVE_TODO'
export const addTodo = makeActionCreator(ADD_TODO, 'text')
export const editTodo = makeActionCreator(EDIT_TODO, 'id', 'text')
export const removeTodo = makeActionCreator(REMOVE_TODO, 'id')
También existen bibliotecas de utilidades que ayudan a generar creadores de acciones, como redux-act y redux-actions. Estas pueden ayudar a reducir el código repetitivo y garantizar el cumplimiento de estándares como Flux Standard Action (FSA).
Creadores de acciones asíncronos
El middleware te permite inyectar lógica personalizada que interpreta cada objeto de acción antes de ser despachado. Las acciones asíncronas son el caso de uso más común para middleware.
Sin ningún middleware, dispatch solo acepta objetos simples, por lo que tenemos que realizar llamadas AJAX dentro de nuestros componentes:
actionCreators.js
export function loadPostsSuccess(userId, response) {
return {
type: 'LOAD_POSTS_SUCCESS',
userId,
response
}
}
export function loadPostsFailure(userId, error) {
return {
type: 'LOAD_POSTS_FAILURE',
userId,
error
}
}
export function loadPostsRequest(userId) {
return {
type: 'LOAD_POSTS_REQUEST',
userId
}
}
UserInfo.js
import { Component } from 'react'
import { connect } from 'react-redux'
import {
loadPostsRequest,
loadPostsSuccess,
loadPostsFailure
} from './actionCreators'
class Posts extends Component {
loadData(userId) {
// Injected into props by React Redux `connect()` call:
const { dispatch, posts } = this.props
if (posts[userId]) {
// There is cached data! Don't do anything.
return
}
// Reducer can react to this action by setting
// `isFetching` and thus letting us show a spinner.
dispatch(loadPostsRequest(userId))
// Reducer can react to these actions by filling the `users`.
fetch(`http://myapi.com/users/${userId}/posts`).then(
response => dispatch(loadPostsSuccess(userId, response)),
error => dispatch(loadPostsFailure(userId, error))
)
}
componentDidMount() {
this.loadData(this.props.userId)
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.loadData(this.props.userId)
}
}
render() {
if (this.props.isFetching) {
return <p>Loading...</p>
}
const posts = this.props.posts.map(post => (
<Post post={post} key={post.id} />
))
return <div>{posts}</div>
}
}
export default connect(state => ({
posts: state.posts,
isFetching: state.isFetching
}))(Posts)
Sin embargo, esto rápidamente se vuelve repetitivo porque diferentes componentes solicitan datos de los mismos endpoints de API. Además, queremos reutilizar parte de esta lógica (por ejemplo, salida anticipada cuando hay datos en caché disponibles) desde muchos componentes.
El middleware nos permite escribir creadores de acciones más expresivos, potencialmente asíncronos. Nos permite despachar algo más que objetos simples e interpretar esos valores. Por ejemplo, el middleware puede "capturar" Promesas despachadas y convertirlas en un par de acciones de solicitud y éxito/fracaso.
El ejemplo más simple de middleware es redux-thunk. El middleware "Thunk" te permite escribir creadores de acciones como "thunks", es decir, funciones que devuelven funciones. Esto invierte el control: recibirás dispatch como argumento, por lo que puedes escribir un creador de acciones que despache múltiples veces.
Nota
El middleware Thunk es solo un ejemplo de middleware. El middleware no se trata de "permitirte despachar funciones". Se trata de permitirte despachar cualquier cosa que el middleware particular que uses sepa manejar. El middleware Thunk añade un comportamiento específico cuando despachas funciones, pero realmente depende del middleware que uses.
Considera el código anterior reescrito con redux-thunk:
actionCreators.js
export function loadPosts(userId) {
// Interpreted by the thunk middleware:
return function (dispatch, getState) {
const { posts } = getState()
if (posts[userId]) {
// There is cached data! Don't do anything.
return
}
dispatch({
type: 'LOAD_POSTS_REQUEST',
userId
})
// Dispatch vanilla actions asynchronously
fetch(`http://myapi.com/users/${userId}/posts`).then(
response =>
dispatch({
type: 'LOAD_POSTS_SUCCESS',
userId,
response
}),
error =>
dispatch({
type: 'LOAD_POSTS_FAILURE',
userId,
error
})
)
}
}
UserInfo.js
import { Component } from 'react'
import { connect } from 'react-redux'
import { loadPosts } from './actionCreators'
class Posts extends Component {
componentDidMount() {
this.props.dispatch(loadPosts(this.props.userId))
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.props.dispatch(loadPosts(this.props.userId))
}
}
render() {
if (this.props.isFetching) {
return <p>Loading...</p>
}
const posts = this.props.posts.map(post => (
<Post post={post} key={post.id} />
))
return <div>{posts}</div>
}
}
export default connect(state => ({
posts: state.posts,
isFetching: state.isFetching
}))(Posts)
¡Esto requiere mucho menos código! Si lo prefieres, aún puedes tener creadores de acciones "básicos" como loadPostsSuccess que usarías desde un creador de acciones contenedor loadPosts.
Finalmente, puedes escribir tu propio middleware. Supongamos que quieres generalizar el patrón anterior y describir tus creadores de acciones asíncronos así:
export function loadPosts(userId) {
return {
// Types of actions to emit before and after
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
// Check the cache (optional):
shouldCallAPI: state => !state.posts[userId],
// Perform the fetching:
callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
// Arguments to inject in begin/end actions
payload: { userId }
}
}
El middleware que interpreta tales acciones podría verse así:
function callAPIMiddleware({ dispatch, getState }) {
return next => action => {
const { types, callAPI, shouldCallAPI = () => true, payload = {} } = action
if (!types) {
// Normal action: pass it on
return next(action)
}
if (
!Array.isArray(types) ||
types.length !== 3 ||
!types.every(type => typeof type === 'string')
) {
throw new Error('Expected an array of three string types.')
}
if (typeof callAPI !== 'function') {
throw new Error('Expected callAPI to be a function.')
}
if (!shouldCallAPI(getState())) {
return
}
const [requestType, successType, failureType] = types
dispatch(
Object.assign({}, payload, {
type: requestType
})
)
return callAPI().then(
response =>
dispatch(
Object.assign({}, payload, {
response,
type: successType
})
),
error =>
dispatch(
Object.assign({}, payload, {
error,
type: failureType
})
)
)
}
}
Después de pasarlo una vez a applyMiddleware(...middlewares), puedes escribir todos tus creadores de acciones que llaman a la API de la misma manera:
export function loadPosts(userId) {
return {
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
shouldCallAPI: state => !state.posts[userId],
callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
payload: { userId }
}
}
export function loadComments(postId) {
return {
types: [
'LOAD_COMMENTS_REQUEST',
'LOAD_COMMENTS_SUCCESS',
'LOAD_COMMENTS_FAILURE'
],
shouldCallAPI: state => !state.comments[postId],
callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`),
payload: { postId }
}
}
export function addComment(postId, message) {
return {
types: [
'ADD_COMMENT_REQUEST',
'ADD_COMMENT_SUCCESS',
'ADD_COMMENT_FAILURE'
],
callAPI: () =>
fetch(`http://myapi.com/posts/${postId}/comments`, {
method: 'post',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ message })
}),
payload: { postId, message }
}
}
Reductores
Redux reduce considerablemente el código repetitivo (boilerplate) de las tiendas Flux al describir la lógica de actualización como una función. Una función es más simple que un objeto, y mucho más simple que una clase.
Considera esta tienda Flux:
const _todos = []
const TodoStore = Object.assign({}, EventEmitter.prototype, {
getAll() {
return _todos
}
})
AppDispatcher.register(function (action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
const text = action.text.trim()
_todos.push(text)
TodoStore.emitChange()
}
})
export default TodoStore
Con Redux, la misma lógica de actualización puede describirse como una función reductora:
export function todos(state = [], action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
const text = action.text.trim()
return [...state, text]
default:
return state
}
}
La sentencia switch no es el verdadero código repetitivo. El verdadero código repetitivo de Flux es conceptual: la necesidad de emitir una actualización, la necesidad de registrar el Store con un Dispatcher, la necesidad de que el Store sea un objeto (y las complicaciones que surgen cuando se desea una aplicación universal).
Es una lástima que muchos sigan eligiendo el framework Flux basándose en si usa sentencias switch en la documentación. Si no te gusta switch, puedes solucionarlo con una única función, como mostramos a continuación.
Generando Reductores
Escribamos una función que nos permita expresar los reductores como un objeto que mapea tipos de acción a manejadores. Por ejemplo, si queremos que nuestros reductores todos se definan así:
export const todos = createReducer([], {
[ActionTypes.ADD_TODO]: (state, action) => {
const text = action.text.trim()
return [...state, text]
}
})
Podemos escribir la siguiente función auxiliar para lograrlo:
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
¿No fue difícil, verdad? Redux no proporciona dicha función de ayuda por defecto porque hay muchas formas de escribirla. Tal vez quieras que convierta automáticamente objetos JS simples a objetos Immutable para hidratar el estado del servidor. Tal vez quieras fusionar el estado devuelto con el estado actual. Puede haber diferentes enfoques para un manejador “atrapa todo”. Todo esto depende de las convenciones que elijas para tu equipo en un proyecto específico.
¿No fue difícil, verdad? Redux no proporciona dicha función auxiliar por defecto porque hay muchas formas de implementarla. Quizás quieras convertir automáticamente objetos JS planos en objetos Inmutables para hidratar el estado del servidor. Tal vez prefieras fusionar el estado devuelto con el estado actual. Puede haber diferentes enfoques para un manejador "catch all". Todo depende de las convenciones que elijas para tu equipo en un proyecto específico.
La API de los reductores de Redux es (state, action) => newState, pero cómo creas esos reductores depende completamente de ti.