ข้ามไปยังเนื้อหา

React PWA Patterns

นอกจาก update prompt แล้ว React PWA ยังได้ประโยชน์จาก utility hook สองตัว ได้แก่ hook สำหรับตรวจสอบสถานะเครือข่าย และ hook สำหรับ install prompt โดย hook เหล่านี้ห่อหุ้ม browser API ไว้ภายใต้ React abstraction ที่สะอาด ทำให้ component ยังคงเป็น declarative

useOnlineStatus hook อ่านค่า navigator.onLine ตอน mount และคอยอัปเดตค่าให้ตรงกันเสมอโดยฟัง event online และ offline บน window การใช้ guard typeof navigator ทำให้ hook ปลอดภัยในสภาพแวดล้อม SSR ที่ไม่มี window

import { useState, useEffect } from 'react';
export function useOnlineStatus(): { isOnline: boolean } {
const [isOnline, setIsOnline] = useState<boolean>(
typeof navigator !== 'undefined' ? navigator.onLine : true
);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return { isOnline };
}

ตัวอย่างการใช้งาน:

import { useOnlineStatus } from './useOnlineStatus';
export function NetworkBanner() {
const { isOnline } = useOnlineStatus();
return (
<div className={isOnline ? 'banner banner--online' : 'banner banner--offline'}>
{isOnline ? 'Online' : 'Offline'}
</div>
);
}

browser จะยิง event beforeinstallprompt ก่อนที่จะแสดง native install UI การเก็บ event นั้นไว้ใน useRef ทำให้คุณสามารถเรียก prompt จากปุ่มได้ทุกเมื่อ หลังจากผู้ใช้ตอบสนองแล้ว ให้ล้าง ref เพื่อให้ปุ่มหายไป

import { useState, useEffect, useRef } from 'react';
interface BeforeInstallPromptEvent extends Event {
prompt(): Promise<void>;
readonly userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
export function useInstallPrompt(): {
isInstallable: boolean;
promptInstall: () => void;
} {
const deferredPrompt = useRef<BeforeInstallPromptEvent | null>(null);
const [isInstallable, setIsInstallable] = useState(false);
useEffect(() => {
function handleBeforeInstallPrompt(e: Event) {
e.preventDefault();
deferredPrompt.current = e as BeforeInstallPromptEvent;
setIsInstallable(true);
}
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
};
}, []);
async function promptInstall() {
if (!deferredPrompt.current) return;
await deferredPrompt.current.prompt();
await deferredPrompt.current.userChoice;
deferredPrompt.current = null;
setIsInstallable(false);
}
return { isInstallable, promptInstall };
}

ตัวอย่างการใช้งาน:

import { useInstallPrompt } from './useInstallPrompt';
export function InstallButton() {
const { isInstallable, promptInstall } = useInstallPrompt();
if (!isInstallable) return null;
return (
<button onClick={promptInstall}>
Install app
</button>
);
}

Create React App ไม่ได้รับการดูแลรักษาอย่างต่อเนื่องอีกต่อไป สำหรับ React PWA ใหม่ ให้ใช้ Vite ร่วมกับ vite-plugin-pwa ซึ่งเร็วกว่า ปรับแต่งได้มากกว่า และยังมีการพัฒนาอยู่ หากโปรเจกต์ต้องการ SSR หรือ full-stack React solution ให้ใช้ Next.js ร่วมกับ package ที่ใช้ Workbox เช่น @ducanh2912/next-pwa หรือ serwist

สำหรับโปรเจกต์ Next.js App Router hook virtual:pwa-register/react ใช้ได้เฉพาะกับ Vite เท่านั้น ไม่สามารถใช้ได้ที่นี่ แนวทางที่แนะนำคือ package @ducanh2912/next-pwa ซึ่งสร้างบน Workbox และรองรับ server-component model ของ App Router หรือจะใช้ library serwist ที่เป็น Workbox wrapper แบบ framework-agnostic พร้อม support ระดับ first-class สำหรับ Next.js ทั้งสองตัวสร้าง service worker ตอน build time และ integrate กับ Next.js build pipeline

ตัวเลือกBenefitCost
ผูก PWA state (installable, update available) เข้ากับ React state/hookUI ตอบสนอง lifecycle ของ service worker ได้ทันที เขียนแบบ declarativeต้องระวัง re-render ที่ไม่จำเป็นถ้า hook ออกแบบไม่ดี
จัดการ service worker แบบแยกจาก React tree (vanilla JS listener)ไม่ผูกกับ re-render cycle ของ React เลย เขียนง่ายกว่าในเคสง่ายๆต้อง sync state กลับเข้า React เองด้วยมือ เพิ่มจุดที่ bug ได้
  • เรียก logic ของ service worker (register, event listener) ใน render function ตรงๆ แทนที่จะอยู่ใน useEffect ทำให้ทำงานซ้ำทุก re-render
  • ไม่ cleanup event listener ใน useEffect cleanup function ทำให้เกิด listener ซ้อนหลายตัวเมื่อ component mount ใหม่
  • ผูก PWA install prompt state ไว้ใน local component state แทน global store ทำให้ปุ่มติดตั้งไม่ sync กันข้ามหลาย component

💡 ตัวอย่างจากของจริง

Vite PWA ecosystem (Elk) — ใช้ hook ครอบ virtual:pwa-register เพื่อให้ UI แจ้งเตือน update ได้แบบ declarative

Squoosh — ผูก service worker lifecycle เข้ากับ UI state เพื่อแจ้งผู้ใช้เมื่อมีเวอร์ชันใหม่พร้อมใช้

Browser event ใดที่ยิงขึ้นก่อนที่ install prompt จะแสดงให้ผู้ใช้เห็น?
เหตุใด useOnlineStatus จึงต้องใช้ typeof เพื่อตรวจสอบ navigator.onLine?
Package ใดที่แนะนำสำหรับเพิ่ม PWA support ให้กับโปรเจกต์ Next.js App Router?
ควรทำอะไรหลังจากผู้ใช้ตอบสนองต่อ install prompt?