I'm using React Native/Typescript/Expo and Firebase (Auth, Firestore). I'm testing in web. I'm using Tanstack Query between my app and Firebase.
I'm running into an issue where, when upserting a user object with partial fields, then navigating and fetching the same user object, the impartial upsert object appears to be returned even though the remote object already contained more fields before I started.
After navigating, or after waiting for the query to refetch, the full object is suddenly returned.
The following sequence diagram presents a sequence flow.
mermaid.initialize({ startOnLoad: true });
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/font-awesome.min.css">
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<pre class="mermaid">
sequenceDiagram
actor User
participant IDP as Firebase Auth
participant App as Application
participant Firestore
User->>IDP: Sign up
IDP-->>User: Account created
Note over User, Firestore: Sign-in flow
User->>IDP: Sign in
IDP-->>App: Authentication successful
App->>Firestore: Initialize user with minimal fields
Note left of Firestore: Merge to preserve<br />existing data if user<br/>record already exists
Firestore-->>App: User initialized/updated
App->>User: Redirect to home screen
Note over User, Firestore: Home screen data flow
User->>App: Access home screen
App->>Firestore: Request full user object
rect rgb(240, 240, 240)
Note over App, Firestore: Expectation vs Reality
rect rgb(220, 255, 220)
Note left of Firestore: Expectation:<br/>Full user object returned
end
rect rgb(255, 220, 220)
Note left of Firestore: Reality:<br/>Only partial initialization<br/>object returned
end
end
Firestore-->>App: Return partial user object
App-->>User: Display home screen with incomplete user data
</pre>
This is the creation code used, for which I have validated the values of User and merge (true).
export async function putUser(user: User, merge: boolean) {
setLogLevel('debug')
const db = getFirebaseDb();
let userRef = doc(db, COLLECTION_USERS, user.uid)
let data = null
if (merge && user.progress.length === 0) {
const { progress, ...rest } = user
data = rest
} else {
data = user
}
await setDoc(userRef, data, { merge: merge });
}
This is the fetch code, where I've already removed the converter and replaced getDoc with getDocFromServer. The getUser print here does not contain the full object, but instead something that looks like the persisted partial object.
export async function getUser(uid: string): Promise<User | null> {
setLogLevel('debug')
const db = getFirebaseDb()
const userRef = doc(db, COLLECTION_USERS, uid)
const userDoc = await getDocFromServer(userRef)
if (!userDoc.exists()) {
console.error(`User ${uid} not found`)
return null;
}
const data = userDoc.data()
const ud = {
uid: userDoc.id,
email: data.email,
displayName: data.displayName,
isActive: data.isActive ?? true,
progress: data.progress || []
}
console.log('getUser', {...ud})
return ud;
}
On login:
initUser.mutate({
uid: cred.user!.uid!,
email: cred.user.email!,
displayName: cred.user.email!.split('@')[0],
isActive: true,
courseProgress: []
}, {
onSuccess: () => {
setIsLoading(false)
router.push('/(drawer)');
}})
In a component below '/(drawer)', note that the query is enabled by uid having a value:
const auth = useAuth()
const { data: user, isLoading, error, isSuccess } = useUser(auth.user?.uid)
I'm confident this has little to do with Tanstack Query, and I'm not sure this has to do with the Firebase local cache since I'm calling getDocFromServer. How can this behavior be explained?
Edit: a createIfNotExists workaround does work, but I'm trying to understand why my approach didn't.
export async function createIfNotExist(user: User) {
const db = getFirebaseDb()
const userRef = doc(db, COLLECTION_USERS, user.uid)
const userDoc = await getDoc(userRef)
if (!userDoc.exists()) {
await setDoc(userRef, user)
}
}