React Native Academy · บทเรียน

ตัวรับฟังแบบเรียลไทม์และการคงข้อมูลออฟไลน์

สมัครรับคอลเลกชัน Firestore ด้วย onSnapshot รับการอัปเดตสดใน UI และเปิดใช้การคงข้อมูลออฟไลน์ เพื่อให้แอปอ่านข้อมูลที่แคชไว้ได้โดยไม่ต้องเชื่อมต่อ

บทเรียน 4 จาก 413 ขั้นตอน

ตัวรับฟังแบบเรียลไทม์และการคงข้อมูลออฟไลน์ เป็นบทเรียน React Native Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน React Native Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส React Native Academy มีบทเรียนทั้งหมด 4 บทเรียน

บางส่วนของบทเรียนนี้ยังไม่ได้รับการแปล และแสดงเป็นภาษาอังกฤษ

ตัวรับฟังแบบเรียลไทม์เทียบกับการอ่านครั้งเดียว

Firestore มีวิธีอ่านข้อมูลสองแบบ ได้แก่ การอ่านครั้งเดียวด้วย .get() และตัวรับฟังแบบเรียลไทม์ด้วย .onSnapshot() ตัวรับฟังแบบเรียลไทม์จะเปิดการเชื่อมต่อถาวรไว้ และเรียกฟังก์ชันเรียกกลับทุกครั้งที่เอกสารหรือผลลัพธ์คำค้นเปลี่ยนแปลง รวมถึงเมื่อโหลดข้อมูลครั้งแรก

ใช้ .get() เมื่อคุณต้องการข้อมูลเพียงครั้งเดียว (เช่น โหลดการตั้งค่าผู้ใช้เมื่อเริ่มแอป) ใช้ .onSnapshot() เมื่อข้อมูลเปลี่ยนแปลงบ่อยและส่วนติดต่อผู้ใช้ต้องเป็นข้อมูลล่าสุดอยู่เสมอ เช่น ฟีดแชตหรือคะแนนสด

การรับฟังเอกสารรายการเดียว

เรียกใช้ .onSnapshot(callback) กับ DocumentReference เพื่อรับฟังการเปลี่ยนแปลงของเอกสารเฉพาะรายการ ฟังก์ชันเรียกกลับจะทำงานทันทีพร้อมข้อมูลปัจจุบัน และทำงานอีกครั้งทุกครั้งที่เอกสารเปลี่ยนแปลง

.onSnapshot() จะส่งคืนฟังก์ชันยกเลิกการรับฟัง ให้จัดเก็บฟังก์ชันนี้ไว้ในตัวแปรและเรียกใช้ภายในการล้างข้อมูลของ useEffect เพื่อหยุดตัวรับฟังเมื่อคอมโพเนนต์ถูกถอดออก และป้องกันการรั่วไหลของหน่วยความจำ

import { useEffect, useState } from 'react';
import firestore from '@react-native-firebase/firestore';

export function usePost(postId: string) {
  const [post, setPost] = useState(null);

  useEffect(() => {
    const unsubscribe = firestore()
      .collection('posts')
      .doc(postId)
      .onSnapshot((snapshot) => {
        if (snapshot.exists) {
          setPost({ id: snapshot.id, ...snapshot.data() });
        }
      });

    return unsubscribe; // Stop listener on unmount
  }, [postId]);

  return post;
}

การรับฟังคำค้นของคอลเลกชัน

ผูก .onSnapshot() เข้ากับคำค้นของคอลเลกชันเพื่อรับการอัปเดตแบบสดทุกครั้งที่มีการเพิ่ม แก้ไข หรือลบเอกสารในชุดผลลัพธ์ QuerySnapshot ที่ส่งให้ฟังก์ชันเรียกกลับจะมีเอกสารทั้งหมดที่ตรงเงื่อนไข ไม่ใช่เฉพาะเอกสารที่เปลี่ยนแปลง

ดังนั้นทุกครั้งที่เอกสารเปลี่ยนแปลง คุณจะสร้างรายการทั้งหมดขึ้นใหม่จาก snapshot.docs วิธีนี้เหมาะสำหรับรายการขนาดเล็กถึงปานกลาง และแคชภายในเครื่องของ Firestore ช่วยให้ทำงานได้รวดเร็ว แม้ข้อมูลที่แคชไว้จะไม่ต้องเดินทางไปกลับผ่านเครือข่าย

useEffect(() => {
  const unsubscribe = firestore()
    .collection('messages')
    .where('roomId', '==', roomId)
    .orderBy('createdAt', 'desc')
    .limit(50)
    .onSnapshot((snapshot) => {
      const messages = snapshot.docs.map((doc) => ({
        id: doc.id,
        ...doc.data(),
      }));
      setMessages(messages);
    });

  return unsubscribe;
}, [roomId]);

การใช้ DocumentChange เพื่อเพิ่มประสิทธิภาพ

Instead of rebuilding the entire list on every snapshot, you can inspect the changes array to see exactly what was added, modified, or removed. Each entry in snapshot.docChanges() has a type property: 'added', 'modified', or 'removed'.

This approach is more efficient for large collections where only a few documents change at a time. You surgically update the local state array instead of replacing it entirely.

snapshot.docChanges().forEach((change) => {
  const item = { id: change.doc.id, ...change.doc.data() };

  if (change.type === 'added') {
    setItems((prev) => [item, ...prev]);
  } else if (change.type === 'modified') {
    setItems((prev) => prev.map((i) => (i.id === item.id ? item : i)));
  } else if (change.type === 'removed') {
    setItems((prev) => prev.filter((i) => i.id !== item.id));
  }
});

Handling Listener Errors

.onSnapshot() accepts a second argument for error handling. When the listener fails — typically due to a permission error from Firestore Security Rules — the error callback fires with a FirebaseError.

Common error codes include permission-denied (security rules blocked the read) and unavailable (offline and no cached data). Always provide an error handler so you can display a fallback UI instead of silently failing.

const unsubscribe = firestore()
  .collection('private-data')
  .onSnapshot(
    (snapshot) => {
      setData(snapshot.docs.map((doc) => doc.data()));
    },
    (error) => {
      if (error.code === 'permission-denied') {
        setError('You do not have access to this data.');
      } else {
        setError('Could not load data. Check your connection.');
      }
    }
);

Firestore Offline Persistence

React Native Firebase enables offline persistence by default on mobile. This means Firestore caches all documents you read or listen to in a local SQLite database on the device. When the device has no internet, reads return cached data immediately.

You do not need any configuration to enable this — it just works. You can control the cache size limit (default 40 MB). When the cache is full, Firestore evicts the least recently used documents. You can also set it to unlimited with cacheSizeBytes: firestore.CACHE_SIZE_UNLIMITED.

import firestore from '@react-native-firebase/firestore';

// Configure cache size (call before any Firestore operations)
await firestore().settings({
  cacheSizeBytes: firestore.CACHE_SIZE_UNLIMITED,
  persistence: true, // enabled by default on mobile
});

// After this, all reads and writes work offline automatically

Reading from Cache vs Server

By default, .get() tries the server first and falls back to the cache if offline. You can override this by specifying a source option: { source: 'cache' } always reads from local cache (instant, no network) and { source: 'server' } always goes to the network.

Cache-only reads are useful for optimistic UI patterns where you want to render existing data immediately while a fresh network request is in progress. If the document is not in the cache, a cache-only read throws an error.

// Always use cached data (instant, works offline)
const snapshot = await firestore()
  .collection('posts')
  .doc(postId)
  .get({ source: 'cache' });

// Always fetch from server (throws if offline)
const freshSnapshot = await firestore()
  .collection('posts')
  .doc(postId)
  .get({ source: 'server' });

// Default: server first, cache fallback
const snapshot2 = await firestore()
  .collection('posts')
  .doc(postId)
  .get();

Pagination with Query Cursors

For large collections, load data in pages using query cursors: .startAfter(lastDocument). You keep a reference to the last document you loaded and pass it to the next query to get the next page. This is known as cursor-based pagination.

Store the last document snapshot in state. When the user scrolls to the bottom of the list (FlatList's onEndReached), trigger a new query starting after the last document. Append the new results to the existing list.

const [posts, setPosts] = useState([]);
const [lastDoc, setLastDoc] = useState(null);
const [hasMore, setHasMore] = useState(true);

async function loadMore() {
  if (!hasMore) return;

  let query = firestore()
    .collection('posts')
    .orderBy('createdAt', 'desc')
    .limit(20);

  if (lastDoc) {
    query = query.startAfter(lastDoc);
  }

  const snapshot = await query.get();
  if (snapshot.empty) {
    setHasMore(false);
    return;
  }

  setLastDoc(snapshot.docs[snapshot.docs.length - 1]);
  const newPosts = snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
  setPosts((prev) => [...prev, ...newPosts]);
}

Composite Indexes

When you combine .where() and .orderBy() on different fields, Firestore requires a composite index. Without it, the query throws a failed-precondition error with a URL to create the index automatically.

Composite indexes are configured in firestore.indexes.json and deployed with the Firebase CLI. During development, just click the URL from the error log — Firebase creates the index for you. For production, always manage indexes in code so they are version controlled.

// This query requires a composite index on (userId ASC, createdAt DESC)
const snapshot = await firestore()
  .collection('posts')
  .where('userId', '==', userId)
  .orderBy('createdAt', 'desc')
  .get();

// If the index is missing, you see:
// FirebaseError: The query requires an index.
// https://console.firebase.google.com/project/.../firestore/indexes?...
// Click the link to auto-create the index

Detaching Listeners on Screen Change

Real-time listeners consume bandwidth and battery. Detach them when the user navigates away from a screen by using React Navigation's useFocusEffect hook instead of useEffect. useFocusEffect runs the effect when the screen comes into focus and cleans up when the screen loses focus.

This way, you only maintain an active Firestore listener when the relevant screen is visible, and the listener is paused (and re-established) as the user navigates away and back.

import { useFocusEffect } from '@react-navigation/native';
import { useCallback } from 'react';

export function ChatScreen({ route }) {
  useFocusEffect(
    useCallback(() => {
      // Start listener when screen is focused
      const unsubscribe = firestore()
        .collection('messages')
        .onSnapshot(handleSnapshot);

      // Stop listener when screen loses focus
      return unsubscribe;
    }, [route.params.roomId])
  );
}

Optimistic Updates with Local State

For a snappy user experience, apply changes to local state immediately before the Firestore write completes — this is called an optimistic update. Show the change right away, then let the real-time listener confirm or correct it when the server response arrives.

For example, when the user likes a post, increment the like count in local state immediately. If the server write fails, revert the local state by decrementing the count and showing an error toast. This avoids perceived lag on slow connections.

async function toggleLike(postId: string, currentlyLiked: boolean) {
  // 1. Optimistically update local state
  setLiked(!currentlyLiked);
  setLikeCount((prev) => prev + (currentlyLiked ? -1 : 1));

  try {
    // 2. Persist to Firestore
    await firestore().collection('posts').doc(postId).update({
      likesCount: firestore.FieldValue.increment(currentlyLiked ? -1 : 1),
    });
  } catch (error) {
    // 3. Revert on failure
    setLiked(currentlyLiked);
    setLikeCount((prev) => prev + (currentlyLiked ? 1 : -1));
    Alert.alert('Error', 'Could not update like. Try again.');
  }
}

Quick Check

Test your understanding of React Native Mobile Development concepts from this lesson.

Lesson Recap

In this lesson you learned: how to attach real-time listeners with .onSnapshot() for live data updates, how Firestore offline persistence automatically caches data for offline use, and how to paginate large collections using startAfter cursor-based pagination. Next up we profile React Native apps and eliminate performance bottlenecks.

เริ่มต้นได้ฟรี

เรียนรู้ JavaScript ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
30
บทเรียน
120

คำถามที่พบบ่อย

บทเรียน “ตัวรับฟังแบบเรียลไทม์และการคงข้อมูลออฟไลน์” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “ตัวรับฟังแบบเรียลไทม์และการคงข้อมูลออฟไลน์” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส React Native Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส React Native Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “ตัวรับฟังแบบเรียลไทม์และการคงข้อมูลออฟไลน์”

สมัครรับคอลเลกชัน Firestore ด้วย onSnapshot รับการอัปเดตสดใน UI และเปิดใช้การคงข้อมูลออฟไลน์ เพื่อให้แอปอ่านข้อมูลที่แคชไว้ได้โดยไม่ต้องเชื่อมต่อ คุณปฏิบัติ React Native Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน React Native Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน React Native Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “ตัวรับฟังแบบเรียลไทม์และการคงข้อมูลออฟไลน์” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน React Native Academy นี้ได้ไหม

ได้ บทเรียน React Native Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การเชื่อม React Native Firebase กับโปรเจกต์
  2. การยืนยันตัวตนด้วยอีเมลและโทรศัพท์ผ่าน Firebase
  3. การอ่านและเขียนเอกสาร Firestore
  4. ตัวรับฟังแบบเรียลไทม์และการคงข้อมูลออฟไลน์
← กลับไปที่ React Native Academy