【问题标题】:How to properly handle state in double nested arrays (ReactJS + Redux)?如何正确处理双嵌套数组(ReactJS + Redux)中的状态?
【发布时间】:2016-06-29 17:00:19
【问题描述】:

我正在尝试构建以下内容:一个可以创建帖子的论坛,它会触发 ADD_POST,然后创建帖子,并将其添加到“帖子”对象数组中。每个“post”对象都使用一个“cmets”数组进行初始化,该数组将保存在该帖子中输入的评论文本(“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.cmets”数组中

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

我听说不建议直接改变状态,因此我将不胜感激有关如何正确执行此操作的任何指导或见解。

【问题讨论】:

    标签: javascript reactjs redux react-jsx


    【解决方案1】:

    您可以考虑规范化您的数据。所以,不要像这样存储你的结构:

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

    你会像这样存储它们:

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

    一开始比较痛苦,但有很大的灵活性。

    或者,您可以修改 ADD_COMMENT 减速器:

    return posts.map(function(post) { 
      if (post.id !== action.id) {
        return post
      }
    
    
      return {
        ...post, 
        comments: [
          ...post.comments, 
          { 
            id: action.id, 
            commentTxt: action.commentTxt 
          }
        ]
      }
    }
    

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

    【讨论】:

    • 在哪里 { this.props.post.cmets.map((comment) => { return }) } 被放置了,你会如何根据新版本的 ADD_COMMENT 进行相应的修改,并通过 mapStateToProps 和 mapDispatchToProps 访问 'ADD_COMMENT' reducer 返回的状态?
    • 我认为您的任何代码都不需要更改,除了 reducer,因为数据的形状保持不变。或者至少,我打算不改变你所期望的数据形状。
    • 我只是坚持我应该如何调用 mapStateToProps 中的状态才能执行诸如“this.props.post.cmets”之类的操作。另外,我将在哪里对数据进行规范化?对不起,如果你不介意,你能举一个完整的例子吗?我似乎无法理解它。
    • 明白了。对不起,也许我的回答很模糊。我是说你可以正常化,但你不必这样做。我展示的 ADD_COMMENTS 减速器假设您没有标准化。您可以保留已有的 mapStateToProps 函数。只要它已经返回正确的帖子,它仍然会返回它并且该帖子将具有 cmets。
    • 你绝对应该规范你的数据,这样做你可以connect你的&lt;Forum/&gt;组件只发送mapStateToProps帖子ID数组,然后,你可以connect每个@ 987654329@ 组件仅发送mapStateToProps 帖子数据+ cmets id 列表等。
    【解决方案2】:

    正如克里斯托弗戴维斯在他的回答中所说,你应该规范你的状态。假设我们有这样的形状:

    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 来处理帖子切片和 cmets 切片:

    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 依赖于组件自己的 props will trigger a re-render of the connected component for any change in the state

    所以我们应该这样重写它:

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

    瞧!我没有测试这个示例,但我认为它可以帮助您了解您需要朝哪个方向发展。

    【讨论】:

      猜你喜欢
      • 2019-07-05
      • 2021-06-07
      • 1970-01-01
      • 1970-01-01
      • 2017-04-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-05-26
      相关资源
      最近更新 更多