Part 8 of the course, video 4

4-Selectively loading comments

Right now, when we write a comments, we emit an action with a payload of comments to update the state to contain an array with all previous comments plus new comments. This array is being mapped inside each and every post (or each and every 'Comments') -> therefore we need a way to store comments for specifid ids.

So here is what we are going to do:

Now, when we dispatch the addComment action, we don't want to just pass the 'comment' as a payload, but we also want to pass the post id to which we submitted to comment. Also, instead of returning an array of comments for state.comments (for this part of the state), we are going to return an object. This object will have properties of post-id, and each property will have its own array of comments.

Note to self: this actually sounds good.

Go to action.js and update the addComment action as follows

action.js


export function addComment(comment, postId){
    return{
        type: 'ADD_COMMENT',
        comment,
        postId
    }

}

Now go to Single, and make sure you also pass the id of the post down as props to the Comment component (so that the Comment component will know which comments it is supposed to fetch).

Single - edited line

//this
<Comments addComment = {this.props.addComment}  comments={comments}/>


//will be updated to this:
                    <Comments addComment = {this.props.addComment}  comments={comments} id={id}/>


Now go to Comments, and in the place where we call the action (inside handleSubmit), pass the payload 'id' alongside the payload 'comment' (action is currently expecting both):

Comments - handleSubmit:

  handleSubmit(event){
        event.preventDefault()
        const comment = event.target.elements.comment.value
        this.props.addComment(comment, this.props.id)  
    }

Now go to the reducer, and console.log the post id inside the 'ADD_COMMENT' case (to make sure things are working propely first).

Reducer - comment reducer

function comments (state = [], action){
    switch(action.type){
        case 'ADD_COMMENT': 
        console.log(action.comment)
        return [...state, action.comment]
        default: return state
    }
};

Everything worked good.

So now transform the return type of the state from array to object and some other edits:

Reducer - comment reducer

function comments (state = {}, action){
    switch(action.type){
        case 'ADD_COMMENT': 
        if (!state[action.postId]){ //if the post we are adding comments to have no earlier comments
            return {...state, [action.postId]: [action.comment]}
        }
        else{
            return {...state, [action.postId]: [...state[action.postId], action.comment]}
        }
        default: return state
    }
};

Note on what happened:

In the case where we are entering a comment for a post that has no comments yet: A new comment is added and the relevant action is called, the reducer will now return an object, that has original state object, in addition to one extra property, where the key-value pair is as follows: "id of post": array of comments

Note: when we did this: {...state, action.postId: action.comment} we got an error, because we are delcaring a varialbe action.postId inside an object. ES6 allows declaration of variables inside objects provided we use an 'array' syntax, so we did the following: {...state, [action.postId]:[action.comment]}. Note that it does not become an array, it is just how ES6 allows us to deal with it.

In the case where we are entering a comment for a post that already has comments: We will return the state as is, then we will access the state.postId property of the state (which belongs to current post), and will update its array of comments by returning previous comments as is, but adding to them the new comment.

Now: since each comment component is interested in displaying comments for the only the post it belongs to, it is better not to pass the whole comment object to it -> better to just pass the part of the comment state that is relevant to it.

So go to Single.js and edit the following

Single - constants section

//before
  const {match, posts} = this.props;
            const id = Number(match.params.id) 
            const post = posts.find((post) => post.id === id);
            const comments = this.props.comments

//after
              const {match, posts} = this.props;
            const id = Number(match.params.id) 
            const post = posts.find((post) => post.id === id);
            const comments = this.props.comments[match.params.id] //note, we could have used 'id' directly because it is present above (the id const)

Now go to Comments.

When we first open the app, we will have no comments yet. So if we attempted to loop throught the array of comments specific to a post inside a Comment Component, it will just crash (because it is trying to access an unexsiting property of the state.comments object). So we should create a logic operator when we are passing down the props -> inside Single as follows:

Single - comments const declaration line

//before:
const comments = this.props.comments[match.params.id]

//after
const comments = this.props.comments[match.params.id] || []

We used the or logic operator (||) to check if o'this.props.comments....' is undefined. In case it was, then we will just return an empty array, and then we would have no problem looping inside an empty array in comments (acceptable).

Go back to Comments and uncomment this section:

 {
                this.props.comments.map((comment, index) => {
                    return(
                        <p key = {index}> {comment} </p>
                    )
                })
            }

And everything should work properly.


Except that if we tried to remove a post from Photo contained inside Single, we will get an error -> because it is calling an action which it does not have access to.

We want to be able to remove a post from within Single (when it is open for comments) - we will then return to the main page and have it removed.

Keep in mind that Photo inside PhotoWall can remove itself, because PhotoWall passes the action down to it, however Single does not pass the action down to Photo, and that is what we are going to do now.

Go to Single and edit as follows:

Single - one line edit

//before
<Photo post = {post}/>

//after
<Photo post = {post}  {...this.props}/>

We passed down all the props of Single down to Photo. However Photo (son of Single) is not ready to call the action yet. Here is how it currently calls the action:

<button className="remove-button" onClick={() => {
                        this.props.removePost(this.props.index);
                    }}>Remove</button>

It has access to removePost now that we passed to props from Single, however 'index' is not part of the props passed down, and so we need to pass that down too.

Go back to Single and edit the following:

Single - Render (before and after)

//before
 render(){
            const {match, posts} = this.props;
            const id = Number(match.params.id) 
            const post = posts.find((post) => post.id === id);
            const comments = this.props.comments[match.params.id] || []
        return(
            <div className="single-photo">
                <Photo post = {post}  {...this.props}/>
                <Comments addComment = {this.props.addComment}  comments={comments} id={id}/>

            </div>
        )
    }
//after
     render(){
            const {match, posts} = this.props;
            const id = Number(match.params.id) 
            const post = posts.find((post) => post.id === id);
            const comments = this.props.comments[match.params.id] || []
            const index = this.props.posts.findIndex((post) => post.id === id)
        return(
            <div className="single-photo">
                <Photo post = {post}  {...this.props} index = {index}/>
                <Comments addComment = {this.props.addComment}  comments={comments} id={id}/>

            </div>
        )
    }

In the above, we used a function called findIndex to find the index of the current post and then we passed down that index as props to Photo. Everything should work out well now.

But we also want to go directly back to original page when we delete a picture, so now go to Photo and edit the onClick function:

Photo - button onClick

//before
  <button className="remove-button" onClick={() => {
                        this.props.removePost(this.props.index);
                        this.props.history.push('/')
                    }}>Remove</button>

//after
  <button className="remove-button" onClick={() => {
                        this.props.removePost(this.props.index);
                        this.props.history.push('/')
                    }}>Remove</button>

Okay, it works now.

5-Comment field

Doing some fixes to the comment field (since it currently doesn't remove old comment from text field once it is submitted). And finalizing some stuff.

So go to Comments, and edit the handleSubmit function:

Comments - handleSubmit

//before
 handleSubmit(event){
        event.preventDefault()
        const comment = event.target.elements.comment.value
        this.props.addComment(comment, this.props.id)  
    }

//after
   handleSubmit(event){
        event.preventDefault()
        const comment = event.target.elements.comment.value
        this.props.addComment(comment, this.props.id)  
        event.target.elements.comment.value = ''
    }

That's it.

Now we also want to add a small link to the comment section below the picture (right-hand side). And we want it to display the number of comments so far.

So go to Photo, and add the following line before the closing tag of the 'button-container' div:

Photo -- added line

 <Link className="button" to = {`/single/${post.id}`}>
                        <div className="comment-count">
                            <div className="speech-bubble"></div>
                            {this.props.comments[post.id] ? this.props.comments[post.id].length : 0}
                        </div>
                    </Link>

Remembering the '?' conditional:

If we just put this.props.comments[post.id].length, it will crash in the case when it is undefined. Therefore we used the '?' to check if defined. If yes, return stuff before ':'. If no, just return 0 (meaning 0 number of comments). That's it.

The comment bubble is pure CSS. Nice.

This is it for this section. Next we will be connecting the app to firebase.

Remeber that you have all the source code inside the files.