Software Prototyping (IV) - Set up Firebase
Software Prototyping (IV) - Set up Firebase
In this tutorial, I will demonstrate how to set up firebase for creating & reading database and adding authentitation.
Read from a database
In this step, we add a backend cloud database to our app. Cloud databases make sense, especially when prototyping, because they are easy to use and free for modest amounts of data and network traffic.
Add a database to your backend Firebase project
Go to the Firebase web console. Click on the card for your project. On the left side, click on Database. On the right side, where it offers several options, scroll until you see Realtime Database. Select that.
When Firebase asks about security, put the database in Test Mode. This lets anyone read and write to your data. Firebase only allows this mode for a relatively short period of time. To see your security rules, click on the Rules tab. You should see this.
{
"rules": {
".read": true,
".write": true
}
}
You will change these rules later in the authentication task.
The Realtime Database is simple – the database is just one big JSON object – and supports real-time notifications of changes to the data. Initially it is empty. You can use the console to import a file with some initial JSON data.
On the Firebase web console. click on Realtime Database on the left. Click on the Data tab. Click on the vertical three-dot menu and select Import JSON. Follow the instructions.
Verify that the data successfully imported and you can browse it on the web console.
Add the Firebase package
You have already installed the Firebase CLI, i.e., firebase-tools. To use the database functions in your code, you need to install the Firebase library. To make using Firebase easier in React, also install react-firebase-hooks.
npm install firebase react-firebase-hooks
The API for Firebase version 9 is quite different than Firebase version 8. The same concepts and code are there, but the way you call the code is different. The reasons and differences ar summarized here. Until version 9 is in commonly use, we show code for both versions below.
Add code to initialize Firebase
To keep the code modular, Firebase initialization should go into a separate file, like src/utilities/firebase.js. The boilerplate for importing Firebase, including the database library, looks like this:
firebase v8:
import firebase from 'firebase/app';
import 'firebase/database';
const firebaseConfig = {
apiKey: "...",
authDomain: "...",
databaseURL: "...",
projectId: "...",
storageBucket: "....",
messagingSenderId: "...",
appId: "..."
};
firebase.initializeApp(firebaseConfig);
const database = firebase.database();
firebase v9:
import { initializeApp } from 'firebase/app';
import { getDatabase, onValue, ref, set } from 'firebase/database';
const firebaseConfig = {
apiKey: "...",
authDomain: "...",
databaseURL: "...",
projectId: "...",
storageBucket: "....",
messagingSenderId: "...",
appId: "..."
};
const firebase = initializeApp(firebaseConfig);
const database = getDatabase(firebase);
You have to get firebaseConfig from the Firebase web console.
Click on your project.
Click on the “gear” icon in the upper left and select Project settings.
Click on the General tab.
Scroll down to the Your apps section, and click on the web app you registered for this project.
If you haven’t registered a web app, do so now by clicking Add app.
On the right, click the config radio button under SDK setup and configuration.
Copy the JavaScript code you see there into firebase.js as shown above.
Fetching data from Firebase
Fetching data from Firebase can be done similarly to how data was fetched using fetch, using useState and useEffect. There are several important differences.
- You access the data with a database reference not a URL.
- You don’t get data directly. Instead, you subscribe to the database, by giving Firebase a function to call when the data changes. That function normally will update a state variable.
A database reference is like a URL, but refers to some part of the JSON data stored in your realtime database. A reference (but no actual data) is created with
firebase v8:
database.ref(path)
firebase v9:
ref(database, path)
where firebase is the variable holding your Firebase object, and path refers to some part of your JSON object. For example, in the following references
firebase v8:
database.ref()
database.ref('/')
database.ref('/courses')
firebase v9:
ref(database)
ref(database, '/')
ref(database, '/courses')
the first two refer to the entire JSON, and the last refers to the data stored under the key courses.
More about Firebase database references.
In Firebase, you normally subscribe to a database. That means you pass Firebase a function to be called every time the data at the end of some path changes. The function will be sent a snapshot object. The val() method of this object will contain the new data.
In React, the appropriate thing to do when Firebase sends new data is to store it in a React state variable. Then normal React processing will be triggered to update your page appropriately.
Getting data usually involves three state variables: data, loading, and error. (You can call them anything you want.) Your app code should be written to handle the following cases:
- error is defined: Something went wrong and your app should signal a problem. Initially error is undefined.
- loading is true: No data is available yet. Your app should indicate that data is being loaded. Initially loading is true.
- Neither of the above is true. Your app should display what’s in data.
There’s a bit of boilerplate needed to get data and manage these failures. It makes sense to create a a custom hook. The version here is a more basic version of the useObjectVal hook defined in react-firebase-hooks. It takes a function as an optional second parameter. If given, the function will be applied to the JSON fetched from Firebase. Whatever function returns will be returned in data. This is handy for calling addScheduleTimes.
Add authentitation
Authentication has been held off until now. It is a common mistake when developing a new app is to start with sign up or log in. That’s misplaced effort. Signing up is not an interesting thing to user test. Start with the part of your app that is new and different.
But now that user can change the course schedule, it makes sense to only let logged in users make changes. This is called authentication. After authentication comes authorization – can this user do this action – but we’ll only do authentication here. With hooks, Firebase, and a helper UI library, it is not too much code.
Require authentication to change data
A series of small steps is needed to require authentication for writing to the database.
- Go to the Firebase console.
- Click on your project.
- Click the Rules tab.
- Click the button Edit rules.
Your rules should currently look like this:
{
"rules": {
".read": true,
".write": true
}
}
Select the true value to the right of “.write”. Replace it with the string "auth.uid != null"
, so that your rules now look like this:
{
"rules": {
".read": true,
".write": "auth.uid != null"
}
}
Click Publish to save the rules.
The new rule is a JavaScript string that Firebase will evaluate when an attempt is made to write data to any part of the database. The variable auth will be set to a user object, and auth.uid will only have a value if the user has logged in.
Test! Reload your web app page. You should still see the courses. Now try to change a course’s meeting time. You should get a permission denied error. If so, you have now protected your database from guest users.
Enable authentication support on Firebase
Authentication means asking a user to identify themselves, usually with a name or email address and a password. Developers used to write code to do this for every app, but a popular alternative now is to let a third-party service, like Google or Facebook, do the work. Those services manage the database of user information, check passwords, provide “forgot password” emails, and so on. Developers like doing less work. Users like having one less password to remember.
Here’s how to enable authentication with Google, i.e., that users can click on a button and sign in with Google.
- Go to Firebase console.
- Go to your project page.
- Click Authentication on the left.
- Click the Sign-in method tab along the top.
- Click the Google line.
- In the panel that opens up, click Enable and enter any required information.
- Click Save.
That last step is easy to miss on the Firebase web site!
There are many other options you can enable. Click on them to see what they do. To use a third-party like Facebook or Twitter, you will need to set up a developer account with that service and get an ID for your app.
Add authentication code to the app
Now that Google authentication is enabled on the backend, we need to add code on the frontend to let users click a button and log in. The Firebase auth object provides a class GoogleAuthProvider to do this, as well as a signOut method to log out.
In src/utilities/firebase.js add this import
Firebase v8:
import 'firebase/auth';
Firebase v9:
import { getAuth, GoogleAuthProvider, onIdTokenChanged, signInWithPopup, signOut } from 'firebase/auth';
Define and export this function:
Firebase v8:
export const signInWithGoogle = () => {
const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider);
}
Firebase v9:
export const signInWithGoogle = () => {
signInWithPopup(getAuth(firebase), new GoogleAuthProvider());
};
Calling signInWithGoogle pops up a dialog box to let the user sign in with Google.
Where should we put the buttons to call these functions? Users will look for this at the top. The term selector seems like a good place. The new button should go to the right, to avoid being accidentally clicked when changing terms.
Bootstrap has many options for laying out a row of buttons. It’s alway best to explore the examples in the documentation and adapt one that looks like what you want. The code below adapts an example from the section on button toolbars, using a temporary dummy “sign in” button.
export const SignInButton = () => {
return (
<div className="btn-toolbar justify-content-between">
<button className="btn btn-secondary btn-sm"
onClick={() => signInWithGoogle()}>
Sign In
</button>
</div>
)
}
Test! Believe it or not, you should now have working authentication. Try changing a course meeting time without signing in. Verify that you get the permission denied error. Now click Sign In. A dialog box should appear instructing you to sign in with Google. After signing in, trying change a course time again. Now it should work!
Add signing out
It’s always good to include a sign out option, so that if someone is using your web app on a public terminal, they can log out.
firebase.auth provides a simple function for signing out. Define and export this function in src/utilities/firebase.js
Firebase v8:
export const signOut = () => firebase.auth().signOut();
Firebase v9:
const firebaseSignOut = () => signOut(getAuth(firebase));
export { firebaseSignOut as signOut };
Calling signOut just logs the user out of Google. No dialog box appears.
Import signOut and define a SignOutButton to go with your SignInButton.
const SignOuButton = () => (
<button className="btn btn-secondary btn-sm"
onClick={() => signOut()}>
Sign Out
</button>
);
Signing out is something that can only happen after you have signed in. So it makes sense to show a sign in button only when there is no user signed in, and a sign out button only when there is a user signed in.
Authentication changes are asynchronous, like database changes. You have to listen for a change in user status. In src/utilities/firebase.js define and export a very simple hook to list for changes in user state.
Firebase v8:
export const useUserState = () => {
const [user, setUser] = useState();
useEffect(() => {
firebase.auth.onAuthStateChanged(setUser);
}, []);
return [user];
};
Firebase v9:
export const useUserState = () => {
const [user, setUser] = useState();
useEffect(() => {
onIdTokenChanged(getAuth(firebase), setUser);
}, []);
return [user];
};
Now we can show different depends on the user state:
export const Display = () => {
const [user] = useUserState();
return (
<div className="ButtonOnState">
{ user ? <SignOutButton /> : <SignInButton /> }
</div>
)
}