1

I am trying to build the following: a forum where one can create a post, which triggers the ADD_POST, and the post is created, and added to the 'posts' object array. The each 'post' object is initialized with a 'comments' array that will hold comment texts ('commentTxt') entered inside that post.

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]

Then when the user enters that post, there is a comment section where the user can enter a comment text and a new object would be added (via 'ADD_COMMENT') to the 'posts.comments' array

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

and would display it. And every time a new comment is added, a new would be rendered along with the previous comment objects in array. Would want to do something like the following:

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

I heard mutating state directly is not recommended, so I would appreciate any guidance or insight on how to properly do so.

2 Answers 2

1

You might consider normalizing your data. So, instead of storing your structure like this:

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

You'd store them like this:

posts: [{
  id: 1,
  title: 'Some post'
}]

comments: [{
  id: 4,
  postId: 1,
  text: 'Hey there'
}]

It's more of a pain at first, but allows a lot of flexibility.

Alternatively, you could modify your 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 
      }
    ]
  }
}

Note: In this last solution, there are no mutations. Don't know how it would perform with tons of comments, but I wouldn't pre-optimize for that scenario unless you have good reason.

Sign up to request clarification or add additional context in comments.

6 Comments

In where { this.props.post.comments.map((comment) => { return <Comment key={comment.id} comment={comment} actions={this.props.actions}/> }) } is placed, how would you modify it accordingly based on the new revision of ADD_COMMENT, and access the state returned from 'ADD_COMMENT' reducer via mapStateToProps and mapDispatchToProps?
I don't think that any of your code would have to change, other than the reducer, as the shape of the data remains untouched. Or at least, I intended to not change the shape of the data from what you were expecting.
I am just stuck on how I should call the state inside the mapStateToProps to be able to do such as 'this.props.post.comments'. Also, where would I do the normalizing of data? Sorry but if you don't mind, could you show a full example. Can't seem to wrap my head around it.
Gotcha. Sorry, maybe my answer was vague. I was saying you could normalize, but you don't have to. The ADD_COMMENTS reducer I showed is assuming you don't normalize. You can just keep the mapStateToProps function that you already have. As long as it was already returning the right post, it'll still return it and that post will have comments.
You definitively should normalize your data, by doing so you can connect your <Forum/> component to only send in mapStateToProps the array of post ids, then, you can connect each <Post/> component to only send in mapStateToProps the post data + a list of comments ids, and so on.
|
1

As stated by Christopher Davies in his answer, you should normalize your state. Let's say we have a shape like this :

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

Ok, now let's write some actions creators that creates action that will mutate the state :

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

Then, you will need two reducers to handle the posts slice, and the comments slice :

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

Let's also create some selectors to pick up data from the 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))
})

Then, your components :

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

And now, the connected containers :

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

That is going to work BUT, if you have many posts, you will hit performance issues with the ConnectedPost component because mapStateToProps that depends on component own props will trigger a re-render of the connected component for any change in the state

So we should rewrite it like this :

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

And voilà ! I didn't test this example, but I think it can help you to see in which direction you need to go.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.