Introducing React Firebase ๐
A Composable, Component based React API over Firebase services.
TLDR;
I just released @react-firebase to make integrating Firebase in your React App fast & easy ๐. If you use React & Firebase, go check it out !
The problem
I โค๏ธ Firebase & I โค๏ธ React.
I use both extensively at work and in side-projects.
But getting them to work together on large codebases with many contributors, would lead to :
- Un-necessary complexity ( Where is this component loading its data from ๐ค ? )
- Subtle bugs with subscription management. ๐
- Zombie listeners sticking around when your UI doesn't need them. ๐
This solution
@react-firebase
offers small, typed, well-tested composable components which allow easy integration of Firebase Auth, Realtime Database and/or Firestore in your React or React Native app.
Overview
@react-firebase/auth
, @react-firebase/database
, @react-firebase/firestore
export Provider and Consumer Components.
Providers
The Provider
components take in firebase
and firebase config options props and should be rendered once in your app as an ancestor to your Consumer
components.
How do I get my Firebase config ?
Web or React Native with Expo
If you're using firebase
Change PROJECT_NAME
to your project name and grab your firebase config here :
https://console.firebase.google.com/project/PROJECT_NAME/settings/general/
Your config file should look something like this :
// Firebase Config
const config = {
apiKey: "API_KEY",
projectId: "PROJECT_ID",
databaseURL: "DATABASE_URL",
authDomain: "AUTH_DOMAIN",
// OPTIONAL
storageBucket: "STORAGE_BUCKET",
messagingSenderId: "MESSAGING_SENDER_ID"
};
React Native Firebase
If you're using react-native-firebase then you don't need to pass any other prop because the firebase app is initialized from the native side.
Provider Examples
import * as firebase from "firebase/app";
import "firebase/auth";
import { FirebaseAuthProvider } from "@react-firebase/auth";
// And or
import { FirebaseDatabaseProvider } from "@react-firebase/database";
import "firebase/database";
// And or
import "firebase/firestore";
import { FirestoreProvider } from "@react-firebase/firestore";
<FirebaseAuthProvider firebase={firebase} {...config}>
<MyApp1 />
<MyApp2 />
</FirebaseAuthProvider>
Consumers
Every Module exports Consumer components that will consume and react (๐ ) to Firebase changes.
All Consumer Components expect their children
prop to be any valid React Node or any sync function that returns any valid React Node. (library-specific examples below ๐)
Firebase Auth Consumers
FirebaseAuthConsumer
Anywhere inside your app component tree, add a FirebaseAuthConsumer to listen and update the ui when the user's auth status changes.
<FirebaseAuthConsumer>
{({ isSignedIn, user, providerId }) => {
return (
<pre style={{ height: 300, overflow: "auto" }}>
{JSON.stringify({ isSignedIn, user, providerId }, null, 2)}
</pre>
);
}}
</FirebaseAuthConsumer>
Firebase Auth Helpers Components
The following are tiny wrapper components over FirebaseAuthConsumer that do common things you might want in an app.
Example :IfFirebaseAuthed
<IfFirebaseAuthed>
{() => {
return <div>You are authenticated</div>;
}}
</IfFirebaseAuthed>
Or
<IfFirebaseAuthed>
<AppWhenAuthed />
</IfFirebaseAuthed>
IfFirebaseUnAuthed
<IfFirebaseUnAuthed>
() => (
<div>
<p>You are not authenticated. This div will disappear when you login.</p>
</div>
)
</IfFirebaseUnAuthed>
Or
<IfFirebaseUnAuthed>
<AppWhenUnAuthed />
</IfFirebaseUnAuthed>
And sometimes you might want to add additional restrictions to some of your UI. For example, you want to show something only if the user is anonymously un-authenticated, or only if they have a certain email or provider.
For that you can use :
IfFirebaseAuthedAnd
<IfFirebaseAuthedAnd filter={({ providerId }) => providerId !== "anonymous"}>
{({ isSignedIn, user, providerId }) => {
return (
<div>
<p>Hello {JSON.stringify(user)}</p>
<p>You are authenticated with {providerId}</p>
</div>
);
}}
</IfFirebaseAuthedAnd>
Or
const AppWhenAuthedAndAnonymous = ({ isSignedIn, user, providerId }) => (
<div>
<p>Hello {JSON.stringify(user)}</p>
<p>You are authenticated with {providerId}</p>
</div>
);
Then in an ancestor's render method :
<IfFirebaseAuthedAnd filter={({ providerId }) => providerId !== "anonymous"}>
<AppWhenAuthedAndAnonymous />
</IfFirebaseAuthedAnd>
API Reference
Firebase Database Consumers
Read Data
FirebaseDatabaseNode
Anywhere inside your app component tree, add a FirebaseDatabaseNode
to listen and update the ui when the data changes at the provided path.
<FirebaseDatabaseNode path={"user_bookmarks/BOOKMARK_ID"}>
{({ value, isLoading, firebase }) => {
return (
<pre style={{ height: 300, overflow: "auto" }}>
{JSON.stringify({ value, isLoading }, null, 2)}
</pre>
);
}}
</FirebaseDatabaseNode>
Write Data
FirebaseDatabaseMutation
& FirebaseDatabaseTransaction
The mutation API is inspired by apollo-graphql mutations.
The mutation components inject a runMutation
or runTransaction
prop, respectively to their children.
FirebaseDatabaseMutation
, in addition to the path
prop requires a mutation type
prop that can be one of :
"set" | "update" | "push";
FirebaseDatabaseMutation Example.
<FirebaseDatabaseMutation path={collectionPath} type="push">
{({ runMutation }) => (
<button
onClick={async () => {
const { key } = await runMutation(testDocValue);
if (key === null || typeof key === "undefined") return;
this.setState(state => ({
keys: [...state.keys, key]
}));
}}
>
add-node-with-generated-key
</button>
)}
</FirebaseDatabaseMutation>
FirebaseDatabaseTransaction Example.
<FirebaseDatabaseTransaction path={path}>
{({ runTransaction }) => {
return (
<div>
<button
onClick={() => {
runTransaction({
reducer: val => {
if (val === null) {
return 1;
} else {
return val + 1;
}
}
}).then(() => {
console.log("Ran transaction");
});
}}
>
Click me to run a transaction
</button>
</div>
);
}}
</FirebaseDatabaseTransaction>
Firestore Database Consumers
FirestoreDocument
& FirestoreCollection
Read Data with Anywhere inside your app component tree, add a FirestoreDocument
or FirestoreCollection
to listen to the data and update the ui when it changes.
FirestoreDocument
Example.
const s = v => JSON.stringify(v, null, 2);
const App = () => (
<FirestoreDocument path={path}>
{({ value, path, isLoading }) => {
return (
<div>
{value && <pre>{s(value)}</pre>}
{path && <div>{path}</div>}
<div>{isLoading}</div>
</div>
);
}}
</FirestoreDocument>
);
FirestoreDocument
API
FirestoreCollection
Example
<FirestoreCollection path="/user_bookmarks/" limit={5}>
{d => {
return <pre>{s(d)}</pre>;
}}
</FirestoreCollection>
FirestoreCollection
API
FirestoreMutation
& FirestoreTransaction
& FirestoreBatchedWrite
Write Data with The mutation API is inspired by apollo-graphql mutations.
The mutation components FirestoreMutation
& FirestoreTransaction
inject a runMutation
or runTransaction
prop, respectively to their children.
FirestoreMutation
in addition to the path
prop requires a mutation type
prop that can be one of :
"set" | "update" | "add";
The BatchedWrite
exposes a different API because it behaves differently than writes and transactions, because it can run many updates before committing
them.
It injects 2 props, addMutationToBatch
and commit
, to its children.
addMutationToBatch
type addMutationToBatch = (
{
path,
value,
type
}: {
path: string;
value: any;
type: "add" | "update" | "set" | "delete";
}
) => void;
commit
type commit = () => Promise<void>;
FirestoreMutation
Example
<FirestoreProvider firebase={firebase} {...config}>
<FirestoreMutation type="set" path={path}>
{({ runMutation }) => {
return (
<div>
<h2> Mutate state </h2>
<button
onClick={async () => {
const mutationResult = await runMutation({
nowOnCli: Date.now(),
nowOnServer: firebase.firestore.FieldValue.serverTimestamp()
});
console.log("Ran mutation ", mutationResult);
}}
>
Mutate Set
</button>
</div>
);
}}
</FirestoreMutation>
</FirestoreProvider>
FirestoreTranasaction
Example
<FirestoreProvider firebase={firebase} {...config}>
<FirestoreTransaction>
{({ runTransaction }) => {
return (
<div>
<button
data-testid="test-set"
onClick={async () => {
await runTransaction(async ({ get, update }) => {
const res = await get({
path
});
const data = res.data();
if (typeof data === "undefined") return;
await update(path, {
...data,
a: isNaN(parseInt(data.a, 10)) ? 1 : data.a + 1
});
});
}}
>
runTransaction
</button>
</div>
);
}}
</FirestoreTransaction>
</FirestoreProvider>
FirestoreBatchedWrite
Example
<FirestoreBatchedWrite>
{({ addMutationToBatch, commit }) => {
return (
<div>
<h2>Batched write</h2>
<button
onClick={() => {
console.log("adding to batch");
addMutationToBatch({
path,
value: { [`a-value-${Date.now()}`]: Date.now() },
type: "set"
});
}}
>
Add to batch
</button>
<button
onClick={() => {
console.log("committing to batch");
commit().then(() => {
console.log("Committed");
});
}}
>
Commit batch
</button>
</div>
);
}}
</FirestoreBatchedWrite>
Resources
Editable Sandboxes
Verbose Tutorial (WIP)
Bookmarking App Example
React Native with Expo and Firebase Example
React Native Firebase Starter Example
Related Work
Acknowledgements
- The React team.
- The Firebase team for building and maintaining a great product !
- @mwestrate's for writing immer (and mobx, not used in this project, but a great library nonetheless ๐).
- The Docusaurus team (This website and blog are built with docusaurus).
Thanks for making it to the end of the article โค๏ธ !
Here's a couple of additional resources just for you :