part 9 of the course, video 3

Updating Database

Some review on actions: now with thunk, we will dispatch special database related actions. So assume we want to add a new post to the project: it will get added to the database, and after doing that it will call our original normal actions to update our current state and rerender the UI. (If I am getting this correctly).

So go to actions.js and create a new action at the top:

action.js - new thunk action

import {database} from '../database/config'


//the below is a special thunk action

export function startAddingPost(post){ 
    return (dispatch) =>{
        return database.ref('posts').update({[post.id]:post}).then(() => {
            dispatch(addPost(post))
        })
    }
}


We want to update the database. So we imported it and then referenced posts (which is one part of our state represeted in the database).

Whenever we want to update a database, it has to be a key/value pair. Note that we are giving it the key in ES6 syntax ([post.id] is not an array). Also note that you are specifying the key value here and NOT leaving it up to firebase to create one.

We are calling the stuff inside then after the database has been updated. We called our normal action inside it. So always: update database, then update our state.

Now go to AddPhoto and alter the action call:

AddPhoto

//before
  if (description && imageLink){
            this.props.addPost(post)
            this.props.history.push('/');
        }

//after
 if (description && imageLink){
            this.props.startAddingPost(post)
            this.props.history.push('/');
        }

Note: the error think because you had too many store enhancers: https://github.com/jhen0409/react-native-debugger/issues/280

Okay, now you tried it, and everything works beautifully.

Just in case you got errors, update your action method with a catch statement so that it could handle errors:

action.js - function edit

export function startAddingPost(post){ 
    return (dispatch) =>{
        return database.ref('posts').update({[post.id]:post}).then(() => {
            dispatch(addPost(post))
        }).catch((error) =>{
            console.log(error)
        })
    }
}

Now, enter all the posts you have in data into your database, and initialize your post there to just an empty array (because the reducer has to initialize your state to empty if there is nothing, and it already relies on that posts array).

Now in case you refresh, you won't have nothing - because we haven't yet brought data from our database. That's is what we are going to do next.

4-Fetching posts

We will use an action creator to fetch the post from the database. Now it will also have an asynchronous method, but instead of updating the database, it will observe the database, grab the data, and use the dispatch method to load the posts into our store.

We need to create a companion action to take each loaded 'post' and add it to our state.

Here are the two actions

Actions

//loading from database

export function startLoadingPost(post){ 
    return (dispatch) =>{
        return database.ref('posts').once('value').then((snapshot) => {
            let posts = []
            snapshot.forEach((childSnapshot) => {
                posts.push(childSnapshot.val())
            })
             dispatch(loadPosts(posts))
        })
    }
}


//action to be called inside startLoadingPost

export function loadPosts(posts){ 
    return {
        type: 'LOAD_POSTS',
        posts               //the action payload is the posts we grab from the database
    }
}

//note: we can write .on or .once. The 'on' means that we are always going to listen to changes in our database, and whenever a change occurs, it is going to invoke the function. //this is what makes database realtime (we can keep obsserving). However we don't need that feature here, so we are going to use once, which means obsever it and when you get one change load it once and that's it. //So it observs the whole 'value' of the database, and if it was successful in 'observing it', we go to 'then', where a snapshot of all observed children will be returned. //now, we want the values inside the children too, so we will iterate through each of them as we did above, and for each one, we will call another action we implemented (LOAD_POSTS) and it will add each new child to our state.

Now go to the posts reducer and add the above normal action:

Reducer - post reducer


function posts (state = _posts, action) {
    switch(action.type){
        case 'REMOVE_POST': return [...state.slice(0, action.index), ...state.slice(action.index + 1)]
        case 'ADD_POST': return [...state, action.post]
        case 'LOAD_POSTS': return action.posts  //when app launces, state is empty, so we just return the payload fetched from database
        default: return state
    }
};

Lastly, we will invoke our 'load' method on the componentDidMount lifecylce in Main.

Main

//add this within the class block
  componentDidMount(){
        this.props.startLoadingPost()
    }

Recap: inside PhotoWall, we are iterating through the 'post' part of the state. However, at the beginning, it is empty (passed down from Main as an empty array). But as soon as the Main component is mounted (didMount), we will call the action that loads data from the database, which will update the state and rerender everything.

5-Removing posts integration with firebase

Now this is similar to what we did before. So now we will create a new action (asynchronous action) to remove the post.

Action - startRemovingPosts creation

export function startRemovingPost(index, id) {
    return (dispatch) => {
        return database.ref(`posts/${id}`).remove().then(() => {
            dispatch(removePost(index))
        }).catch((error) => {
            console.log(error)
        })
    }
}

In the above, we will travel down through posts in our database, and once we reach the posts with our specific id (passed in payload) we will remove() it. Then we will dispatch our normal removePost action to update our current state.

So we used the 'id' argument to specify the path. However, we also called removePost which needs an index, that is why we also took index as an argument in original action (to be able to pass it down to removePost).

Now go to Photo component, and upate the following line:

Photo

//before
 this.props.removePost(this.props.index);

//after
this.props.startRemovingPost(this.props.index, post.id);

That's it. It works beautifully. (Both inside Single and inside PhotoWall).

6-Saving comments to firebase

Small reminder: you don't need to create the 'folders' in database to be able to write to them. They are created once when attempt to write for the first time, and then afterwards are just referenced.

Like always. Here is the action:

Action

export function startAddingComment(comment, postId) {
    return (dispatch) => {
        return database.ref('comments/'+postId).push(comment).then(() => {
            dispatch(addComment(comment, postId))
        }).catch((error) => {
            console.log(error)
        })
    }
}

Note: we used push here, meaning that firebase will autogenerated an id key for our comments. So we are creating the names for the keys of each post (post/comments -> posts ids are decided by us, but comments ids are decided by firebase).

Now, we need to pass startAddingComment to the comment component (here, we weren't passing all actions, we were just passing the indivudal addComment action through props, so now we need to change it to the asynchronous one).

So go to Single and edit the following line

Single - one line edit

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

//after]

Now go to comments and edit the following line:

Comments - inside handleSubmit

//before
this.props.addComment(comment, this.props.id)  

//after
        this.props.startAddingComment(comment, this.props.id)  

7-Loading comments

Action - added actions

//load comments from database
export function startLoadingComments() {
    return (dispatch) => {
        return database.ref('comments').once('value').then((snapshot) => {
            let comments = {}
            snapshot.forEach((childSnapshot) => {
                comments[childSnapshot.key] = Object.values(childSnapshot.val())
            })
            dispatch(loadComments(comments))
        })
    }
}


export function loadComments(comments) {
    return {
        type: 'LOAD_COMMENTS',
        comments
    }
}

In the above, we created an empty object called comments. Then we looped through all the comments in database, and created a key-value pair inside our comments object, such that the key is the same key as in database (key of post), but the value is not the direct children (because direct children are represented by their auto generated keys in firebase, which we are not interested in). Instead, we wanted only the value of those children, and that is why we wrapped the (childSnapshot.val()) with an Object.values thing.

Read below

Note that earlier, we put our snapshot into an array. Here, we put it into an object because our comment state is an object type.

Add the normal action to the reducer:

Reducer -comments

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]}
        }
        case 'LOAD_COMMENTS': return action.comments
        default: return state
    }
};

And then go to Main and call the startLoadinComments insdie the componentDidMount:

Main - componentDidMount

componentDidMount(){
        this.props.startLoadingPost()
        this.props.startLoadingComments()
    }

Uploading to firebase: do everything normally but

DO NOT FORGET TO RUN NPM RUN BUILD BEFORE YOU DEPLOY

And then enter 'build' when they ask you for your 'public directory' (the directory you want to host from) -> this is done after you've created build above.