-
Notifications
You must be signed in to change notification settings - Fork 5
Bootcamp Part 4: Firebase Realtime Database
Deadline: November 26th 11:59pm ET
This week, we will be retrieving and saving our flashcards data into Firebase Realtime Database. This allows us to have many different decks of flashcards and allow you to access saved flashcards that other people have created. This is a super exciting week to bring lots of what we've learned together, and it's a good amount of material, so try to start early! As usual, if you're stumped, try to come out to one of my office hours to ask questions.
I've split up the videos into as reasonable as possible parts and tried to keep them concise, but there's just a lot of material and so there are longer videos this week. Videos are also linked below in their respective sections.
- Higher-order Components Introduction
- CardViewer component
- CardEditor component
- Homepage
You can find the slides here: Slides
I also tried to recap a lot of the video in writing in the transcript this week, so if the video doesn't make sense the first time around, see if the transcript explanations can supplement it!
If you didn't catch this section in week 3, please go back and do it! No need to submit the form, but make sure you understand how the Firebase Realtime Database works. Link: https://github.com/harvard-datamatch/bootcamp/wiki/Bootcamp-Week-3:-React-Router-&-React%E2%80%93Redux%E2%80%93Firebase#intro-to-firebase-realtime-database
Now that we have Firebase Realtime Database and a Redux store, our components need ways to interact with these things. This is where higher-order components come into play. Higher-order components are functions that take as input React components and return enhanced, more powerful React components (by passing in more props). There are three higher-order components that we will use: withRouter, connect, and firebaseConnect.
Link to Video on withRouter: Video (Optional if you attended 11/21 meeting)
Code diff: Github link
withRouter gives a component access to the following aspects of the React Router through props:
-
historyallows the component to mutate browser history -
locationtells the component the pathname (URL) the component is rendered on -
matchobject gives information on how the path specified in the Route component matched the path URL
The match object allows us to create Route parameters, dynamic values that are set in a page's URL. For example, if we want a Route component to match all URL paths of type /test/:id and we go to the URL /test/asdf, then we can access the Route/URL parameter asdf by going to props.match.params.id. If you want to learn more about Route parameters: https://medium.com/better-programming/how-to-pass-multiple-route-parameters-in-a-react-url-path-4b919de0abbe
Small exercise: create a Route that matches the URL /users/joshua, where I can get the parameter joshua from the match object prop, specifically props.match.params.name should equal joshua if I got to the URL /users/joshua.
Documentation on withRouter: https://reacttraining.com/react-router/core/api/withRouter
Link to Video on connect: Video
Code diff: Github link
connect connects a React component with the Redux store allowing the React component to retrieve data from the Redux store. (The React component can also mutate the Redux store through the dispatch prop, but we will not be covering this.) The main part of the connect higher-order component is the mapStateToProps function that is the first parameter into connect.
mapStateToProps is a function that takes state (that is Redux global state), picks and chooses what part of the Redux state it wants, and returns an object that is passed as a prop into the Component. So, the following simple mapStateToProps function will pass the prop test (which has value '1234') and the prop isEmpty (which is the value of firebase.profile.isEmpty in the Redux state) into the Component, which we are trying to enhance.
const mapStateToProps = state => {
const isEmpty = state.firebase.profile.isEmpty;
return { test: '1234', isEmpty: isEmpty };
}
export default connect(mapStateToProps)(Component);Documentation on connect: https://react-redux.js.org/api/connect
Link to Video on firebaseConnect: Video
Code diff: Github link
firebaseConnect requests data from Firebase Realtime Database to be stored in the Redux global store and provides the enhanced component with Firebase props (like login, logout, createUser, update, etc). You'll see firebaseConnect used in conjunction with connect a lot because once we have data from Firebase Realtime Database in the Redux global store, we would usually like the React component to get the data from the Redux global store.
The first parameter to firebaseConnect is an array, which are all the database paths at which you want to retrieve data from in Firebase Realtime Database. Remember that the time it takes to retrieve data is unknown, so data will often be undefined to start with in Redux, until the data is loaded into Firebase. Moreover, in the render function, make sure to use the isLoaded function to have the React component wait for the data to be loaded into Redux.
Small exercise: Instead of retrieving all of /flashcards, try solely retrieving one of your deck's name only. (Hint: you'll have to use a deeper query, like /flashcards/something/something, where you fill in the something.) Then, display that name once again in your Test component using the connect higher-order component to pass the name as a prop into the React component.
Resources
- Another explanation of
firebaseConnect: https://react-redux-firebase.com/docs/queries.html#firebaseConnect -
firebaseConnectdocumentation: https://react-redux-firebase.com/docs/api/firebaseConnect.html - Documentation on
isLoaded: https://react-redux-firebase.com/docs/api/helpers.html#isloaded
I gave the mathematical way of thinking about compose in the video. Essentially it takes expressions of the form f(g(x)) to be compose(f, g)(x). If you want to read the documentation, you can find it here. But, it's not very good unfortunately. This StackOverflow answer might be better: https://stackoverflow.com/questions/41357897/understanding-compose-functions-in-redux, but it's quite similar to what I'm trying to show mathematically.
The order in which we compose higher-order components is important also, so make sure to reason them out logically. If a certain higher-order component, like connect, requires the data from another higher-order component like firebaseConnect, then firebaseConnect should wrap around connect and so firebaseConnect should come first in the function composition before connect, so something like compose(firebaseConnect, connect).
Link to Video on CardViewer component: Video
The code changes I made for this section: Github link
For the CardViewer component, we need to retrieve the flashcards data from Firebase based on the Route/URL parameter, which is the flashcards deck ID or unique key in the Firebase Realtime Database.
Note: if you keep cards state in CardViewer, you should read the following!
Unfortunately, state doesn’t get updated simultaneously when the props change, so we have to write these 4 lines:
componentDidUpdate(prevProps) {
if (this.props.cards !== prevProps.cards) {
this.setState({ cards: this.props.cards });
}
}This allows us to check if the props actually changed, and if the cards prop changes, then we also update the cards state. Moreover, if you are storing the length of cards in state, you can also update this by changing the setState call to be:
this.setState({ cards: this.props.cards, length: this.props.cards.length });You can check out my code here, where I have the randomize function that needs the cards state: https://github.com/harvard-datamatch/bootcamp/blob/week4-state/src/CardViewer.js
Back to the other CardViewer changes. Here are the new things shown in this section:
We can specify where in Redux we want to store our Firebase Realtime Database data in firebaseConnect. The way we do this is as follows:
firebaseConnect([
{ path: '/flashcards/deck1', storeAs: 'deck1' }
])By default, Redux will store our Firebase Realtime Database data in the same structure as the database path we fetched. So, if the database path was /flashcards/deck1, the data would be stored under state.firebase.data.flashcards.deck1. However, with the storeAs attribute in the object, it tells firebaseConnect to store our Redux in a certain place, for example storeAs: 'deck' makes the data stored under state.firebase.data.deck (notice that flashcards is not in this path anymore). This helps with making the data object more shallow and doesn't require us to go deep into the object.
firebaseConnect also has access to props by changing the first parameter from an array to a function:
firebaseConnect(props => {
// here we can use props! like props.match.params.id or whatever
return ['/some/path'];
})Javascript is an interesting language and allows us to write expressions like deck && deck.name. If we understand what this means, it can be very powerful! If you go back to what the AND operator means, essentially both inputs to the AND operator must be true for the AND expression to be true. For example, if we have false && true, we know immediately at the false that this AND expression cannot be true. And so, we can just return false immediately without even evaluating or caring about the second input. This is called short-circuiting. You can read more about that here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND
Now, back to deck && deck.name. If deck is undefined, since undefined is a false value in Javascript, then deck && deck.name = undefined, since we know that deck is false value, so we don't even have to evaluate deck.name. Similarly, if deck is null, then deck && deck.name = null. Only when deck is an object, which is a true value in Javascript, then Javascript must evaluate the second input, which is deck.name, and that second input is returned. If you had true && blah, blah is the only thing that matters to the AND operator because it already knows the first input is true, so everything lies on the value of blah.
- Difference between
undefinedandnullin Javascript: https://codeburst.io/javascript-null-vs-undefined-20f955215a2 - Template strings in Javascript: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
-
isEmptydocumentation: https://react-redux-firebase.com/docs/api/helpers.html#isempty
Link to Video on CardEditor component: Video
The code changes I made for this section: Github link
For the CardEditor component, we will learn to save and update data into Firebase Realtime Database. We will also learn about callback functions and redirecting users using the history prop from the withRouter higher-order component.
The first Firebase prop we use is this.props.firebase.push(). This creates a new Firebase Realtime Database reference, essentially it references or refers to a path in our Firebase Realtime Database. Note that in our video, we pass this.props.firebase.push('/flashcards') because it tells the push function that the new Database reference should be under the /flashcards database path. This new Firebase Realtime Database reference returned from push has a key attribute, which holds the unique key generated by Firebase that we can use for our flashcards deck ID/key. Moreover, these push keys are generated based on the current timestamp, so all the keys will be in order of the time they were generated!
The second Firebase prop we use is this.props.firebase.update(path, object, callback). The first parameter to update is the database path in the Realtime Database that you want to update. The second parameter is the object that you want to update the database path with, so the new data. Finally, the third parameter is the callback function, which is executed once Firebase successfully updates the database with the new data.
Retrieve and save data to Firebase walkthrough: https://firebase.google.com/docs/database/web/read-and-write
A callback function is a function that gets executed right after another function finishes executing. Essentially that other function, on completion of its execution, "calls-back" the callback function. So in our case, the callback function we pass into this.props.firebase.update is called/executed right after Firebase successfully updates the database with the new data. The reason we need a callback function is because since we are sending data across the internet network, we aren't sure how long it'll take to reach Firebase and so, using a callback function allows Firebase Realtime Database to let our React application know that it's completed its updates by calling the callback function. More information on callbacks: https://codeburst.io/javascript-what-the-heck-is-a-callback-aba4da2deced
Link to Video on Homepage component: Video
The code changes I made for this section: Github link
Last but not least, the Homepage component. We talk about the concept of duplicating data in Firebase Realtime Database. We do this whenever the data we are retrieving includes an excess amount of unnecessary/unused data by the component. In our case, the Homepage component only needed the names of the flashcard decks and so retrieving all of /flashcards, which includes the individual cards data, was way too excessive as we'd be throwing away 99% of the data. Larger pieces of data also take longer to transfer over the internet network and Firebase actually charges you by how much your React application is reading/retrieving and writing/saving data in Firebase Realtime Database. So, in this case, we are reading a lot of unnecessary data, when we can just duplicate the data (writing very little) and save money and time.
The issue with duplicating data is making sure that data is written simultaneously in two database paths/places. But luckily, Firebase Realtime Database allows us to do that with the this.props.firebase.update function. Instead of passing in a single database path and a single piece of data to update, we pass an object of updates, like the following:
const updates = {};
updates['/first/path'] = { some: 'data' };
updates['/second/path'] = { some: 'other data' };
this.props.firebase.update('/', updates);So, in our example above, the updates object's keys are the Firebase database paths that we want to update and the updates object's values is the new data that we want to update the database with. Finally, the last difference is the this.props.firebase.update call, which is called on the root path, which is just '/', and we pass in the updates object.
Implement the Homepage component. We've already created the data that the Homepage component needs, which is under the /homepage path in the Firebase Realtime Database. Now, all you have to do is retrieve that data and display each of the flashcard deck names as a Link component that takes the user to the corresponding flashcard deck in the Card Viewer. So, if a deck had name "Geometric shapes" and unique ID/key deck1, then the Link component should have text "Geometric shapes" and when the user clicks on the Link it should take them to the URL path /viewer/deck1.
-
Hint 1: You might want to take a look at
Object.keys(), since the data from the/homepagepath is an object, not an array, so you can't easilymapover an object, but you can easilymapover the array of keys of an object. -
Hint 2: With hint 1, look at the CardEditor and see if you can see the similarities between rendering a bunch of links with rendering a bunch of card rows!
-
(Easy) Add a description to the flashcard decks! You should add a description box to the CardEditor that allows one to describe their flashcard deck. On submission, you should save that description under a
descriptionattribute in the Firebase Realtime Database. Then, in the CardViewer component, underneath the name, display the description and on the Homepage, also display the description along with the deck name.- Hint: Make sure you save the description for both the
/flashcardsand/homepagedatabase paths.
- Hint: Make sure you save the description for both the
-
(Medium) Allow people to save flashcard decks, which prioritizes these flashcard decks on the Homepage. Add a Save button to the CardViewer and if a user clicks on the Save button, save that to Firebase Realtime Database. The Save button should say "Unsave" or something like that after being clicked, so that a user can undo their save (make sure to reflect this in Firebase Realtime Database). Then, on the Homepage, show the flashcard decks that have been saved by users first. Then show the rest of the flashcard decks that haven't been saved by users.
-
(Medium-Hard) Allow people to star cards. (Currently, there's no way to distinguish between your users, so everyone will be sharing the same starred cards, but don't worry about that.) The functionality is this: for every card in a flashcard deck in the CardViewer, there should be a star symbol so that when you click the star symbol, it'll save to Firebase Realtime Database that that specific card in that deck is starred. The star symbol should indicate that that card has been starred (by filling it in with a color visually or what not). Clicking the star symbol again will un-star the card (making sure to save this in the Firebase Realtime Database also).
Then, you can hit a "Starred cards only" mode for that specific flashcards deck in the Card Viewer, and it will only show the starred cards in that deck and allow you to browse through those starred cards. (Make sure there's a way to revert back to a non-starred cards mode also.)
Fill out this form when you're done! Make sure to deploy your app and push your changes to GitHub (be sure to be doing this as you go), so we can take a look. It'll notify me to review your work and let me know you finished.
Form link: https://forms.gle/Y4cehXobLKhx5kjz7