A document is the basic unit of data storage in Firestore. Think of it as a single record that contains fields and values, similar to a row in a spreadsheet or an object in JavaScript. Each document has a unique identifier (ID) within its collection.
// Example document structure
{
name: "Alice",
score: 95,
lastPlayed: Timestamp.now(),
preferences: {
difficulty: "medium",
soundEnabled: true
}
}A collection is a container for documents, like a folder that holds files. Collections can only contain documents (not other collections directly), and each document in a collection must have a unique ID.
A subcollection is a collection that exists within a specific document. This creates a hierarchical structure. For example, a game document might have a subcollection of players.
games (collection)
└── gameABC123 (document)
└── players (subcollection)
├── Alice (document)
└── Bob (document)A reference is a lightweight object that points to a location in your database. References don't contain data—they're like addresses that tell Firestore where to find or place data. You use references to read from or write to specific locations.
A snapshot is the actual data retrieved from Firestore at a specific point in time. When you read a document, you get a snapshot that contains the document's current data and metadata.
To read data from Firestore, you first create a reference to the document or collection you want to read, then use getDoc() for single documents or getDocs() for collections.
import { doc, getDoc } from 'firebase/firestore';
import { db } from '@/firebase';
async function readPlayerData() {
// Create a reference to a specific document
const playerRef = doc(db, 'games', 'gameABC123', 'players', 'Alice');
// Get the snapshot
const snapshot = await getDoc(playerRef);
// Check if the document exists
if (snapshot.exists()) {
// Access the data
const data = snapshot.data();
console.log('Player data:', data);
console.log('Player score:', data.score);
} else {
console.log('No such document!');
}
}import { collection, getDocs } from 'firebase/firestore';
import { db } from '@/firebase';
async function readAllPlayers() {
// Create a reference to a collection
const playersRef = collection(db, 'games', 'gameABC123', 'players');
// Get all documents in the collection
const snapshot = await getDocs(playersRef);
// Iterate through the documents
snapshot.forEach((doc) => {
console.log('Player ID:', doc.id);
console.log('Player data:', doc.data());
});
}Writing data involves creating or updating documents. Firestore provides different methods depending on your needs.
Use setDoc() to create a new document or completely overwrite an existing one:
import { doc, setDoc, Timestamp } from 'firebase/firestore';
import { db } from '@/firebase';
async function createPlayer() {
// Create a reference with a specific ID
const playerRef = doc(db, 'games', 'gameABC123', 'players', 'Charlie');
// Set the document data
await setDoc(playerRef, {
name: 'Charlie',
score: 0,
joinedAt: Timestamp.now(),
level: 1
});
console.log('Player created!');
}Use updateDoc() to update only specific fields without overwriting the entire document:
import { doc, updateDoc } from 'firebase/firestore';
import { db } from '@/firebase';
async function updatePlayerScore() {
const playerRef = doc(db, 'games', 'gameABC123', 'players', 'Charlie');
// Update only the score field
await updateDoc(playerRef, {
score: 150,
lastUpdated: Timestamp.now()
});
console.log('Score updated!');
}Use addDoc() when you want Firestore to generate a unique ID:
import { collection, addDoc, Timestamp } from 'firebase/firestore';
import { db } from '@/firebase';
async function addNotification() {
const notificationsRef = collection(db, 'games', 'gameABC123', 'notifications');
const docRef = await addDoc(notificationsRef, {
message: 'New achievement unlocked!',
timestamp: Timestamp.now(),
read: false
});
console.log('Notification added with ID:', docRef.id);
}Creating a new game instance with initial data:
import { doc, setDoc, Timestamp } from 'firebase/firestore';
import { db } from '@/firebase';
async function setupNewGame(gameCode, hostId) {
const gameRef = doc(db, 'games', gameCode);
await setDoc(gameRef, {
metadata: {
type: 'VirusGame',
gameCode: gameCode,
hostId: hostId
},
lastPlayed: Timestamp.now(),
players: [],
status: 'initializing',
gameParams: {
difficulty: 'medium',
maxPlayers: 30
}
});
console.log(`Game ${gameCode} created!`);
}Adding a player to an existing game:
import { doc, updateDoc, arrayUnion, setDoc, Timestamp } from 'firebase/firestore';
import { db } from '@/firebase';
async function joinGame(gameCode, playerName, userId) {
// First, add player name to the game's players array
const gameRef = doc(db, 'games', gameCode);
await updateDoc(gameRef, {
players: arrayUnion(playerName)
});
// Then, create the player document in the subcollection
const playerRef = doc(db, 'games', gameCode, 'players', playerName);
await setDoc(playerRef, {
name: playerName,
currentUid: userId,
joinedAt: Timestamp.now(),
approved: false,
score: 0
});
console.log(`${playerName} joined game ${gameCode}!`);
}Checking game status and updating it:
import { doc, getDoc, updateDoc } from 'firebase/firestore';
import { db } from '@/firebase';
async function startGameIfReady(gameCode) {
const gameRef = doc(db, 'games', gameCode);
const snapshot = await getDoc(gameRef);
if (!snapshot.exists()) {
console.error('Game not found!');
return;
}
const gameData = snapshot.data();
// Check if we can start the game
if (gameData.status === 'initializing' && gameData.players.length >= 2) {
await updateDoc(gameRef, {
status: 'running',
gameStartedAt: Timestamp.now()
});
console.log('Game started!');
} else {
console.log('Game not ready to start');
}
}This project provides several conveniences that make working with Firebase much easier:
The most important thing to understand is that you rarely need to read data directly from Firestore. The project automatically keeps a Zustand store synchronized with all relevant Firestore data. This means you can simply read from the store in your components:
// Instead of manually reading from Firestore, use the game-specific store hook
const playerScore = useVirusGameStore(state => state.playerData.score);
const allPlayers = useVirusGameStore(state => state.players);
const stations = useVirusGameStore(state => state.subcollections.stations);The data in the store updates automatically whenever the Firestore data changes, so your components will always show the latest information.
The project provides convenient helper functions in refs.ts to get commonly-used references without manually constructing paths:
import { gameDoc, playerDoc, gameSubcol } from '@/firebase';
// Get a reference to a game document
const gameRef = gameDoc<'VirusGame'>('ABC123');
// Get a reference to a player document
const playerRef = playerDoc<'VirusGame'>('ABC123', 'Alice');
// Get a reference to a subcollection
const stationsRef = gameSubcol<'VirusGame'>('ABC123')('stations');These helpers ensure type safety and prevent errors from incorrect paths.
While reading is handled by the store, you still write directly to Firestore using the standard Firebase methods. The typical pattern is:
import { updateDoc } from 'firebase/firestore';
import { gameDoc } from '@/firebase';
function MyGameComponent() {
const gameCode = useGameCode();
const currentScore = useVirusGameStore(state => state.gameData.totalScore);
const incrementScore = async () => {
// Write directly to Firestore
await updateDoc(gameDoc<'VirusGame'>(gameCode), {
totalScore: currentScore + 10
});
// No need to update local state - the store will sync automatically!
};
return <button onClick={incrementScore}>Add 10 Points</button>;
}The project uses TypeScript to ensure type safety throughout. When you use the game-specific store hook or helper functions with the game type specified, TypeScript will:
Different components have access to different data based on their role:
This is handled automatically by the useGameDataSync hook that runs in the background.
This infrastructure means you can focus on building your game features without worrying about the complexities of real-time data synchronization or manual state management.