Spec-Zone.ru › Redux 3

Управление нормализованными данными

Как упоминалось в Нормализации формы состояния, библиотека Normalizr часто используется для преобразования вложенных данных ответа в нормализованную форму, подходящую для интеграции в хранилище. Однако это не решает проблему выполнения дополнительных обновлений этих нормализованных данных, поскольку они используются в других частях приложения. Существует множество различных подходов, которые вы можете использовать, исходя из ваших предпочтений. Мы рассмотрим пример добавления нового комментария к посту.

Стандартные подходы

Простое объединение

Один из подходов — объединить содержимое действия в существующее состояние. В этом случае нам необходимо выполнить глубокое рекурсивное объединение, а не просто поверхностную копию. Функция Lodash merge может справиться с этим за нас:

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;
        }
    }
}

Это требует наименьшего объема работы со стороны редьюсера, но требует, чтобы создатель действия, возможно, проделал значительную работу по организации данных в правильную форму перед отправкой действия. Также он не обрабатывает попытки удаления элемента.

Композиция редьюсеров срезов

Если у нас есть вложенное дерево редьюсеров срезов, каждый редьюсер среза должен знать, как соответствующим образом реагировать на это действие. Мы должны включить все соответствующие данные в действие. Мы должны обновить правильный объект Post с идентификатором комментария, создать новый объект Comment, используя этот идентификатор в качестве ключа, и включить идентификатор комментария в список всех идентификаторов комментариев. Вот как могут быть объединены эти части:

// 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
});

Пример немного длинный, потому что он показывает, как все различные редьюсеры срезов и редьюсеры случаев взаимодействуют друг с другом. Обратите внимание на делегирование, которое здесь используется. Редьюсер среза postsById делегирует работу по этому случаю addComment, который вставляет идентификатор нового комментария в правильный элемент Post. Между тем, у редьюсеров срезов commentsById и allComments есть свои собственные редьюсеры случаев, которые соответственно обновляют таблицу поиска Comment и список всех идентификаторов Comment.

Другие подходы

Обновления, основанные на задачах

Поскольку редьюсеры — это просто функции, существует бесконечное множество способов разбить эту логику. Хотя использование редьюсеров срезов, очевидно, является наиболее распространенным, также возможно организовать поведение в более ориентированной на задачи структуре. Поскольку это часто предполагает более вложенные обновления, вы можете использовать библиотеку утилиты для работы с неизменяемыми данными, такую как dot-prop-immutable или object-path-immutable, чтобы упростить операторы обновления. Вот пример того, как это может выглядеть:

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
);

Этот подход делает очень ясным, что происходит в случае "ADD_COMMENTS", но он требует вложенной логики обновления и определенных знаний о структуре дерева состояния. В зависимости от того, как вы хотите составить логику редьюсера, это может быть или не быть желательным.

Redux-ORM

Библиотека Redux-ORM предоставляет очень полезный абстракционный уровень для управления нормализованными данными в хранилище Redux. Она позволяет объявлять классы моделей и определять связи между ними. Затем она может генерировать пустые «таблицы» для ваших типов данных, выступать в качестве специализированного инструмента выбора для поиска данных и выполнять неизменяемые обновления этих данных.

Существует несколько способов использования Redux-ORM для выполнения обновлений. Во-первых, документация Redux-ORM предлагает определить функции редьюсеров для каждого подкласса модели, а затем включить автоматически сгенерированную объединенную функцию редьюсера в ваше хранилище:

// models.js
import {Model, many, Schema} from "redux-orm";

export class Post extends Model {
  static get fields() {
    return {
      // Define a many-sided relation - one Post can have many Comments, 
      // at a field named "comments"
      comments : many("Comment") 
    };
  }

  static reducer(state, action, Post) {
    switch(action.type) {
      case "CREATE_POST" : {
        // Queue up the creation of a Post instance
        Post.create(action.payload);
        break;
      }
      case "ADD_COMMENT" : {
        const {payload} = action;
        const {postId, commentId} = payload;
        // Queue up the addition of a relation between this Comment ID 
        // and this Post instance
        Post.withId(postId).comments.add(commentId);
        break;
      }
    }

    // Redux-ORM will automatically apply queued updates after this returns
  }
}
Post.modelName = "Post";

export class Comment extends Model {
  static get fields() {
    return {};
  }

  static reducer(state, action, Comment) {
    switch(action.type) {
      case "ADD_COMMENT" : {
        const {payload} = action;
        const {commentId, commentText} = payload;

        // Queue up the creation of a Comment instance
        Comment.create({id : commentId, text : commentText});
        break;
      }   
    }

    // Redux-ORM will automatically apply queued updates after this returns
  }
}
Comment.modelName = "Comment";

// Create a Schema instance, and hook up the Post and Comment models
export const schema = new Schema();
schema.register(Post, Comment);


// main.js
import { createStore, combineReducers } from 'redux'
import {schema} 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 : schema.reducer()
});

// 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 : {
    postId : 1,
    commentId : 123,
    commentText : "This is a comment"
  }
});

Библиотека Redux-ORM поддерживает внутреннюю очередь обновлений, которые должны быть применены. Затем эти обновления применяются неизменно, упрощая процесс обновления.

Другой вариант — использовать Redux-ORM в качестве абстракционного уровня в пределах одного редьюсера случая:

import {schema} 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) {
    const session = schema.from(entitiesState);
    const {Post, Comment} = session;
    const {payload} = action;
    const {postId, commentId, commentText} = payload;

    const post = Post.withId(postId);
    post.comments.add(commentId);

    Comment.create({id : commentId, text : commentText});

    return session.reduce();
}

В целом, Redux-ORM предоставляет очень полезный набор абстракций для определения связей между типами данных, создания «таблиц» в нашем состоянии, извлечения и денормализации реляционных данных и применения неизменяемых обновлений к реляционным данным.

© 2015–2017 Dan Abramov
Licensed under the MIT License.
http://redux.js.org/docs/recipes/reducers/UpdatingNormalizedData.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API