Understanding Firebase Realtime Database using React
Firebase has two types of database: Realtime Database and Cloud Firestore. Both databases are NoSQL-like databases so the database is structured as key-value pairs.
In this tutorial, we’ll focus on the Realtime database and explore how the security rules secure the integrity of the data in our database.
We will build a note-taking called Easi Notes along the way that will allow users to add, edit, update and delete notes.
This tutorial builds on the previous tutorial on Firebase authentication. Users will only be able to modify their notes when logged in. The client app is built in React and you can get the starter code here.
Setup Project on Firebase
To use firebase, you need to have a Google account. Then go to firebase console and create a new project.
From the Firebase console, click Add project.
1. Enter a name for the project and click continue
2. Firebase takes a few seconds to delegate resources for the project
3. After that, you are taken to the dashboard
The next thing we need to do is to add firebase to our web app. To do that, we have to get the configuration details of our project. From the dashboard, click on the </>
icon.
On the next screen, enter a name for your app and click Register app.
Copy and store the configuration details on the next screen in a safe place. We’ll use it later.
Understanding the Realtime Database
Click Database from the sidebar and scroll down to Realtime Database and select Create Database.
A dialog box pops up to set the default rules for the database. Select Start in test mode and click Enable.
Once done, it takes you to your freshly minted database.
The first thing you’ll notice is the warning that the security rules are defined as public and our data is not safe. We’ll fix that later in the .
If you have ever interacted with an API, you’ll probably be familiar with the JSON format:
{
"notes": [
"0001": {
"content": "Hello world",
"note_id": "2001",
"uid": "0001"
},
...
]
}
This is the same structure used by NoSQL-like databases like the Realtime database to store data. You can add, modify and delete data directly from the database by pressing the + button.
Our users won’t have to access to our database so let’s set up our client web app that will give them a friendlier interface to interact with their data.
React App Setup
You can download the starter code or just fork it on codesandbox and modify directly in your browser.
If you are running it locally, run:
yarn install
to download the dependencies used by the app. Once downloaded, you can run it by:
yarn start
Which would open a new tab in your default browser with the app.
If you are on codesandbox, create a fork copy to your account so you can make modifications to the code.
You may see something like this in your browser:
This is because we have not added our configuration details yet. Even if you don’t get this error, open src/services/firebase.js
:
import firebase from "firebase";
const config = {
apiKey: "YOUR_DETAILS_HERE",
authDomain: "YOUR_DETAILS_HERE",
databaseURL: "YOUR_DETAILS_HERE"
};
firebase.initializeApp(config);
export const auth = firebase.auth;
Replace apiKey
, authDomain
and databaseURL
with the respective configuration details gotten in the previous section. Once done, save and refresh the app to clear the error.
This app uses email/password authentication. For our app to work correctly, we have to enable it in from the Firebase console if you haven’t already.
From the Sign-in method tab, click Email/Password and enable it.
Once enabled, users should be able to login/sign up on the app.
Fetch Data from Realtime Database
At the moment, all users can do is login / sign up which is not very interactive. Let’s add a note from the firebase console and see how we can read it from the app.
Modify your database to look like this:
At the top level, we have a node all_notes
where all the users notes will be stored. Each node under that is a user’s id and it’s children would be an array of notes. Each note will have:
content
– the contents of the notenote_id
– the unique id of the noteuid
– the unique id of the user that created the note.
With this structure in place, we can fetch a user’s note by querying all_notes/uid
. Let’s see how that works.
Switch back to your editor, open src/services/firebase.js
and add this line at the bottom:
export const db = firebase.database();
This is where we initialize Firebase. There is one export auth
which we use for authentication and db
will be used to access the database methods.
Open src/pages/Profile.js
. Change
import { auth } from "../services/firebase";
to
import { auth, db } from "../services/firebase";
Add a new state variable notes
that will hold the user’s notes:
this.state = {
...
notes: []
};
We already added some data to our database. Just under the constructor, add this:
componentDidMount() {
db.ref("all_notes/0001").on("value", snapshot => {
let allNotes = [];
snapshot.forEach(snap => {
allNotes.push(snap.val());
});
this.setState({ notes: allNotes });
});
}
componentDidMount
simply reads as “when this component has successfully been added to the DOM, do this.” What are we doing? We get the db
module we imported and create a reference to the path all_notes/0001
. The next step is where the realtime comes into play. .on
runs the statements in its scope every time there is a new value
added to the database. This means we don’t have to refresh to get subsequent values added the initial fetch.
The result of the query is a snapshot of the user’s notes. We loop through it to get each note and append to allNotes
. Once we are done reading everything, we set the state variable notes
to the array of notes.
We can now modify the render()
method. Under <Header />
, add this:
{this.state.notes.map(note => {
return (
<div key={note.note_id}>
{note.content}
</div>
);
})}
We loop through notes and render each note. Save and refresh, login and navigate to /profile
and you will see a single note with hello world
or whatever value you added to the content field in the previous step.
Adding Data to the Realtime Database
We know how to add data from the Firebase console but users need to be able to add data directly from the app. Let’s write some code to achieve this purpose.
Just under the code that renders the notes fetched from the database, add this:
<div>
<input
onChange={this.handleChange}
value={this.state.content}
/>
<button onClick={this.createNote}>
Create Note
</button>
</div>
An input field to enter a note and a button to send the note to the database when clicked. The input field has an onChange
handler while the button has an onClick
handler so create the respective handlers just above render:
handleChange(e) {}
createNote() {}
render() {
...
}
In React, we also need to bind the handler to the component so get the correct this
scope in the handler. In the constructor, add:
this.handleChange = this.handleChange.bind(this);
this.createNote = this.createNote.bind(this);
Also, add a new state variable content which is bound to the input field
this.state = {
...
content: ""
}
Inside handleChange
, add this:
this.setState({
content: e.target.value
});
This grabs the value of the input field and sets the state variable content
.
Finally, add this to createNote
method:
createNote() {
const uid = this.state.user.uid;
const { content } = this.state;
const note_id = `note-${Date.now()}`;
db.ref(`all_notes/${uid}/${note_id}`)
.set({
content,
note_id,
uid
})
.then(_ => {
this.setState({ content: "" });
});
}
At the top, we get the user’s id which is available at this.state.user.uid as well as the content that will be added to the database. Next, we get a reference to all_notes/${uid}/${note_id}
. If the path doesn’t exist, it will be created during execution. A user should only be able to read/write to their own paths which is not enforced at the moment since, in the beginning, we set the rules to public. We’ll fix that in the section on security rules. In the Realtime database, two nodes with the same level cannot have the same key so we must create a unique key each time we push to the database.
We can write to the realtime database with either .push
or .set
. The difference is that .push
automatically generate a unique key and pushes the contents to that key while .set
writes the contents to the specified path in .ref()
. With .set
, if there is an existing data at the specified path the data gets overwritten with the new data.
db.ref(`all_notes/${uid}`).push({});
or
db.ref(`all_notes/${uid}/${note_id}`).set({});
We’ll be using .set since we are generating our own unique note id.
We end up with a structure that looks like this:
all_notes/
user_unique_id/
autogenerated_unique_key/
content
note_id
uid
If the data was successfully added to the database, we set content to an empty string which resets the input field.
If you try running it now, you will not see the new notes being added but if you check the Firebase console you will see the data there. This is because initially, we were reading notes from all_notes/0001
. Modify the path to:
db.ref(`all_notes/${this.state.user.uid}`);
And the newly added notes should apply as you add new notes. You can go ahead to delete all_notes/0001
.
Update Data in the Database
To update data, we fetch the content using the note_id
, populate the input field with the content and update the data with the new content.
Modify the code that renders the notes like so:
<div key={note.note_id}>
<p>{note.content}</p>
<button
onClick={() => this.editNote(note.note_id)}
>
Edit
</button>
</div>
We add a button that calls a method editNote
and passes note_id
to the method. Create editNote
method:
editNote(note_id) {
db.ref(`all_notes/${this.state.user.uid}/${note_id}`)
.once("value")
.then(snapshot => {
this.setState({
note: snapshot.val(),
content: snapshot.val().content
});
});
}
We get a reference to the all_notes/${this.state.user.uid}/${note_id}
which is where the note is stored. In this case, we want to read it just once so we use once()
. Then assign the state variable content with the note’s content. Our input variable binds to content so it is immediately populated with the note’s content. We also assign the note object to note. This has not been created yet so update the state variables to include it.
this.state = {
...
note: {}
}
To update the note, modify createNote
like so:
createNote() {
const uid = this.state.user.uid;
const { content } = this.state;
const note = this.state.note;
if (note && note.content) {
return db
.ref(`all_notes/${uid}/${note.note_id}`)
.update({
content,
})
.then(_ => {
this.setState({ content: "", note: {} });
});
}
const note_id = `note-${Date.now()}`;
db.ref(`all_notes/${uid}/${note_id}`)
.set({
content,
note_id,
uid
})
.then(_ => {
this.setState({ content: "" });
});
}
The main thing happening here is if condition. We first check to see if note.content
has a value. If it does, it means we want to edit a note so we get a reference to the note’s path and call update()
with the new note’s content. We are not updating the uid
or note_id
since those should not change hence the reason content is the only argument.
Deleting data from the Database
Let’s add another button to delete a note.
<button
onClick={() => this.deleteNote(note.note_id)}
>
Delete
</button>
Like with editNote
, we pass the note id to the method. Create deleteNote
and add this code:
deleteNote(note_id) {
db.ref(`all_notes/${this.state.user.uid}/${note_id}`).remove();
}
We get the path of the note and call .remove()
. We could have also used .set(null)
because, in the realtime database, the value of a key can’t be null
so that node automatically gets deleted.
Securing the Database with Firebase Security Rules
One of the tabs you’ll notice is Rules which is how we set access levels on the contents of the database.
The realtime database rules are defined as key-value pairs as well. Realtime Database has four types of rules:
.read
– if and when data can be read by users.write
– if and when data can be written.validate
– what if data is valid, if it’s in the right format and type.indexOn
– Specifies a child to index to support ordering and querying.
We’ll be covering the first 3 in this tutorial. At the moment, our rules allow anyone – authenticated or not – to read/write data from the database. The Realtime database includes some built-in variables and functions that simplify the process of validating data integrity and authorization. Let’s look at a few and how they can be used.
auth
– Represents an authenticated user’s token payloadnewData
– the new data to be written to the database$ variables
– A wildcard path used to represent ids and dynamic child keys.
At the top level of the rules, we have:
{
"rules": {
...
}
}
All the rules go in here. Let’s modify the rules to allow only authenticated users read/write to the database.
{
"rules": {
".read": "auth.uid !== null",
".write": "auth.uid !== null"
}
}
auth
is a built-in variable. When we query the realtime database, auth
is passed along with the query. Here, we check to make sure uid
is not equal to null
which would mean the user is not authenticated.
The problem with the rule is that it’s global which means it affects every node in the database. Once a user is authenticated, he/she can read/write data to any node in the database. This is not what we want for obvious reasons. For the notes app we’ve built, we only want users to be able to read/write only notes that belong to them i.e. all_notes/uid
.
Modify the rules like so:
{
"rules": {
"all_notes": {
"$uid": {
".read": "auth.uid === $uid",
".write": "auth.uid === $uid"
}
}
}
}
Copy the rules above and replace what you have in the rules tab and click Publish. Now, users can only read notes that belong to them. We can also validate the data that gets added to the database to make sure it is in the required format. Let’s modify the rules to make sure a note’s content
and note_id
have a length greater than 5 and uid
matches the user’s id.
{
"rules": {
"all_notes": {
"$uid": {
".read": "auth.uid === $uid",
".write": "auth.uid === $uid",
"$note_id": {
"content": { ".validate": "newData.val().length > 5" },
"note_id": { ".validate": "newData.val().length > 5" },
"uid": { ".validate": "newData.val() === $uid" },
}
}
}
}
}
Here, we use .validate
to check the data being added. newData.val()
contains the data being added. Publish the rules and refresh the app again. This time if you try to add a note with a length less than or equal to 5, the app throws an error.
You can add:
.catch(error => console.log(error.message));
To handle errors and show a useful error message to users. You can learn more about firebase rules.
Conclusion
In this tutorial, we’ve looked at the Firebase Realtime Database and how to use it in a React application. We ended it by seeing how we can secure our data with security rules. You can check out the source code to compare with your final work.