Reputation: 7575
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.
Upvotes: 1
Views: 1116
Reputation: 8686
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.
Upvotes: 1
Reputation: 4551
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.
Upvotes: 1