如何正确处理双嵌套数组的状态(ReactJS + Redux)

How to properly handle state in double nested arrays (ReactJS + Redux)?

本文关键字:ReactJS Redux 状态 正确处理 嵌套 数组      更新时间:2023-09-26

我试图建立以下内容:一个论坛,在那里可以创建一个帖子,触发ADD_POST,并创建该帖子,并添加到'posts'对象数组。每个"post"对象初始化为一个"comments"数组,该数组将保存在该帖子中输入的评论文本("commentTxt")。

let postReducer = function(posts = [],  action) {
  switch (action.type) {
    case 'ADD_POST':
      return [{
        id: getId(posts), //just calls a function that provides an id that increments by 1 starting from 0
        comments: [
          {
            id: getId(posts),
            commentTxt: ''
          }
        ]
      }, ...posts]

然后,当用户输入该帖子时,有一个评论部分,用户可以在其中输入评论文本,并将一个新对象(通过'ADD_COMMENT')添加到'posts.comments'数组

  case 'ADD_COMMENT':
      return posts.map(function(post){
    //find the right 'post' object in the 'posts' array to update the correct 'comments' array. 
        if(post.id === action.id){
    //update 'comments' object array of a 'post' object by adding a new object that contains 'commentTxt', and replaces the current 'comments' array
          return post.comments = [{
            id: action.id,
    //new object is made with text entered (action.commentTxt) and added to 'post.comments' array
            commentTxt: action.commentTxt
          }, ...post.comments]
        }
      })

并显示它。每次添加新的注释时,都会在数组中呈现一个新的注释对象和先前的注释对象。想要做如下的事情:

      {
        this.props.post.comments.map((comment) => {
          return <Comment key={comment.id} comment={comment} actions={this.props.actions}/>
        })
      }

我听说直接改变状态是不推荐的,所以我将感谢任何指导或见解如何正确地这样做。

您可以考虑规范化您的数据。因此,与其像这样存储结构体:

posts: [{
  title: 'Some post',
  comments: [{
    text: 'Hey there'
  }]
}]

你可以这样存储它们:

posts: [{
  id: 1,
  title: 'Some post'
}]
comments: [{
  id: 4,
  postId: 1,
  text: 'Hey there'
}]

一开始比较麻烦,但是有很大的灵活性。

或者,您可以修改您的ADD_COMMENT reducer:

return posts.map(function(post) { 
  if (post.id !== action.id) {
    return post
  }

  return {
    ...post, 
    comments: [
      ...post.comments, 
      { 
        id: action.id, 
        commentTxt: action.commentTxt 
      }
    ]
  }
}

注意:在最后一个解决方案中,没有突变。我不知道如果有大量的注释,它会如何运行,但除非你有很好的理由,否则我不会对这种情况进行预优化。

正如Christopher Davies在他的回答中所说,您应该使您的状态正常化。假设我们有一个这样的形状:

const exampleState = {
    posts: {
        '123': {
            id: '123',
            title: 'My first post',
            comments: []  // an array of comments ids
        },
        '456': {
            id: '456',
            title: 'My second post',
            comments: []  // an array of comments ids
        }
    },
    comments: {
        'abc': {
            id: 'abc',
            text: 'Lorem ipsum'
        },
        'def': {
            id: 'def',
            text: 'Dolor sit'
        },
        'ghi': {
            id: 'ghi',
            text: 'Amet conseguir'
        }
    }
}

好的,现在让我们编写一些动作创建器来创建可以改变状态的动作:

const addPost = (post) => ({
    type: 'ADD_POST',
    post
})
const addComment = (postId, comment) => ({  // for the sake of example, let's say the "comment" object here is a comment object returned by some ajax request and having it's own id
    type: 'ADD_COMMENT',
    postId,
    comment
})

然后,您将需要两个reducer来处理posts片和comments片:

const postsReducer = (posts = {}, action = {}) => {
    switch(action.type) {
        case 'ADD_POST':
            const id = getId(posts)
            return {
                ...posts,
                [id]: action.post
            }
        case 'ADD_COMMENT':
            return {
                ...posts.map(p => {
                    if (p.id == action.postId) {
                        return {
                            ...p,
                            comments: p.comments.concat([action.comment.id])
                        }
                    }
                    return p
                })
            }
        default:
            return state
    }
}
const commentsReducer = (comments = {}, action = {}) => {
    switch(action.type) {
        case 'ADD_COMMENT':
            return {
                ...comments,
                [action.comment.id]: action.comment
            }
        default:
            return state
    }
}

让我们还创建一些选择器来从状态中获取数据:

const getPost = (state, id) => state.posts[id]
const getCommentsForPost = (state, id) => ({
    const commentsIds = state.posts[id].comments
    return state.comments.filter(c => commentsIds.includes(c.id))
})

然后,你的组件:

const PostLists = (posts) => (
    <ul>
        {posts.map(p) => <Post key={p} id={p} />}
    </ul>
)
PostLists.propTypes = {
    posts: React.PropTypes.arrayOf(React.PropTypes.string)  //just an id of posts
}

const Post = ({id, title, comments}) => (
    <li>
        {title}
        {comments.map(c) => <Comment key={c.id} {...c}/>}
    </li>
)
Post.propTypes = {
    id: React.PropTypes.string,
    comments: React.PropTypes.arrayOf(React.PropTypes.shape({
        id: React.PropTypes.string,
        text: React.PropTypes.text
    }))
}

const Comment = ({ id, text }) => (
    <p>{text}</p>
)

现在,连接的容器:

// the mapStateToProps if very simple here, we just extract posts ids from state
const ConnectedPostLists = connect(
    (state) => ({
        posts: Objects.keys(state.posts)
    })
)(PostLists)

// The ConnectedPost could be written naively using the component props passed as the second argument of mapStateToProps :
const ConnectedPost = connect(
    (state, { id }) => ({
        id,
        title: getPost(state, id).title,
        comments: getCommentsForPost(state, id)
    })
)(Post)

这是要工作的,但是,如果你有很多帖子,你会遇到ConnectedPost组件的性能问题,因为mapStateToProps依赖于组件自己的道具将触发连接组件的重新渲染在状态的任何变化

所以我们应该这样重写:

// Since the post id is never intended to change for this particular post, we can write the ConnectedPost like this :
const ConnectedPost = connect(
    (_, { id}) => (state) => ({
        id,
        title: getPost(state, id).title,
        comments: getCommentsForPost(state, id)
    })
)

好啦!我没有测试这个例子,但我认为它可以帮助你看到你需要去的方向