The following quick start example builds upon the burrito
example in the reactfire quick start guide (opens in a new tab).
If you have not done so already, follow those instructions and we will pick up where it leaves off.
Concepts
Before we start coding, let's review some concepts.
The concept of how we will manage Server Side Rendering (SSR) is based (and heavily inspired ❤️) by the tanstack query (opens in a new tab) model for SSR.
Hydration
We will be Hydrating
the reactfire
data with pre fetched data which we dehydrate
from the server.
This simply means we will be using getServerSideProps
to fetch the data on the server and pass it to reactfire
as the initial data for queries.
The outcome is that there is no initial loading state for the data on the client, with the data available immediately, to then be updated with reactfire
as the data changes in firestore.
Key points to note
- Be careful not to use
firestore
andfirebase-admin
in the wrong placefirestore
is for the clientfirebase-admin
is for the server (getServerSideProps)
Hydrate component
reactfire-ssr
provides a Hydrate
component which is responsible for passing through any dehydrated
data to your nextjs pages.
Add this component to your _app.tsx
file.
Ensure it is a child of the FirestoreProvider
component from reactfire
.
import { Hydrate } from 'reactfire-ssr'
import {getFirestore} from "firebase/firestore";
import {
FirestoreProvider,
useFirebaseApp,
} from "reactfire";
export default function MyApp({ Component, pageProps }) {
const sdk = getFirestore(useFirebaseApp());
return (
<FirestoreProvider sdk={sdk}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
</FirestoreProvider>
)
}
Pre fetching data
Now we need to use getServerSideProps
to pre-fetch the data from firestore.
Because getServerSideProps
runs on the server we need to use the firebase-admin
sdk to access firestore not firebase
.
First initialize initReactfireSSR
which is the base object which all exposed methods you need are attached to (this also helps with type hinting).
This function accepts a generic type which you should populate with unique identifiers for each DocumentReference
and CollectionReference
you will be using.
We aim to remove the need for this in the future, but for now until we can figure out a better way to do this, this is the best solution.
The unique identifiers can be anything you want, you will pass them to the hooks when you access the data on the client.
Only call initializeReactfireSSR
once in your application. Its a good idea to do this in a separate file and import it where you need it.
import {initializeReactfireSSR} from "reactfire-ssr";
const reactfireSSR =
initializeReactfireSSR<
[
"burritoDocument",
"tryreactfireCollection",
"tryreactfireCollectionYummy"
]
>();
Use the dehydrate
method function which is a helper to unwrap a list of DocumentReference
and CollectionReference
objects,
pass this as props to the page to make it available to reactfire-ssr
hooks in your components.
You will notice the keys in the objects passed to dehydrate
match the unique identifiers you passed to initializeReactfireSSR
.
import {getFirestore} from "firebase-admin";
import {initializeReactfireSSR} from "reactfire-ssr";
/**
* Ensure you have initialized firebase somewhere,
* see the examples directory in the repository for an example
*/
const firestore = getFirestore()
const reactfireSSR =
initializeReactfireSSR<
[
"burritoDocument",
"tryreactfireCollection",
"tryreactfireCollectionYummy"
]
>();
export async function getServerSideProps() {
const burritoDocRef = firestore
.doc("tryreactfire/burrito")
.withConverter(burritoConverterAdmin); // This converter is optional, but it provides better type safety if you provide one
const tryreactfireCollectionRef = firestore
.collection("tryreactfire")
.withConverter(burritoConverterAdmin);
const tryreactfireCollectionRefYummy = tryreactfireCollectionRef.where(
"yummy",
"==",
true
);
const dehydrateData = await reactfireSSR.dehydrate(
{
burritoDocument: burritoDocRef,
burritoDocument2: burritoDocRef,
},
{
tryreactfireCollection: tryreactfireCollectionRef,
tryreactfireCollectionYummy: tryreactfireCollectionRefYummy,
}
);
return {
props: dehydrateData,
/**
* Or pass other props alongside the dehydrated data
*/
// props: { ...dehydrateData, myOtherProp: "goesHere" }
};
}
Using Pre fetched data
Now we can use the useHydratedFirestoreDocData
and useHydratedFirestoreCollectionData
hooks to access the pre-fetched data.
The first argument is the same as the useFirestoreDocData
and useFirestoreCollectionData
hooks which these wrap, the second argument is the unique identifier you passed to initializeReactfireSSR
.
Under the hood these hooks will use the dehydrated
data if it is available against the unique identifier you passed to the hook.
If it's available, your data will be available instantly on the client without a loading state.
import {reactfireSSR} from "reactfire-ssr";
const firestore = getFirestore()
export async function getServerSideProps() {
...
}
export default function Home() {
const burritoDocRef = doc(
useFirestore(),
"tryreactfire",
"burrito",
).withConverter(burritoConverter);
const collectionRef = collection(useFirestore(), "tryreactfire").withConverter(burritoConverter), // This converter is optional, but it provides better type safety
const burritoDoc = reactfireSSR.useHydratedFirestoreDocData(burritoDocRef, "burritoDocument");
const tryreactfireCollection = reactfireSSR.useHydratedFirestoreCollectionData(
collectionRef, "tryreactfireCollection"
);
if (
burritoDoc.status === "loading" ||
tryreactfireCollection.status === "loading"
) {
/**
* We are prefetching the required data on the server, so this scenario
* should never happen on the client.
*/
throw new Error("Should not happen!");
}
return (
<p>
There are {tryreactfireCollection.data.length} document(s) in the
collection! <br />
The burrito is {burritoDoc.data.yummy ? "good" : "bad"}!
</p>
);
}
Stuck?
If you get stuck there is a working example in the repository (opens in a new tab).
If you are still stuck, please open an issue (opens in a new tab)