Haliviq
React

ใช้ useDeferredValue แทน debounce กับ list ที่ re-render หนัก

debounce แค่หน่วงเวลาอัปเดต พอถึงเวลา render หนัก ๆ ก็ยังทำให้หน้าค้างอยู่ดี แต่ useDeferredValue ให้ React หยุด render ตรงกลางได้เมื่อมีการพิมพ์ตัวใหม่เข้ามา พิมพ์เลยลื่นแม้บนตาราง 5,000 แถว เหมาะกับกรณีที่ช่องค้นหาคุม list ใหญ่ ๆ และเราไม่อยากตั้งเวลารอแบบเดาเอา

เกิดอะไรขึ้น

ในโปรเจกต์หนึ่งของเรา ช่องค้นหาคุมตารางที่มี 5,000 แถว เราใส่ debounce ไว้ที่ input แล้ว ช่วงพิมพ์รู้สึกโอเค แต่พอหมดเวลาหน่วง หน้าเว็บจะค้างไปพักหนึ่งระหว่างที่ตาราง render ใหม่

ปัญหาคือ debounce ไม่ได้ทำให้งานหนักหายไป แค่ย้ายไปทำทีหลัง ทุกครั้งที่ timer ทำงาน ผู้ใช้ก็ยังโดนล็อกหน้าจนกว่า React จะ render เสร็จ

ทำไมถึงเป็นแบบนั้น

debounce หน่วงแค่จังหวะที่ state ถูกอัปเดต แต่พออัปเดตแล้ว งาน render ก็ใหญ่เท่าเดิม state update ปกติถือเป็นงานเร่งด่วน React จึงทำให้จบโดยไม่หยุดกลางทาง ตัวอักษรที่พิมพ์เข้ามาระหว่างนั้นต้องรออยู่ในคิว

useDeferredValue จะทำเครื่องหมายว่าค่านั้นมีความสำคัญต่ำกว่า React จะ render ด้วยค่าเก่าก่อน (เบา) แล้วค่อยเริ่ม render ด้วยค่าใหม่อยู่เบื้องหลัง ถ้ามีการพิมพ์ตัวใหม่เข้ามา React จะทิ้ง render ที่ทำค้างไว้แล้วเริ่มใหม่ด้วยค่าล่าสุด ช่องพิมพ์เลยตอบสนองทันตลอด

ตอนนี้เราทำอย่างไร

ตอนนี้เรา bind input กับ state ปกติ แล้วส่ง useDeferredValue(query) ให้ list เท่านั้น และห่อ list ด้วย memo เพราะถ้าไม่ห่อ component ลูกจะ render ซ้ำด้วยค่าเก่าอยู่ดี ก็ไม่ได้อะไรเลย เรายังเทียบ query กับ deferredQuery เพื่อทำให้ list จางลงระหว่างที่ยังตามไม่ทันได้ด้วย

แต่ debounce ยังเป็นเครื่องมือที่ถูกต้องเมื่อทุกการอัปเดตต้องยิง request ไป network เพราะ useDeferredValue ไม่ได้ลดจำนวน request ถ้าปัญหาคือต้นทุนการ render ฝั่ง client ล้วน ๆ มันช่วยตัดเวลารอที่ต้องเดาเองออกไปได้

ให้ input ใช้ค่าล่าสุด ส่วน list ใช้ค่าที่ถูกเลื่อนไว้tsx
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery

return (
  <>
    <input value={query} onChange={e => setQuery(e.target.value)} />
    <div style={{ opacity: isStale ? 0.6 : 1 }}>
      <HeavyList query={deferredQuery} />
    </div>
  </>
)

// HeavyList ต้องห่อด้วย memo
const HeavyList = memo(function HeavyList({ query }: { query: string }) {
  // render rows ที่ filter ด้วย query
  return null
})

เช็กลิสต์สั้น ๆ

  1. 1เก็บค่าใน input ไว้ใน state ปกติ เพื่อให้การพิมพ์ไม่ถูกหน่วง
  2. 2สร้าง deferredQuery ด้วย useDeferredValue(query)
  3. 3ส่ง deferredQuery ให้ list หนัก ๆ และห่อ list ด้วย memo
  4. 4แสดงสถานะจางลงเมื่อ query ยังไม่เท่ากับ deferredQuery

จำไว้สั้น ๆ

ถ้าอยากให้ render หนัก ๆ ถูกขัดจังหวะได้ ให้ใช้ useDeferredValue ส่วน debounce เก็บไว้ใช้ตอนต้องลดจำนวน request

เราอยากฟังว่าคุณกำลังสร้างอะไรอยู่

คุยกับเรา