I thought everything would be sunshine and roses when I discovered Firebase and Firestore and a few tutorials on hooking it up to React. Turns out it made my project hell; I’ve created about 5 different new projects just to test various aspects in isolation in the hopes of finding at least one universal approach to making Adventure Outliner work the way I needed it to. Every single one seemed to work fine, but only up to a point. Eventually there’d be some stupid roadblock that logically shouldn’t be there but was there anyway, making my life hell. Until today, thankfully.
What is Firebase and Firestore?
Firebase is a service from Google that provides a whole lot of “free as in reverse deductible” services for developers like authentication, database, and file storage, among others. As any developer knows, rolling authentication/authorization is a royal pain in the ass, which is why OAuth services from popular social media outlets exist. I hate doing it because I haven’t done it in years. Back when I had to do it the stakes were much, much lower. Now there are all kinds of sophisticated threats that didn’t exist back when I had to create CRM back-ends, and now there are all kinds of ways to try and keep one step ahead of the Black Hats. Firebase puts the eggs in Google’s basket, to an extent, by allowing me to let them handle the authentication and authorization heavy lifting. I just need to persist the JWT (JSON web token) they send to me so I can use it to verify that the user accessing the site is who they are logged in as.
Firestore, on the other hand, is a NoSQL database. I believe I’ve mentioned this before, but NoSQL is a bunch of text files that respect no schema, but still remains fast and flexible. Although I’d prefer a relational database for many reasons, Firestore allows for an insane number of free transactions before I start getting billed, which at least for the development future is about how much money I have to invest in this project.
What’s the Problem?
Well, there’s a lot of problems, actually.
First, React is a language in flux. Originally it relied on classes that extended base classes, meaning that there were internal objects that developers could build upon to add our own functionality. We didn’t have to write as much code as we would have had those base classes not been around. In the past few years, though, React has been moving towards a more functional design by ditching classes and using functions. If all of that is Greek to you, don’t worry; it’s not the point. What is the point is that whenever I searched the web for info, I’d get 25% of the results talking about using the new, functional methods, and 75% using the old, someday-gonna-be-retired class-based method. While there’s also a lot of bridge-info out there on converting from class to function, there’s been just enough older info to leave massive holes in my understanding of what I was looking to do.
Second, a lot of times I was looking for info on Firebase, Firestore, or some other third party library. If I didn’t specifically enter “React” in the search box when looking for it, I’d get all kinds of other languages mixed in there. React Native (a more mobile-centric flavor of React which usually focuses on Android or iOS specifics) or AngularJS (a horribly, horribly overwrought monster of a framework) were common in the results which sometimes provided answers, but more often than not did not.
Third, there is a lot of bullshit info out there. Not that I mean that there’s wrong info, but there’s a lot of folks making posts like these who do nothing but copy and modify someone else’s examples. I had read those examples, Chief, and the reason I ended up on your site was because the original example didn’t provide enough info, or provided just enough example to illustrate that yes, this library or technique works, but under such contrived and trivial circumstances that make the info almost useless in a real world scenario. So thanks for that.
Fourth, Medium.com is apparently the MySpace of the React developer crowd. I don’t know what the hell the deal is, but 50% of my search results lead to a Medium.com blog, and 75% of those are behind a paywall. That’s frustrating as hell, but thankfully about 50% of that 75% of the original 50% are just reprints from somewhere else on the web, and searching for the title and author can almost always turn up the original “free as in beer” post.
And fifth, a lot of my troubles have caused me to think of alternative ways to approach a problem, and most of the time those alternate approaches didn’t help solve the original problem — they just enhanced the original exponentially. For example, because my core issue was how to persist and expose access to Firebase’s authentication system I had turned to Redux, which is a third party library designed to store info at a global level. Turns out that this is complete bullshit because nowhere up front does it tell you that “global” only means “global” under certain circumstances. If you move to another page, for example, you’re SOL meaning that I’d still need to to figure out how to store that so-called “global” storage data somewhere that persists.
Where Am I Now?
I think I’m finally in a good place, though. I kicked Redux to the curb because I couldn’t see the value in just adding another layer on top of the problem I was trying to solve originally, and had a good, long think on the situation. Because my posts can never be long enough, here’s the rundown (as much for my later self as it is for anyone who might find this post).
The Main Problem
My issue was that although I could get authenticated with Firebase, I had no idea how to ensure that my authenticated users were recognized all throughout the site. I could log a user in, but when the user went to another page, the site lost track of them.
Solution #1: Follow By Example
Although I sang praises of one particular tutorial I found on the web, in hindsight it really only provided about 10% useful information for me. It was the first explanation I found, looked technical, was well explained, and so I considered it to be the “right” way to do things. In this case, the author had me creating a “Context”, which is a two part data transport vehicle that gets around one of React’s biggest issues, which is…data transport. Normally I’d pass data via “props”, which are localized variables passed from parent component to child component. As you might imagine, setting a value at the top of the component tree, but needing it three levels down is a massive problem, akin to passing a bucket of water down a ladder if every few rungs was occupied by a different person. Context solves that by creating a kind of “energy field” of data that any component rendered inside that field can just grab “out of thin air”. Technical terms, folks. I has ’em.
This approach lead to too much code stuffed into dark corners that I had to remember was present in order to use it. Sometimes I could use a single approach and get the job done. Sometimes I had to stack them. Sometimes I had to stack them in a particular order and I could just imagine that as the project grew things would get out of control. Ultimately, having to figure out the ramifications of using one approach at the top or the bottom of a block of code (as the results could be wildly different) or remembering where and when to use one method over another, or in conjunction with another, was too much of a headache.
Solution #2: Fallback on Old Habits
As a long time C# MVC developer, I turned to the “model-view-controller” paradigm. In this case, a “view” is what the end-user sees: the HTML and all the fancy functionality. The “model” is a nebulous term that encompasses processing libraries for things like business rules and database access, as well as data objects that keep and ship information around the site. A “controller” bridges the other two, taking data from the view and passing it to the model, and receiving data from the model and sending it to the view.
My goal was to isolate anything having to do with Firebase and Firestore so when it came time to work on visual components I could just call a method and receive the results. This would keep the components small and clean, and would allow me to write reusable methods that I could use when I needed them. Unfortunately this didn’t pan out. Putting methods into a Firebase business logic model resulted in too many unexplainable interruptions in data flow. Although I could get the service to authenticate me, I’d lose the data if I tried to do anything after that, even in the same component. Although I’m not completely sure it was the fault of the design, it caused enough headaches that I felt that I needed to find a better way.
Solution #3: Redux
As I mentioned, Redux is a library that supposedly allows for the global storage of data. Ideally this would make that data available everywhere, but as I found out late in the game, to do that I’d have to figure out a way to basically do what I was looking to do: persist data in a way that I can get to it whenever I needed it, wherever I needed it.
Redux is very popular, so folks must know the best ways to go about using it, but I didn’t find much of that when I searched around to figure out how to integrate it into my project. Like most of my other searches, any info on Redux was either base minimum and pointless, or mind-bendingly complex and highly steeped in the Redux slang that I couldn’t see how it would fit into my own work. I have gotten to know React pretty well over the past few weeks but I am by no means an expert; trying to learn Redux on top of other perpetual learning was just too much of a pain, considering that I’d still have to figure out a way to persist data between pages and component re-renders.
Solution Now: Understanding the Service
After I sat down and thought about it this afternoon, I kicked myself for having overthought the whole thing.
Firebase is a service. Specifically, the authentication portion that I was after is a service, and because the web is stateless, there’s no ongoing communication between Firebase Auth and my application. I knew that Firebase used JWT, and I know that JWT is designed to be sent with each communication in order to authenticate the user’s endpoint. That means that I didn’t need to persist the connection to Firebase. In fact, I finally understood that the reason I was working with Firebase and not trying to roll my own auth system was because Firebase handled all that shit for me. I was literally trying to do its job while also trying to get the service to do its job.
So here we go.
Firebase > auth.js
import firebase from "firebase/app"
import "firebase/auth"
const config = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain: process.env.REACT_APP_AUTH_DOMAIN,
projectId: process.env.REACT_APP_PROJECT_ID,
storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_APP_ID
}
//If the app isn't initialized, then do it using the above config
!firebase.apps.length && firebase.initializeApp(config)
//Set up the auth variable
const auth = firebase.auth()
//Export the auth variable.
export default auth;
This is the core-connection to the Firebase service. It uses the provided configuration information to identify my project, connects to the service, and then sets the key variable, auth. I export auth as the only item (for now). Auth is the gateway to Firebase authentication services like registration, login and logout. Auth what I was trying to persist before I realized that I just needed to import it and call it when I needed it. Exporting auth allows me to import the access conduit to the service, which does not require persistence.
Firebase > context.js
import React, { createContext } from 'react';
import auth from './auth';
//Create the AuthContext.
const AuthContext = new createContext(null);
//AuthProvider is used in App.js (or app/index.js).
//When the auth state changes, the value passed to consumers will
// also change, so this is updated on the fly.
function AuthProvider(props){
const [currentUser, setCurrentUser] = React.useState(null);
auth.onAuthStateChanged(user=>{
if (user){
setCurrentUser(user)
}else{
setCurrentUser(null);
}
})
return(
<AuthContext.Provider value={currentUser}>
{props.children}
</AuthContext.Provider>
)
}
//Export the provider, but also the context because we need
// to use the context for the consumer. Consumers are added
// as needed and do not have a HOC.
export { AuthContext, AuthProvider }
As mentioned, a context creates a kind of “energy field” that allows data to “magically” get plucked from the air. This file imports the necessary libraries from React, and also the auth which connects to Firebase.
The key to this whole process lies in the AuthProvider function. A special component tag set will be placed around the entire website, and that will expose the firebase.currentUser.user object to basically everywhere. This object is pure ol’ JSON and will contain information about the currently logged in user (avatar URL, display name, email, and the service they used to log in). This set of tags does not expose auth, meaning that this component only deals with the aftermath of a successful registration, login, or logout operation, and it does that via auth.onAuthStateChanged.
onAuthStateChanged is an event handler that fires whenever…well, you can read it for yourself. The auth state will change whenever we move from non-auth’d to auth’d (registration or login), or vice versa (logout). If the event returns a user (aka the firebase.currentUser.user object) with data, we store the user info in the currentUser component state variable. if the event returns null, we clear the currentUser component state variable.
So long as the user logs in and doesn’t log out, this state variable remains populated, and we have info on the current user. We make that user data available to the rest of the site by defining the AuthContext “provider” higher-order component, passing in the currentUser state value as the component’s value…value. When we wrap the site in the resulting AuthProvider component tags, the tags we wrap will be rendered where {props.children} appear, and they and their children (and so on) will be able to access currentUser‘s value.
src > App.js
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
//AuthProvider is the context we use to provide the User to children.
import { AuthProvider } from './Firebase';
import SignIn from './Components/SignIn';
import Home from './Components/Home';
function App() {
return (
<div className="App">
<AuthProvider>
<BrowserRouter>
<Switch>
<Route path="/signin" component={SignIn}/>
<Route exact path="/" component={Home} />
</Switch>
</BrowserRouter>
</AuthProvider>
</div>
);
}
export default App;
As the entry point of the app, App.js does a lot regarding mapping and routing for the site, but for our purposes the most important part here is that the rest of the site is going to be rendered in between the <AuthProvider> tags. As we saw in the previous code block, doing this makes the data in the currentUser state variable available to any other components within those tags.
Components > SignIn > index.js
import React, { useState } from 'react';
import { Container, Row, Col } from 'react-bootstrap';
import { useHistory } from 'react-router-dom';
import { auth } from '../../Firebase';
const SignIn = () =>{
const [ email, setEmail ] = useState('');
const [ password, setPassword ] = useState('');
const history = useHistory();
const handleSubmit = (e) =>{
e.preventDefault();
//signInWithEmailAndPassword is a Firebase service function and only
//one of several authentication methods.
auth.signInWithEmailAndPassword(email, password)
.then(()=>{history.push("/");})
.catch(error=>console.log(`Caught error: ${error}`));
}
return(
<form onSubmit={handleSubmit}>
<Container>
<Row>
<Col lg={3}><label>Email</label></Col>
<Col lg={9}><input type="text" name="email" value={email} onChange={(e)=>setEmail(e.target.value)} /></Col>
</Row>
<Row>
<Col lg={3}><label>Password</label></Col>
<Col lg={9}><input type="password" name="password" value={password} onChange={(e)=>setPassword(e.target.value)} /></Col>
</Row>
<Row><Col style={{textAlign:"center"}}><input type="submit" value="Create"/></Col></Row>
</Container>
</form>
)
}
export default SignIn;
I have users in the system already so my tests have been strictly done with the sign in process.
As this component needs to access Firebase service methods, I had to import auth. I wanted to avoid having to get that close to the bare metal but right now this is by far the best option. I also have to import a lot of other things like layout from Bootstrap, core React stuff, and routing functionality from the Router DOM.
Email and password are stored in state so the site can have “controlled” form elements. When a user types into a textbox, the onChange event is fired with every keystroke. The onChange event takes whatever the user enters as they enter it and stores it into the appropriate state variable. The input boxes also derive their values from these state variables, making this a round-trip input-update-return process — which is why these are considered “controlled” form fields, since they are “controlled” by the state system.
The key point of interest here is the handleSubmit handler. When the form is submitted, the handler will prevent the form from re-submitting itself the HTML way, which by default will simply post back to itself (and also puts the form field data into the browser’s address bar which is a no-no especially since this page deals with a password). Then the handler takes the email and password info stored in the state variables and passes them to the auth.signInWithEmailAndPassword function.
There are many ways to sign in via Firebase. Eventually I’ll bring back the Google, Twitter, and Facebook login options, but I want to make email and password an option for folks who prefer to use that. This event handler returns a Promise which I believe I have mentioned before. A Promise is literally that: the handler takes data in and shuffles it on its way, and then immediately returns its attention to the caller, which is this component. Meanwhile, the data is being processed within the service and when that has completed, the Promise “fulfills” its mandate to notify the caller that it has completed. The then and catch statements handle whatever the handler method returns. If everything went smoothly, then the site will end up in then. If there was an error, the site will end up in catch. In the case of catch the site will spit out the error so I can view it in the debug console. If the process was successful, then I “push” a path — “/” or the designated root of the site — into the router’s history. This is how a React single-page-app (SPA) does redirects from code.
Components > Home > index.js
import React, { useContext } from 'react';
import { auth, AuthContext } from '../../Firebase';
import { useHistory } from 'react-router-dom';
function Home(){
const context = useContext(AuthContext);
const history = useHistory();
let msg;
if(context === null){
msg=(
<div>
<div>Please sign in to be welcomed</div>
<div><input type="button" onClick={()=>history.push("/signin")} value="Sign In"></input> </div>
</div>
);
}else{
msg=(
<div>
<div>Welcome back, {context.email}</div>
<div><input type="button" onClick={()=>auth.signOut()} value="Sign Out"></input> </div>
</div>
);
}
return <div>{msg}</div>
}
export default Home;
This more than any other component received my focus, because it was here that proved or disproved several of my theories.
At the top of the file I import a lot of stuff. Some React libraries, both auth and authContext from the Firebase code, and useHistory again for routing.
The main component defines a context variable here. Normally it’s assumed that Context is used a tag to wrap other components to provide or consume an object, but React exposes the useContext hook to get access to the Context Provider’s value object in code without having to use the HOC paradigm. By passing the context object into useContext, the value of context will contain whatever the value was defined as at the Provider level (in our case, that’s the JSON data representing the authenticated user).
Now, this next section I am not proud of, but I needed a quick solution and this was it. One of the coolest things about React is that code representing different states can be written side by side, and the app can choose which one to expose based on the data it has at the time. Since React’s claim to fame is being able to conditionally re-render parts of the site as data changes in real time, this is a massive boon. In this case, I’m checking the value of context and if it’s null, I’m defining a variable msg to have a value which informs the user that he or she should log in, and provides them with a button that uses history.push to send them to the sign in page. If context has a value — meaning we recognize a logged in user — then the site will display the user’s email and provide them with a button to instantly log out using the auth.signOut Firebase method.
The Result
I can’t show you what happens, but I’ll try and describe the flow:
- A new user loads the site and is greeted with a note asking them to log in with a button to take them to the sign in page
- At this point the currentUser state variable in the AuthContext is null. The Home component pulls this via useContext and displays the appropriate non-auth’d message and button.
- On the sign in page, the user enters her email and password and presses the SIGN IN button.
- As the user types, the email and password state variables are populated. This data is immediately re-read by the field’s value assignment.
- When the user clicks the button, the data in the state variables is sent to auth.signInWithEmailAndPassword handler. After the service returns a result to the app, an error will display in the console and the user will remain on the page, while a success will redirect the user to the Home page.
- Behind the scenes, the data we received from auth is stored in a local persistant storage. This is not the same as the Javascript localStorage, but rather as a cookie that Firebase’s library renders. This is what I was trying to accomplish until I realized that Firebase does it for me.
- As a result, the onAuthStateChanged event handler in Context fires because it’s detected that the user has logged in (successfully, we assume in this example). The state currentUser is updated with the firebase.currentUser.user JSON object, returned to use as what I choose to call just plain ‘ol user.
- The AuthContext.Provider value attribute is immediately updated with the new value of currentUser, making the user JSON instantly available to any components which A) are wrapped in AuthProvider (i.e. the entire site), and B) consumes the context either using <AuthContext.Consumer> or in code via useContext.
- Back on the Home page, the user sees the “welcome back” message with their email address displayed. There’s also a button that allows them to instantly log out (which they won’t do here).
- Because 2.5. happened above — the value of the HOC Provider wrapper was updated — the code in Home had a new value returned via useContext, and therefor and a new output block was rendered.
Looking Ahead
My tests so far are small but promising. I need to get my registration page operational.
An account management page is going to take some time because Firebase Auth only manages and exposes a few properties: displayName, photoUrl, email, and the name of the service that the user used (google.com, twitter.com, password, etc). I can’t change those things for OAuth services since they are pulled from those services, but I can change some of them if the user logged in with an email and password. However, in order to offer parity, I need to bring in Firestore, the database, to create profiles attached to the authentication data. This will mean that when a user registers, I’ll duplicate some data (username, email, and photo URL) along with some other preferences I want to offer. Then, once a user authenticates, it will be the Firestore profile record that will provide the info for username and portrait display. This gets tricky because I’ll need to keep the auth and profile in sync, and also find a way to expose one object that combines the auth data with the profile data.
Beyond that, I need to set up Storage, which is a Firebase storage depot. When a user uploads a new portrait, it’ll get stored with their user ID, and then both auth and profile will need to be updated with the URL to that image. Any time we display it, we use that URL, and any time the user wants to update it, we have to take the new image, and update both auth and profile.