Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
Gestión de datos normalizados
Como se mencionó en Normalización de la estructura del estado, la biblioteca Normalizr se utiliza frecuentemente para transformar datos de respuesta anidados en una forma normalizada adecuada para integrarse en el store. Sin embargo, esto no aborda el problema de ejecutar actualizaciones posteriores a esos datos normalizados mientras se utilizan en otras partes de la aplicación. Existen varios enfoques diferentes que puedes utilizar, según tus preferencias. Usaremos el ejemplo de manejar mutaciones de Comentarios en una Publicación.
Enfoques estándar
Fusión simple
Un enfoque es fusionar el contenido de la acción en el estado existente. En este caso, podemos usar una fusión recursiva profunda, no solo una copia superficial, para permitir que acciones con elementos parciales actualicen los elementos almacenados. La función merge de Lodash puede manejar esto por nosotros:
import merge from 'lodash/merge'
function commentsById(state = {}, action) {
switch (action.type) {
default: {
if (action.entities && action.entities.comments) {
return merge({}, state, action.entities.comments.byId)
}
return state
}
}
}
Esto requiere menos trabajo en el lado del reducer, pero exige que el creador de acciones potencialmente realice bastante trabajo para organizar los datos en la forma correcta antes de despachar la acción. Tampoco maneja intentos de eliminar un elemento.
Composición de slice reducers
Si tenemos un árbol anidado de slice reducers, cada slice reducer necesitará saber cómo responder adecuadamente a esta acción. Necesitaremos incluir todos los datos relevantes en la acción. Debemos actualizar el objeto Publicación correcto con el ID del comentario, crear un nuevo objeto Comentario usando ese ID como clave e incluir el ID del Comentario en la lista de todos los IDs de Comentarios. Así es como podrían encajar las piezas:
// actions.js
function addComment(postId, commentText) {
// Generate a unique ID for this comment
const commentId = generateId('comment')
return {
type: 'ADD_COMMENT',
payload: {
postId,
commentId,
commentText
}
}
}
// reducers/posts.js
function addComment(state, action) {
const { payload } = action
const { postId, commentId } = payload
// Look up the correct post, to simplify the rest of the code
const post = state[postId]
return {
...state,
// Update our Post object with a new "comments" array
[postId]: {
...post,
comments: post.comments.concat(commentId)
}
}
}
function postsById(state = {}, action) {
switch (action.type) {
case 'ADD_COMMENT':
return addComment(state, action)
default:
return state
}
}
function allPosts(state = [], action) {
// omitted - no work to be done for this example
}
const postsReducer = combineReducers({
byId: postsById,
allIds: allPosts
})
// reducers/comments.js
function addCommentEntry(state, action) {
const { payload } = action
const { commentId, commentText } = payload
// Create our new Comment object
const comment = { id: commentId, text: commentText }
// Insert the new Comment object into the updated lookup table
return {
...state,
[commentId]: comment
}
}
function commentsById(state = {}, action) {
switch (action.type) {
case 'ADD_COMMENT':
return addCommentEntry(state, action)
default:
return state
}
}
function addCommentId(state, action) {
const { payload } = action
const { commentId } = payload
// Just append the new Comment's ID to the list of all IDs
return state.concat(commentId)
}
function allComments(state = [], action) {
switch (action.type) {
case 'ADD_COMMENT':
return addCommentId(state, action)
default:
return state
}
}
const commentsReducer = combineReducers({
byId: commentsById,
allIds: allComments
})
El ejemplo es un poco extenso porque muestra cómo encajan todos los diferentes slice reducers y case reducers. Observa la delegación involucrada aquí. El slice reducer postsById delega el trabajo de este caso a addComment, que inserta el ID del nuevo Comentario en el elemento Publicación correcto. Mientras tanto, los slice reducers commentsById y allComments tienen sus propios case reducers que actualizan adecuadamente la tabla de búsqueda de Comentarios y la lista de todos los IDs de Comentarios.
Otros enfoques
Actualizaciones basadas en tareas
Dado que los reducers son simplemente funciones, existen infinitas formas de dividir esta lógica. Aunque usar slice reducers es lo más común, también es posible organizar el comportamiento de forma más orientada a tareas. Como esto a menudo implica actualizaciones más anidadas, quizá quieras usar una biblioteca de utilidades para actualizaciones inmutables como dot-prop-immutable u object-path-immutable para simplificar las declaraciones de actualización. Aquí un ejemplo de cómo podría verse:
import posts from "./postsReducer";
import comments from "./commentsReducer";
import dotProp from "dot-prop-immutable";
import {combineReducers} from "redux";
import reduceReducers from "reduce-reducers";
const combinedReducer = combineReducers({
posts,
comments
});
function addComment(state, action) {
const {payload} = action;
const {postId, commentId, commentText} = payload;
// State here is the entire combined state
const updatedWithPostState = dotProp.set(
state,
`posts.byId.${postId}.comments`,
comments => comments.concat(commentId)
);
const updatedWithCommentsTable = dotProp.set(
updatedWithPostState,
`comments.byId.${commentId}`,
{id : commentId, text : commentText}
);
const updatedWithCommentsList = dotProp.set(
updatedWithCommentsTable,
`comments.allIds`,
allIds => allIds.concat(commentId);
);
return updatedWithCommentsList;
}
const featureReducers = createReducer({}, {
ADD_COMMENT : addComment,
});
const rootReducer = reduceReducers(
combinedReducer,
featureReducers
);
Este enfoque deja muy claro lo que sucede para el caso "ADD_COMMENTS", pero requiere lógica de actualización anidada y cierto conocimiento específico de la forma del árbol de estado. Dependiendo de cómo quieras componer tu lógica de reducer, esto puede ser deseable o no.
Redux-ORM
La biblioteca Redux-ORM proporciona una capa de abstracción muy útil para gestionar datos normalizados en un store de Redux. Te permite declarar clases Modelo y definir relaciones entre ellas. Puede generar "tablas" vacías para tus tipos de datos, actuar como herramienta especializada de selección para buscar datos y realizar actualizaciones inmutables sobre esos datos.
Existen un par de formas en que Redux-ORM puede usarse para realizar actualizaciones. Primero, la documentación de Redux-ORM sugiere definir funciones reducer en cada subclase Modelo, luego incluir la función reducer combinada generada automáticamente en tu store:
// models.js
import { Model, fk, attr, ORM } from 'redux-orm'
export class Post extends Model {
static get fields() {
return {
id: attr(),
name: attr()
}
}
static reducer(action, Post, session) {
switch (action.type) {
case 'CREATE_POST': {
Post.create(action.payload)
break
}
}
}
}
Post.modelName = 'Post'
export class Comment extends Model {
static get fields() {
return {
id: attr(),
text: attr(),
// Define a foreign key relation - one Post can have many Comments
postId: fk({
to: 'Post', // must be the same as Post.modelName
as: 'post', // name for accessor (comment.post)
relatedName: 'comments' // name for backward accessor (post.comments)
})
}
}
static reducer(action, Comment, session) {
switch (action.type) {
case 'ADD_COMMENT': {
Comment.create(action.payload)
break
}
}
}
}
Comment.modelName = 'Comment'
// Create an ORM instance and hook up the Post and Comment models
export const orm = new ORM()
orm.register(Post, Comment)
// main.js
import { createStore, combineReducers } from 'redux'
import { createReducer } from 'redux-orm'
import { orm } from './models'
const rootReducer = combineReducers({
// Insert the auto-generated Redux-ORM reducer. This will
// initialize our model "tables", and hook up the reducer
// logic we defined on each Model subclass
entities: createReducer(orm)
})
// Dispatch an action to create a Post instance
store.dispatch({
type: 'CREATE_POST',
payload: {
id: 1,
name: 'Test Post Please Ignore'
}
})
// Dispatch an action to create a Comment instance as a child of that Post
store.dispatch({
type: 'ADD_COMMENT',
payload: {
id: 123,
text: 'This is a comment',
postId: 1
}
})
La biblioteca Redux-ORM mantiene las relaciones entre modelos por ti. Las actualizaciones se aplican de forma inmutable por defecto, simplificando el proceso de actualización.
Otra variación es usar Redux-ORM como capa de abstracción dentro de un único case reducer:
import { orm } from './models'
// Assume this case reducer is being used in our "entities" slice reducer,
// and we do not have reducers defined on our Redux-ORM Model subclasses
function addComment(entitiesState, action) {
// Start an immutable session
const session = orm.session(entitiesState)
session.Comment.create(action.payload)
// The internal state reference has now changed
return session.state
}
Al utilizar la interfaz de sesión, ahora puedes usar los accesores de relaciones para acceder directamente a los modelos referenciados:
const session = orm.session(store.getState().entities)
const comment = session.Comment.first() // Comment instance
const { post } = comment // Post instance
post.comments.filter(c => c.text === 'This is a comment').count() // 1
En general, Redux-ORM proporciona un conjunto de abstracciones muy útil para:
- Definir relaciones entre tipos de datos
- Crear las "tablas" en nuestro estado
- Recuperar y desnormalizar datos relacionales
- Aplicar actualizaciones inmutables a datos relacionales