Redux From Scratch (Chapter 6 | React, Redux, Firebase Stack)
Buy the Official Ebook
If you would like to support the author and receive a PDF, EPUB, and/or MOBI copy of the book, please purchase the official ebook.
Prerequisites
Chapter 1 | Core Concepts
Chapter 2 | Practicing the Basics
Chapter 3 | Implementing With React
Chapter 4 | Async Practice With Twitch API
Chapter 5 | Implementing Middleware
Scope of This Chapter
In chapter 3, we were able to use Redux to do create, read, update, and delete (CRUD) operations.
However, all of our data was manually specified. We going to make significant improvements in this chapter by integrating with Firebase. Our user interface will be populated using dynamic data from a database without having to write any server-side code.
Understanding Firebase
Firebase is a backend as a service. In short, it takes care of a lot of backend development for you (authorization, databases, etc.). In our use case, it can provide a cloud-based database for storing and syncing data without having to write our own backend code. It will do a lot of the traditional setup and coding for a database behind the scenes. This will allow us to get the feel of using a data from a database in a React & Redux application without being overwhelming.
With that being said, it’s not as if Firebase is just a temporary thing that we will use for learning purposes that you wouldn’t use in the real-world. Firebase is a valid alternative to traditional databases and can be used for full-scale, dynamic web applications.
In order to help you dip your feet into Firebase, I am going to punt to other material that I have written. Here’s a chapter from my book React.js for the Visual Learner where I walk you through creating a Tile Background Changer Mini-App using Firebase.
Going forward, I am going to assume you to a look at this and are ready to learn how to implement Firebase in a React & Redux application.
Setting Up Firebase
Let’s setup Firebase for our BookCards project. You can retrieve the latest version of this project here via GitHub.
Once you have the project up to date, head over to the Firebase Console.
Create a new project and call it Book Cards:
Click Database on the left-hand menu and select the Rules tab.
Update the read and write values to true and hit Publish:
Next, open the BookCards project in a code editor and cd into the root of the project in command line.
Let’s install firebase:
npm install --save firebase
Then, we can import it at the top of our index.js file:
import * as firebase from "firebase";
Next, go to the Firebase console and select Add Firebase to your web app:
Just copy the code highlighted and paste it just above our App component:
// Initialize Firebase
var config = {
apiKey: "AIzaSyD_G28vy2ksn7ZoiVKyvVprguvsp41p04g",
authDomain: "book-cards.firebaseapp.com",
databaseURL: "https://book-cards.firebaseio.com",
projectId: "book-cards",
storageBucket: "book-cards.appspot.com",
messagingSenderId: "123503941995"
};
firebase.initializeApp(config);
We have just successfully configured Firebase with our application and can make use of the database 👌.
Our initial setup is complete. Easy, right?!
Structuring Our Data
The next thing to discuss is how to structure our data within the Firebase database for our Book Card project.
If you are familiar with more traditional databases, data is structured within columns and rows:
In the example above, there is a Books column which contains rows of information that is relevant to that column. Some of the rows, like author_name, in the Books column is a row from another column called Authors.
In our project, we need data to render book cards like this:
Therefore, a simple database in terms of columns and rows for our project would look like this:
In Firebase, we structure our database as a JSON tree:
In this tree, you can think of the outermost objects (users and groups) as the columns and the nested objects within as the rows. Once you can see the tree in this light, a database like this is very friendly for JavaScript developers since we are already familiar with working with plain objects.
Let’s take the simple database for our app which I represented as columns and rows and structure it as a JSON tree:
Let’s unpack this. The Books object is pretty straightforward with exception to the nested Authors object.
Information for each book will be within an ID object within the outermost Books object for organization.
Inside the ID object, the Cover, Link, and Title properties provide basic information about a book and the values were extracted from the current initial state as defined in BooksApp.js:
Now, you are probably thinking: “Why is there a nested Authors object underneath the Books object and another outermost object (column) called Authors?”
You were probably expecting something like this:
When structuring data in Firebase, we have to consider how a database may scale and we want to avoid as much nesting as possible.
To use an example, let’s say we had to contain information about an author other than their name. We might think to do something like this:
First off, this makes our Books object unnecessarily cluttered. If the Books object will just be used to populate the book card component, then we want to store the author information elsewhere. Second, what if our book had multiple authors? This would require a lot of additional clutter:
Third, what if we have multiple books from the same author? Do we want to manually write out all of the author information for each book? Of course not.
I’m not going to go into the technical, nitty-gritty details, but in short, we want to keep our database modular and well-organized like all of our other code.
In the correct structure, we have a separate outermost Authors object (column) which could contain all the information of multiple authors. The Books object has an Authors object that will tell us what authors wrote a certain book.
We could then check the outermost Authors object to get the information for that author as needed.
If you’re still confused, you can think of the “mikemangialardi”: true
in the Authors object within books as the equivalent of the line being drawn in this graphic:
If that didn’t help, it will help to see how this works when we are actually fetching for Firebase data to render our user interface in our application.
Before we move on, populate your database with the same structured data. Just change ID to 0 like so:
You can also import the following JSON tree found on this GitHub Gist.
Read From Firebase Database
Updating the Initial State
Let’s try reading data from our Firebase database and use that to render a single book card component:
Currently, the book card is rendered using the data in the following initial state found in our reducer file, BooksApp.js:
const initialState = {
books: [0],
cover: "https://s3.amazonaws.com/titlepages.leanpub.com/reactjsforthevisuallearner/hero?1496374274",
title: "React.js for the Visual Learner",
author: "Mike Mangialardi",
link: "https://leanpub.com/reactjsforthevisuallearner"
}
Recall, books: []
controls how many book card components are being rendered. For example, books: [0,1]
would render:
We are going to set our initial state to have blank values and update it using the data read from our Firebase database:
//define the initial state
const initialState = {
books: [],
cover: "",
title: "",
author: "",
link: ""
}
Applying Middleware
First off, let’s add the very helpful redux-logger middleware to help us debug any issues that arise.
Install redux-logger:
npm install --save redux-logger
Then, we will import this and applyMiddleware from the Redux library in index.js:
import { applyMiddleware, createStore } from 'redux';
import logger from 'redux-logger'
Lastly, we apply this middleware when we initialize our store:
//initialize store
let store = createStore(
BooksApp,
applyMiddleware(logger)
);
Cool! Next, we will install and configure redux-thunk.
We will interact with our Firebase database through an API. Therefore, we can use the redux-thunk middleware so we can do our API request and dispatching from one action creator as we did in the previous chapter.
Install redux-thunk:
npm install --save redux-thunk
Import it:
import thunk from 'redux-thunk';
Lastly, let’s apply it before the logger:
//initialize store
let store = createStore(
BooksApp,
applyMiddleware(thunk, logger)
);
Sweet! Our middleware is all set.
Writing Our Action Creators
As a refresh, we had an action creator called RequestApi in the previous chapter that looked like this:
function RequestApi() {
return (dispatch) => {
//API request
axios.get('https://api.twitch.tv/kraken/streams/featured?&client_id=skawlpb80ixx8e9cxafxepbn66xhe1')
.then(response => {
const streams = response.data.featured.map(function(feat) {
return feat.stream;
});
//dispatch FetchSuccess, order 2
dispatch(FetchSuccess(streams))
})
.catch(e => {
//dispatch FetchFailure, order 3
dispatch(FetchFailure(e))
}); //dispatch FetchRequest, order 1
dispatch(FetchRequest())
}
}
This action creator made use of redux-thunk to be able to make the API request and dispatch actions that ultimately told the UI what the status of the API request was so things could be rendered accordingly.
To save time, we are just going to write two more action creators.
One action creator will be for the API request called RequestApi.
Another action creator will be an update to the existing EditBook action creator found in EditBook.js. This will contain an action for updating our state with the Firebase data which ultimately control how our book card component renders.
The RequestApi action creator will dispatch the EditBook action creator once there is a successful response from the API request.
Let’s get started with the first one by creating a file called RequestApi.js within the actions folder.
We can add the shell of the action creator and a line for importing Firebase:
import * as firebase from "firebase";function RequestApi() {
return (dispatch) => { }
}export default RequestApi
Then, we can add two variables:
function RequestApi() {
return (dispatch) => {
const database = firebase.database();
const books = database.ref('Books');
}
}
database
holds the API call to connect our database. books
contains a reference to the Books object on our JSON tree.
We can then take a snapshot of our reference and do something with the data. For now, let’s just log the result:
function RequestApi() {
return (dispatch) => {
const database = firebase.database();
const books = database.ref('Books'); books.on('value', function(snapshot) {
console.log(snapshot.val())
});
}
}
Run npm start
and check the console in our local host:
We can see the outermost Books object and the ID object nested under it:
If we expand another level, we see the book information as defined in Firebase:
The final level down is the Authors object which tells us the author so we can check the outermost Authors object containing the information for all authors in our database:
We can now see how data can be accessed from Firebase in order to update our state.
Before we write the code to retrieve this data, let’s write the EditBook that will update the state with the retrieved data.
Open EditBook.js and let’s see what we currently have:
//define action within an action creator
function EditBook() {
const EDIT_BOOK = 'EDIT_BOOK'
return {
type: EDIT_BOOK,
cover: "https://s3.amazonaws.com/titlepages.leanpub.com/learnreactvr/hero?1496886273",
title: "Learn React VR",
author: "Mike Mangialardi",
link: "https://leanpub.com/learnreactvr"
}
}export default EditBook
As you can see, we just defined an action that would update the state with manually entered data.
Let’s adjust this so that the state will be updated with the retrieved data from the API request which will be passed into this action creator:
//define action within an action creator
function EditBook(firebaseData) {
const EDIT_BOOK = 'EDIT_BOOK'
return {
type: EDIT_BOOK,
cover: firebaseData.Cover,
title: firebaseData.Title,
author: "Mike Mangialardi",
link: firebaseData.Link
}
}export default EditBook
Note that I left the author line as is. We will update this later on.
As a refresher, this is how the reducer in BooksApp.js will handle this action when dispatched:
case 'EDIT_BOOK':
let edited = Object.assign({}, state, {
cover: action.cover,
title: action.title,
author: action.author,
link: action.link
})
return edited
Let’s return to RequestApi.js. We have to write the code to get the data from the Firebase database and pass into our EditBook action creator at its dispatch.
Instead of just logging the snapshot of the entire Books object, let’s iterate through each book within it and print its information:
books.on('value', function(snapshot) {
snapshot.val().map((book) => {
console.log(book);
});
});
We should now see the following in our console:
This is the object data nested with the ID object:
If we have multiple books, we would see 0, 1, 2, 3, etc. objects. All of these objects would contain information about a certain book.
Let’s copy this object into a variable called firebaseData and pass it into the EditBook action creator when dispatched:
books.on('value', function(snapshot) {
snapshot.val().map((book) => {
const firebaseData = Object.assign({}, book);
dispatch(EditBook(firebaseData));
});
});
The final piece is to import the EditBook action creator like so:
import EditBook from './EditBook';
Let’s check the local host:
Nothing is being rendered but we can see the updates from our EDIT_BOOK
action worked correctly.
The issue is that the books array is still empty. The size of this array determines how many book cards are rendered.
The AddBook action creator increments the books array when dispatched as can be seen in our reducer:
switch(action.type) {
case 'ADD_BOOK':
let incremented = Object.assign({}, state)
incremented.books.push(incremented.books.length)
return incremented
Therefore, we just have to import and dispatch the ADD_BOOK
action after the EDIT_BOOK
action in our RequestApi action creator:
import AddBook from './AddBook';function RequestApi() {
return (dispatch) => {
const database = firebase.database();
const books = database.ref('Books'); books.on('value', function(snapshot) {
snapshot.val().map((book) => {
const firebaseData = Object.assign({}, book);
dispatch(EditBook(firebaseData));
dispatch(AddBook());
});
});
}
}
There’s an issue with the code shown above. All of this should be asynchronous. Meaning, we should have code that says: “Hmm. Firebase is currently retrieving data containing all the information of books in our database. We better wait for it to finish then dispatch an action to use the retrieved data to edit our state. We also better wait for that action to finishing dispatching before we increment the books array in our state.”
In order to have code that says that, we need to create functions that return promises.
Here’s the function for the step where the Firebase data containing book information is being retrieved:
//function with promise to retrieve book info except author
function retrieveBookInfo() {
let promise = new Promise((resolve, reject) => {
books.on('value', function(snapshot) {
snapshot.val().map((book) => {
const firebaseData = Object.assign({}, book);
resolve(firebaseData);
});
});
}); return promise;
}
In the code above, we wrap a Promise around the code retrieving the Firebase data. When the data has been fetched (or resolved), we pass use resolve()
to pass a parameter to the next function that is waiting for this one to complete.
The next function needing firebaseData
will be another function with a promise where we run dispatch(EditBook(firebaseData));
:
//dispatch edit book action to update state with book info
function dispatchEditBook(firebaseData) {
let promise = new Promise((resolve, reject) => {
dispatch(EditBook(firebaseData));
resolve();
});
return promise;
}
The dispatch(AddBook());
needs to wait for the function above to be resolved before it can run. However, there is no parameter passed in:
//add book to books array in state
function dispatchAddBook() {
let promise = new Promise((resolve, reject) => {
dispatch(AddBook());
resolve();
});
return promise;
}
The final step is to call the first function containing our promise and then chain the other functions using .then()
:
//call all the promises in order
retrieveBookInfo()
.then(dispatchEditBook)
.then(dispatchAddBook)
Now, we are doing the following:
- Retrieving the data from Firebase
- Dispatching
EDIT_BOOK
once the data is retrieved - Dispatching
ADD_BOOK
afterEDIT_BOOK
is dispatched
We should now see the following in the local host:
Woohoo! Our book card is now rendering using Firebase data!
Except for the author name which we manually specified….
firebaseData contains the following Authors object:
We need to take the author ID of mikemangialardi
and use it to get name of the author from the outermost Authors object:
How do we do this?
Essentially, we have to another function with a promise in between function retrieveBookInfo
and function dispatchEditBook
. This function will use the author ID, mikemangialardi
, found in the Authors object within the Book object (this Authors object contains all the authors IDs for that one book) in order to retrieve the authors’ full name in the outermost Authors object (this Authors object contains the full information of all the authors in the entire database).
Let’s write this function out together.
Really quickly, let’s create a variable for the reference point of the outermost Authors object:
function RequestApi() {
return (dispatch) => {
//connect to database
const database = firebase.database(); //make references to outermost objects of JSON tree
const books = database.ref('Books');
const authors = database.ref('Authors'); //...
Next, add the shell of our new function:
function retrieveAuthorsName(firebaseData) {
let promise = new Promise((resolve, reject) => { resolve();
}); return promise;
}
Let’s log firebaseData.Authors
:
function retrieveAuthorsName(firebaseData) {
let promise = new Promise((resolve, reject) => {
console.log(firebaseData.Authors);
resolve();
});
return promise;
}
We also need to chain this promise in the right order like so:
//call all the promises in order
retrieveBookInfo()
.then(retrieveAuthorsName)
.then(dispatchEditBook)
.then(dispatchAddBook)
If we check the console, we can see the Actions object within the Books object:
We need to extract the name of the property and not the value of the property. Therefore, we can add the following to our function:
function retrieveAuthorsName(firebaseData) {
let promise = new Promise((resolve, reject) => {
const bookAuthors = Object.getOwnPropertyNames(firebaseData);
console.log(bookAuthors);
resolve();
}); return promise;
}
The code above will copy the property names into an object. If we check the console, we can see this:
Next, we have to get a snapshot of the outermost Actions object:
function retrieveAuthorsName(firebaseData) {
let promise = new Promise((resolve, reject) => {
const bookAuthors = Object.getOwnPropertyNames(firebaseData);
authors.on('value', function(snapshot) {
console.log(snapshot.val());
resolve();
});
}); return promise;
}
The snapshot looks like this:
There is only one object for our one author in our database. However, this is an object that would contain the objects for all authors in the database. We need to access the name for the author objects that match the author IDs stored in the bookAuthors
object.
To do that, we iterate through the book authors object:
function retrieveAuthorsName(firebaseData) {
let promise = new Promise((resolve, reject) => {
const bookAuthors = Object.getOwnPropertyNames(firebaseData);
authors.on('value', function(snapshot) {
bookAuthors.map((bookAuthor) => {
console.log(bookAuthor);
resolve();
}); //end bookAuthors iteration
}); //end snapshot of authors reference
}); //end promise return promise;
}
If we check the console, we can see that the bookAuthor
in the bookAuthors
iteration is the author ID:
We can then use that author ID to access the object that matches:
function retrieveAuthorsName(firebaseData) {
let promise = new Promise((resolve, reject) => {
const bookAuthors = Object.getOwnPropertyNames(firebaseData);
authors.on('value', function(snapshot) {
bookAuthors.map((bookAuthor) => {
const authorObj = Object.assign({}, snapshot.val()[bookAuthor]);
resolve();
});
});
});return promise;
}
We can then access the value of the name property in this object:
function retrieveAuthorsName(firebaseData) {
let promise = new Promise((resolve, reject) => {
const bookAuthors = Object.getOwnPropertyNames(firebaseData);
authors.on('value', function(snapshot) {
bookAuthors.map((bookAuthor) => {
const authorObj = Object.assign({}, snapshot.val()[bookAuthor]);
const authorName = authorObj["name"];
console.log(authorName);
resolve();
});
});
}); return promise;
}
In the console, we can see that we have finally access the name for all the authors in this book:
Again, this is necessary for allowing our database to scale despite the tediousness and sense of overkill upfront.
Anyways, the final step in this function is to pass firebaseData
(containing all the book information except the author’s name) and authorName
to the next function once the fetching of the data has been resolved.
You might be thinking that we can just do this:
resolve(firebaseData, authorName);
However, we can only pass one parameter. Therefore, we can pass an array:
resolve([firebaseData, authorName]);
Hang in there, almost done!
The next step is to update the function dispatchEditBook
to handle this new parameter which is now an array.
We can update the parameter name to bookInfoAry
:
//dispatch edit book action to update state with book info
function dispatchEditBook(bookInfoAry) { ... }
Then, we can use each element in the array as a parameter to the EditBook action creator:
//dispatch edit book action to update state with book info
function dispatchEditBook(bookInfoAry) {
//bookInfoAry[1] = firebaseData
//bookInfoAry[2] = authorName
let promise = new Promise((resolve, reject) => {
dispatch(EditBook(bookInfoAry[0], bookInfoAry[1]));
resolve();
});
return promise;
}
The final step is to update the EditBook action creator (EditBook.js) to handle this extra parameter:
//define action within an action creator
function EditBook(firebaseData, authorName) {
const EDIT_BOOK = 'EDIT_BOOK'
return {
type: EDIT_BOOK,
cover: firebaseData.Cover,
title: firebaseData.Title,
author: authorName,
link: firebaseData.Link
}
}export default EditBook
There’s been a lot of code change so you make sure everything looks good at this point by looking at the GitHub Gist.
If we check the local host, we can see that we are finally rendering the entire book card component using data from Firebase:
Adding Different Books
Recapping the Current Functionality
Let’s recap how our application is working at this point.
We have one state that can contain information for one book:
//define the initial state
const initialState = {
books: [],
cover: "",
title: "",
author: "",
link: ""
}
A book card component will render using the book information in the state for every element that is in the books array:
const bookItems = stateProps.books.map((book) =>
<BookCard
key = { book }
stateProps = { stateProps }
dispatchAction = {this.dispatchAction.bind(this)}
/>
);
The ADD_BOOK
action simply increments the books array:
The DELETE_BOOK
action does the opposite.
Before our Books container component mounts, RequestApi
is dispatched. This action creator dispatches the EDIT_BOOK
action which updates the initial state (which is completely blank) with book information provided by the Firebase database. It also dispatches the ADD_BOOK
action so the books array is given a single element, causing a single book card component to render:
The plus icon dispatches ADD_BOOK
on a click which adds causes another book card component to render with the same book information. The trash icon dispatches DELETE_BOOK
on a click which removes the book card component:
The aim of this section is to have multiple books defined in our database. Clicking the plus icon should add a book card component that is populated with the information for the next book in the database.
For example, if our database looked like this:
Then, the first book card component should render using the information from the 0 object within the Books object. If we clicked the plus icon, then the next book card component that renders should use the information from the 1 object.
Update your Firebase database to match the tree above. Here is the JSON tree.
Understanding the Problem
In theory, there are a few edits that should get this working.
First, we should make the book information in the state (found in BooksApps.js) to be arrays:
//define the initial state
const initialState = {
books: [],
cover: [],
title: [],
author: [],
link: []
}
Then, we should pass down a count to the presentational components when the books array is being iterated in Books.js:
const bookItems = stateProps.books.map((book) =>
<BookCard
key = { book }
count = { book }
stateProps = { stateProps }
dispatchAction = {this.dispatchAction.bind(this)}
/>
);
Note: Key is required for multiple components to be created via an iteration. It can’t be used as a prop. Therefore, we add the count prop.
Our presentational components in BookCard.js will then use the count to render the book information at the correct index of the arrays:
<img
style={{width: 250, height: 323}}
src={this.props.stateProps.cover[this.props.count]}
/>
<h2>{this.props.stateProps.title[this.props.count]}</h2>
<h3>{this.props.stateProps.author[this.props.count]}</h3>
<a href={this.props.stateProps.link[this.props.count]}>
{this.props.stateProps.link[this.props.count]}
</a>
<br/>
Back in our reducer found in BooksApp.js, we can handle the EDIT_BOOK
function a tad differently. We append to the arrays in our state instead of just simply updating single information:
case 'EDIT_BOOK':
let edited = Object.assign({}, state)
edited.cover.push(action.cover)
edited.title.push(action.title)
edited.author.push(action.author)
edited.link.push(action.link)
return edited
When the plus icon is clicked, we want to append new information to the arrays in our state using data from Firebase as well as render an additional book card component by incrementing the books array. All of this should be done by the RequestApi action creator.
Therefore, we can update our dispatchActions()
in Books.js like so:
dispatchAction (input) {
switch (input) {
case "TRASH":
this.props.store.dispatch(DeleteBook());
break;
case "PLUS":
this.props.store.dispatch(RequestApi());
break;
case "PENCIL":
this.props.store.dispatch(EditBook());
break;
}
}
Now, the plus icon should dispatch RequestApi()
on a click.
Let’s see if this worked:
Uh oh! Something is not working….
Thankfully, we have our handy redux-logger to give us clues on what the problem is:
The second EDIT_BOOK
action shown above is appending to the arrays in our state. However, it’s using the data in the 0 object within Books every time:
Therefore, the issue must lie in RequestApi.js.
Editorial Note: Quite frequently, I go back and rewrite parts of the chapter when I realize the logic was wrong for some code. This avoids having to go back and fix it together. However, I think it’s helpful to leave this in here to show that mistakes are going to happen and you will need to debug. Hopefully, you’ve been able to see the steps I took in order to isolate the issue.
Fixing Our Problem
I know this chapter has not been the easiest. Don’t fret. The solution to our problem is quite simple.
Essentially, there is one small snippet in our RequestApi.js file that needs to be fixed.
In the functions with promises, the next chained function runs once the resolve()
line is hit.
Currently, our function retrieveBookInfo
is iterating through every book object nested in the Books object. The resolve()
is also in each iteration:
books.on('value', function(snapshot) {
snapshot.val().map((book) => {
const firebaseData = Object.assign({}, book);
resolve(firebaseData);
});
});
Therefore, the firebaseData
variable is always storing the first book object.
This is incorrect. We don’t need iterate through every book object in our Books object. We only need to information for book object that is needed to populate the next book card.
For example, if we have one book card currently rendered, then that book’s information should be from the first book object in the Books object. If we add a second book card, then the book’s information should be from the second book object in the Books object.
Let’s remove the snapshot.val().map…
from function retrieveBookInfo
:
//function with promise to retrieve book info except author
function retrieveBookInfo(index) {
let promise = new Promise((resolve, reject) => {
books.on('value', function(snapshot) {
const firebaseData = Object.assign({}, book);
resolve(firebaseData);
});
});return promise;
}
Instead of Object.assign({}, book);
, firebaseData
should equal the following:
const firebaseData = snapshot.val()[*insert index*]
In our current database, the index could be 0 or 1:
Let’s try this manually:
//function with promise to retrieve book info except author
function retrieveBookInfo(index) {
let promise = new Promise((resolve, reject) => {
books.on('value', function(snapshot) {
console.log(snapshot.val()[1]);
const firebaseData = Object.assign({}, book);
resolve(firebaseData);
});
}); return promise;
}
We should see the second book object in the console (Learn React VR info):
If we change it to 0:
console.log(snapshot.val()[0]);
We see the information for the first book object:
Therefore, we should pass in an index to this function so it automatically gets the right book object depending on the index.
How do we get the right index?
Well, the current book object index will always be the same as the current length of the books array. If the books array has a length of 0, then the no card component has been rendered yet. Therefore, 0 is correct index of the first book object:
It just so happens that a nice feature of redux-thunk middleware is that we can pass in getState to our action creator returns without any imports.
Let’s do this:
function RequestApi() {
return (dispatch, getState) => { //...
}
Then, we can pass getState().books.length
as a parameter to the function retrieveBookInfo
:
//call all the promises in order
retrieveBookInfo(getState().books.length)
.then(retrieveAuthorsName)
.then(dispatchEditBook)
.then(dispatchAddBook)
Finally, we can just make two more tweaks to function retrieveBookInfo
.
First, we add a parameter called index:
function retrieveBookInfo(index) { ... }
Second, we use the index to retrieve the current book object by updating the value of firebaseData
to snapshot.val()[index]
:
//function with promise to retrieve book info except author
function retrieveBookInfo(index) {
let promise = new Promise((resolve, reject) => {
books.on('value', function(snapshot) {
console.log(snapshot.val()[0]);
const firebaseData = snapshot.val()[index];
resolve(firebaseData);
});
}); return promise;
}
Finally! The click of the plus button now loads the next book in the Books object within our Firebase database:
Concluding Thoughts
So far, we have 3 actions: ADD_BOOK
, DELETE_BOOK
, and EDIT_BOOK
.
ADD_BOOK
ultimately causes another book card component to render. DELETE_BOOK
removes a book card component from rendering:
EDIT_BOOK
edits the book information for the next book card component.
All of this to say, we don’t have any actions for writing to a Firebase database. Meaning, we are just reading existing data from Firebase. We are not creating, updating, or removing data from Firebase.
I have been going back and forth as to whether I wanted to go over how to write data to Firebase.
My aim in this chapter has been to introduce how to implement React, Redux, and Firebase. It is not meant to be a fully detailed explanation of Firebase. This is especially the case because in the next chapter we will be replacing Firebase with MongoDB. In that chapter, I will cover read and write operations.
I think we have done enough coding along together and I will be wrapping up this chapter.
If you are interested in learning how to write data to Firebase, you can take a look at the official documentation. It should only be a slight adjustment from what we have already done. You should have enough knowledge of a React, Redux, and Firebase implementation to achieve learning this on your own. If you are looking for a challenge between this chapter and the next one, try to update our Book Cards application so that it implements writing to Firebase and updating the user interface accordingly.
Final Code
Chapter 7
Buy the Official Ebook
If you would like to support the author and receive a PDF, EPUB, and/or MOBI copy of the book, please purchase the official ebook.
Cheers,
Mike Mangialardi