ข้อมูลค้างขณะตรวจสอบการดึงข้อมูลด้วย React Hooks: A Guide

เผยแพร่แล้ว: 2022-03-11

การใช้ประโยชน์จากส่วนขยาย HTTP Cache-Control ที่เก่าขณะตรวจสอบใหม่เป็นเทคนิคยอดนิยม มันเกี่ยวข้องกับการใช้เนื้อหาที่แคช (เก่า) หากพบในแคช จากนั้นตรวจสอบความถูกต้องของแคชอีกครั้งและอัปเดตด้วยเนื้อหาเวอร์ชันใหม่กว่าหากจำเป็น ดังนั้นชื่อ stale-while-revalidate

วิธี stale-while-revalidate

เมื่อมีการส่งคำขอเป็นครั้งแรก เบราว์เซอร์จะแคชคำขอไว้ จากนั้นเมื่อมีการส่งคำขอเดียวกันเป็นครั้งที่สอง แคชจะถูกตรวจสอบก่อน หากแคชของคำขอนั้นใช้ได้และถูกต้อง แคชจะถูกส่งคืนเป็นการตอบกลับ จากนั้นแคชจะถูกตรวจสอบหาความเก่าและจะได้รับการอัปเดตหากพบว่าไม่มีการอัปเดต ความค้างของแคชถูกกำหนดโดย max-age อยู่ในส่วนหัว Cache-Control พร้อมกับ stale-while-revalidate

โฟลว์ชาร์ตที่ติดตามตรรกะที่เก่าในขณะตรวจสอบใหม่ มันเริ่มต้นด้วยการร้องขอ หากไม่ได้แคชไว้ หรือแคชไม่ถูกต้อง คำขอจะถูกส่ง ตอบกลับ และแคชจะได้รับการอัปเดต มิฉะนั้น การตอบสนองที่แคชจะถูกส่งกลับ หลังจากนั้นแคชจะถูกตรวจสอบความไม่เสถียร หากเป็นเวอร์ชันเก่า คำขอจะถูกส่งไปและแคชจะได้รับการอัปเดต

ซึ่งช่วยให้โหลดหน้าเว็บได้อย่างรวดเร็ว เนื่องจากเนื้อหาที่แคชไม่อยู่ในเส้นทางวิกฤติอีกต่อไป พวกเขาจะโหลดทันที นอกจากนี้ เนื่องจากนักพัฒนาควบคุมความถี่ในการใช้และอัปเดตแคช จึงสามารถป้องกันเบราว์เซอร์ไม่ให้แสดงข้อมูลที่ล้าสมัยมากเกินไปต่อผู้ใช้

ผู้อ่านอาจคิดว่าถ้าพวกเขาสามารถให้เซิร์ฟเวอร์ใช้ส่วนหัวบางอย่างในการตอบกลับและปล่อยให้เบราว์เซอร์ใช้จากที่นั่นแล้วจำเป็นต้องใช้ React และ Hooks สำหรับการแคชอย่างไร

ปรากฎว่าวิธีเซิร์ฟเวอร์และเบราว์เซอร์ทำงานได้ดีเมื่อเราต้องการแคชเนื้อหาแบบคงที่เท่านั้น แล้วการใช้ stale-while-revalidate สำหรับ API แบบไดนามิกล่ะ เป็นการยากที่จะคิดค่าที่ดีสำหรับ max-age และ stale-while-revalidate ในกรณีดังกล่าว บ่อยครั้ง การทำให้แคชใช้งานไม่ได้และดึงการตอบกลับใหม่ทุกครั้งที่ส่งคำขอจะเป็นตัวเลือกที่ดีที่สุด สิ่งนี้มีประสิทธิภาพหมายถึงไม่มีการแคชเลย แต่ด้วย React และ Hooks เราสามารถทำได้ดีกว่านี้

stale-while-revalidate สำหรับ API

เราสังเกตเห็นว่า HTTP's stale-while-revalidate ของ HTTP ทำงานได้ไม่ดีกับคำขอแบบไดนามิก เช่น การเรียก API

แม้ว่าเราจะใช้งานเสร็จแล้ว เบราว์เซอร์จะส่งคืนแคชหรือการตอบกลับใหม่ ไม่ใช่ทั้งสองอย่าง สิ่งนี้ไม่ได้ผลดีกับคำขอ API เนื่องจากเราต้องการการตอบกลับใหม่ทุกครั้งที่ส่งคำขอ อย่างไรก็ตาม การรอคำตอบใหม่จะทำให้การใช้งานแอปช้าลง

ดังนั้นสิ่งที่เราจะทำ?

เราใช้กลไกการแคชแบบกำหนดเอง ภายในนั้น เราหาวิธีส่งคืนทั้งแคชและการตอบสนองใหม่ ใน UI การตอบสนองที่แคชไว้จะถูกแทนที่ด้วยการตอบสนองใหม่เมื่อพร้อมใช้งาน นี่คือลักษณะของตรรกะ:

  1. เมื่อคำขอถูกส่งไปยังปลายทางของเซิร์ฟเวอร์ API เป็นครั้งแรก ให้แคชการตอบสนองแล้วส่งคืน
  2. ครั้งถัดไปที่เกิดคำขอ API เดียวกัน ให้ใช้การตอบกลับที่แคชทันที
  3. จากนั้นส่งคำขอแบบอะซิงโครนัสเพื่อดึงการตอบกลับใหม่ เมื่อการตอบสนองมาถึง เผยแพร่การเปลี่ยนแปลงแบบอะซิงโครนัสไปยัง UI และอัปเดตแคช

วิธีนี้ช่วยให้สามารถอัปเดต UI ได้ในทันที เนื่องจากคำขอ API ทุกรายการถูกแคชไว้ แต่ยังรวมถึงความถูกต้องในท้ายที่สุดใน UI เนื่องจากข้อมูลการตอบสนองใหม่จะแสดงขึ้นทันทีที่พร้อมใช้งาน

ในบทช่วยสอนนี้ เราจะเห็นวิธีการทีละขั้นตอนเกี่ยวกับวิธีการใช้งาน เราจะเรียกวิธีการนี้ว่าไม่ อัปเดตขณะรีเฟรช เนื่องจาก UI ได้รับการ รีเฟรช จริง ๆ เมื่อได้รับการตอบกลับใหม่

การเตรียมการ: API

ในการเริ่มต้นบทช่วยสอนนี้ ก่อนอื่นเราต้องมี API ที่เราดึงข้อมูล โชคดีที่มีบริการ API จำลองมากมาย สำหรับบทช่วยสอนนี้ เราจะใช้ reqres.in

ข้อมูลที่เราดึงมาคือรายชื่อผู้ใช้ที่มีพารามิเตอร์การสืบค้น page นี่คือลักษณะของการดึงรหัส:

 fetch("https://reqres.in/api/users?page=2") .then(res => res.json()) .then(json => { console.log(json); });

การรันโค้ดนี้ให้ผลลัพธ์ต่อไปนี้แก่เรา นี่คือเวอร์ชันที่ไม่ซ้ำ:

 { page: 2, per_page: 6, total: 12, total_pages: 2, data: [ { id: 7, email: "[email protected]", first_name: "Michael", last_name: "Lawson", avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/follettkyle/128.jpg" }, // 5 more items ] }

คุณจะเห็นได้ว่านี่เป็นเหมือน API จริง เรามีการแบ่งหน้าในการตอบกลับ พารามิเตอร์การสืบค้น page มีหน้าที่ในการเปลี่ยนหน้า และเรามีหน้าทั้งหมดสองหน้าในชุดข้อมูล

การใช้ API ในแอป React

มาดูกันว่าเราใช้ API ในแอป React อย่างไร เมื่อเรารู้วิธีทำแล้ว เราจะหาส่วนการแคช เราจะใช้คลาสเพื่อสร้างองค์ประกอบของเรา นี่คือรหัส:

 import React from "react"; import PropTypes from "prop-types"; export default class Component extends React.Component { state = { users: [] }; componentDidMount() { this.load(); } load() { fetch(`https://reqres.in/api/users?page=${this.props.page}`) .then(res => res.json()) .then(json => { this.setState({ users: json.data }); }); } componentDidUpdate(prevProps) { if (prevProps.page !== this.props.page) { this.load(); } } render() { const users = this.state.users.map(user => ( <p key={user.id}> <img src={user.avatar} alt={user.first_name} style={{ height: 24, width: 24 }} /> {user.first_name} {user.last_name} </p> )); return <div>{users}</div>; } } Component.propTypes = { page: PropTypes.number.isRequired };

สังเกตว่าเราได้รับค่า page ผ่าน props ซึ่งมักเกิดขึ้นในแอปพลิเคชันในโลกแห่งความเป็นจริง นอกจากนี้เรายังมีฟังก์ชัน componentDidUpdate ซึ่งจะดึงข้อมูล API ทุกครั้งที่มีการเปลี่ยนแปลง this.props.page

ณ จุดนี้ จะแสดงรายการผู้ใช้หกรายเนื่องจาก API ส่งคืนหกรายการต่อหน้า:

การแสดงตัวอย่างส่วนประกอบต้นแบบ React ของเรา: เส้นกึ่งกลาง 6 เส้น แต่ละเส้นมีรูปถ่ายทางด้านซ้ายของชื่อ

การเพิ่มแคชเก่าขณะรีเฟรช

หากเราต้องการเพิ่มการแคชที่เก่าขณะรีเฟรชลงในสิ่งนี้ เราจำเป็นต้องอัปเดตตรรกะของแอปเป็น:

  1. แคชการตอบกลับของคำขอโดยไม่ซ้ำกันหลังจากที่ถูกดึงข้อมูลเป็นครั้งแรก
  2. ส่งคืนการตอบสนองแคชทันทีหากพบแคชของคำขอ จากนั้นส่งคำขอและส่งคืนการตอบกลับใหม่แบบอะซิงโครนัส แคชการตอบสนองนี้ในครั้งต่อไปด้วย

เราสามารถทำได้โดยมีวัตถุ CACHE ส่วนกลางที่เก็บแคชไว้ไม่ซ้ำกัน เพื่อความเป็นเอกลักษณ์ เราสามารถใช้ค่า this.props.page เป็นคีย์ในวัตถุ CACHE ของเรา จากนั้นเราก็เขียนโค้ดอัลกอริธึมที่กล่าวถึงข้างต้น

 import apiFetch from "./apiFetch"; const CACHE = {}; export default class Component extends React.Component { state = { users: [] }; componentDidMount() { this.load(); } load() { if (CACHE[this.props.page] !== undefined) { this.setState({ users: CACHE[this.props.page] }); } apiFetch(`https://reqres.in/api/users?page=${this.props.page}`).then( json => { CACHE[this.props.page] = json.data; this.setState({ users: json.data }); } ); } componentDidUpdate(prevProps) { if (prevProps.page !== this.props.page) { this.load(); } } render() { // same render code as above } }

เนื่องจากแคชถูกส่งกลับทันทีที่พบ และเนื่องจากข้อมูลตอบกลับใหม่ถูกส่งกลับโดย setState เช่นกัน ซึ่งหมายความว่าเรามีการอัปเดต UI ที่ราบรื่นและไม่ต้องรอแอปอีกต่อไปตั้งแต่คำขอครั้งที่สองเป็นต้นไป นี่เป็นวิธีที่สมบูรณ์แบบและเป็นวิธีที่ไม่อัปเดตขณะรีเฟรชโดยสรุป

ผังงานที่ติดตามลอจิกที่ค้างขณะรีเฟรช มันเริ่มต้นด้วยการร้องขอ หากแคชไว้ setState() จะถูกเรียกพร้อมการตอบสนองที่แคชไว้ ไม่ว่าจะด้วยวิธีใด คำขอจะถูกส่ง แคชถูกตั้งค่า และ setState() ถูกเรียกด้วยการตอบกลับใหม่

ฟังก์ชัน apiFetch ในที่นี้ไม่มีอะไรเลยนอกจาก wrapper over fetch เพื่อให้เราเห็นประโยชน์ของการแคชในแบบเรียลไทม์ โดยเพิ่มผู้ใช้แบบสุ่มลงในรายชื่อ users ที่ส่งกลับโดยคำขอ API นอกจากนี้ยังเพิ่มการหน่วงเวลาแบบสุ่มเข้าไปด้วย:

 export default async function apiFetch(...args) { await delay(Math.ceil(400 + Math.random() * 300)); const res = await fetch(...args); const json = await res.json(); json.data.push(getFakeUser()); return json; } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }

getFakeUser() ที่นี่มีหน้าที่สร้างวัตถุผู้ใช้ปลอม

ด้วยการเปลี่ยนแปลงเหล่านี้ API ของเราจึงเป็นจริงมากกว่าเดิม

  1. มีความล่าช้าแบบสุ่มในการตอบสนอง
  2. ส่งคืนข้อมูลที่แตกต่างกันเล็กน้อยสำหรับคำขอเดียวกัน

จากสิ่งนี้ เมื่อเราเปลี่ยน page ส่งผ่านไปยัง Component จากองค์ประกอบหลักของเรา เราจะเห็นการทำงานของการแคช API ลองคลิกปุ่ม Toggle ทุกๆ สองสามวินาทีใน CodeSandbox นี้ แล้วคุณจะเห็นพฤติกรรมดังนี้:

ภาพเคลื่อนไหวที่แสดงหน้าสลับที่เปิดใช้งานการแคช รายละเอียดมีอธิบายไว้ในบทความ

ถ้าสังเกตดีๆ บางอย่างก็เกิดขึ้น

  1. เมื่อแอปเริ่มทำงานและอยู่ในสถานะเริ่มต้น เราจะเห็นรายชื่อผู้ใช้เจ็ดราย จดบันทึกผู้ใช้คนสุดท้ายในรายการ เนื่องจากเป็นผู้ใช้ที่จะถูกแก้ไขแบบสุ่มในครั้งต่อไปที่ส่งคำขอนี้
  2. เมื่อเราคลิก Toggle เป็นครั้งแรก จะรอสักครู่ (400-700ms) จากนั้นอัปเดตรายการไปยังหน้าถัดไป
  3. ตอนนี้เราอยู่ในหน้าที่สอง จดบันทึกผู้ใช้คนสุดท้ายในรายการอีกครั้ง
  4. ตอนนี้เราคลิกที่ Toggle อีกครั้ง และแอปจะกลับไปที่หน้าแรก โปรดสังเกตว่าตอนนี้รายการสุดท้ายยังคงเป็นผู้ใช้คนเดิมที่เราจดบันทึกไว้ในขั้นตอนที่ 1 จากนั้นจะเปลี่ยนเป็นผู้ใช้ใหม่ (แบบสุ่ม) ในภายหลัง นี่เป็นเพราะในตอนแรก แคชกำลังถูกแสดง จากนั้นการตอบสนองที่แท้จริงก็เริ่มขึ้น
  5. เราคลิกที่ Toggle อีกครั้ง ปรากฏการณ์เดียวกันก็เกิดขึ้น การตอบสนองที่แคชไว้จากครั้งที่แล้วถูกโหลดทันที จากนั้นจึงดึงข้อมูลใหม่ ดังนั้นเราจึงเห็นการอัปเดตรายการล่าสุดจากสิ่งที่เราจดบันทึกไว้ในขั้นตอนที่ 3

นี่คือแคชที่ค้างขณะรีเฟรชที่เรากำลังมองหา แต่วิธีการนี้ประสบปัญหาการซ้ำซ้อนของโค้ด มาดูกันว่ามันจะเป็นอย่างไรถ้าเรามีองค์ประกอบการดึงข้อมูลอื่นที่มีการแคช องค์ประกอบนี้แสดงรายการต่างจากองค์ประกอบแรกของเรา

การเพิ่ม Stale-while-refresh ไปยังส่วนประกอบอื่น

เราสามารถทำได้โดยเพียงแค่คัดลอกตรรกะจากองค์ประกอบแรก องค์ประกอบที่สองของเราแสดงรายการแมว:

 const CACHE = {}; export default class Component2 extends React.Component { state = { cats: [] }; componentDidMount() { this.load(); } load() { if (CACHE[this.props.page] !== undefined) { this.setState({ cats: CACHE[this.props.page] }); } apiFetch(`https://reqres.in/api/cats?page=${this.props.page}`).then( json => { CACHE[this.props.page] = json.data; this.setState({ cats: json.data }); } ); } componentDidUpdate(prevProps) { if (prevProps.page !== this.props.page) { this.load(); } } render() { const cats = this.state.cats.map(cat => ( <p key={cat.id} style={{ background: cat.color, padding: "4px", width: 240 }} > {cat.name} (born {cat.year}) </p> )); return <div>{cats}</div>; } }

อย่างที่คุณเห็น ตรรกะขององค์ประกอบที่เกี่ยวข้องที่นี่ค่อนข้างเหมือนกับองค์ประกอบแรก ข้อแตกต่างเพียงอย่างเดียวคือในปลายทางที่ร้องขอและแสดงรายการต่างกัน

ตอนนี้ เราแสดงส่วนประกอบทั้งสองนี้เคียงข้างกัน คุณจะเห็นว่าพวกมันมีพฤติกรรมคล้ายกัน:

แอนิเมชั่นที่แสดงการสลับองค์ประกอบสองส่วนที่อยู่เคียงข้างกัน

เพื่อให้ได้ผลลัพธ์นี้ เราต้องทำสำเนาโค้ดจำนวนมาก ถ้าเรามีหลายองค์ประกอบเช่นนี้ เราจะทำซ้ำโค้ดมากเกินไป

เพื่อแก้ปัญหาในลักษณะที่ไม่ซ้ำกัน เราสามารถมีส่วนประกอบที่มีลำดับสูงกว่าสำหรับการดึงและแคชข้อมูลและส่งต่อเป็นอุปกรณ์ประกอบฉาก มันไม่เหมาะ แต่จะใช้งานได้ แต่ถ้าเราต้องส่งคำขอหลายรายการในองค์ประกอบเดียว การมีส่วนประกอบที่มีลำดับสูงกว่าหลายรายการจะดูน่าเกลียดอย่างรวดเร็ว

จากนั้น เราก็มีรูปแบบการเรนเดอร์อุปกรณ์ประกอบฉาก ซึ่งน่าจะเป็นวิธีที่ดีที่สุดที่จะทำสิ่งนี้ในคอมโพเนนต์ของคลาส มันทำงานได้อย่างสมบูรณ์ แต่อีกครั้ง มันมักจะ "ห่อนรก" และต้องการให้เราผูกบริบทปัจจุบันในบางครั้ง นี่ไม่ใช่ประสบการณ์ที่ยอดเยี่ยมสำหรับนักพัฒนาและอาจนำไปสู่ความยุ่งยากและข้อบกพร่อง

นี่คือจุดที่ React Hooks ช่วยคุณได้ ซึ่งช่วยให้เราสามารถบรรจุกล่องตรรกะของส่วนประกอบในภาชนะที่นำกลับมาใช้ใหม่ได้ เพื่อให้เราสามารถใช้งานได้ในหลายที่ React Hooks ถูกนำมาใช้ใน React 16.8 และใช้งานได้กับส่วนประกอบฟังก์ชันเท่านั้น ก่อนที่เราจะไปที่การควบคุมแคชของ React โดยเฉพาะอย่างยิ่ง การแคชเนื้อหาด้วย Hooks มาดูวิธีที่เราทำการดึงข้อมูลอย่างง่ายในองค์ประกอบของฟังก์ชันก่อน

การดึงข้อมูล API ในส่วนประกอบฟังก์ชัน

ในการดึงข้อมูล API ในองค์ประกอบของฟังก์ชัน เราใช้ useState และ useEffect hooks

useState นั้นคล้ายคลึงกับ state และ setState ของคอมโพเนนต์คลาส เราใช้เบ็ดนี้เพื่อให้มีสถานะคอนเทนเนอร์อะตอมภายในองค์ประกอบฟังก์ชัน

useEffect เป็น lifecycle hook และคุณสามารถคิดว่ามันเป็นการรวมกันของ componentDidMount , componentDidUpdate และ componentWillUnmount พารามิเตอร์ที่สองที่ส่งผ่านไปยัง useEffect เรียกว่าอาร์เรย์การพึ่งพา เมื่ออาร์เรย์การขึ้นต่อกันเปลี่ยนแปลง การเรียกกลับที่ส่งผ่านเมื่ออาร์กิวเมนต์แรกสำหรับ useEffect ถูกเรียกใช้อีกครั้ง

เราจะใช้ hooks เหล่านี้ในการดึงข้อมูลได้อย่างไร:

 import React, { useState, useEffect } from "react"; export default function Component({ page }) { const [users, setUsers] = useState([]); useEffect(() => { fetch(`https://reqres.in/api/users?page=${page}`) .then(res => res.json()) .then(json => { setUsers(json.data); }); }, [page]); const usersDOM = users.map(user => ( <p key={user.id}> <img src={user.avatar} alt={user.first_name} style={{ height: 24, width: 24 }} /> {user.first_name} {user.last_name} </p> )); return <div>{usersDOM}</div>; }

โดยการระบุ page เป็นการพึ่งพา useEffect เราสั่งให้ React เรียกใช้ useEffect callback ทุกครั้งที่เปลี่ยน page มันเหมือนกับ componentDidUpdate นอกจากนี้ useEffect จะทำงานในครั้งแรกเสมอ ดังนั้นมันจึงทำงานเหมือน componentDidMount เช่นกัน

ค้างขณะรีเฟรชในคอมโพเนนต์ของฟังก์ชัน

เรารู้ว่า useEffect นั้นคล้ายกับวิธีวงจรชีวิตของส่วนประกอบ ดังนั้นเราจึงสามารถแก้ไขฟังก์ชันเรียกกลับที่ส่งผ่านเพื่อสร้างแคชที่ค้างในขณะรีเฟรชที่เรามีในคอมโพเนนต์ของคลาส ทุกอย่างยังคงเหมือนเดิม ยกเว้น useEffect hook

 const CACHE = {}; export default function Component({ page }) { const [users, setUsers] = useState([]); useEffect(() => { if (CACHE[page] !== undefined) { setUsers(CACHE[page]); } apiFetch(`https://reqres.in/api/users?page=${page}`).then(json => { CACHE[page] = json.data; setUsers(json.data); }); }, [page]); // ... create usersDOM from users return <div>{usersDOM}</div>; }

ดังนั้นเราจึงมีการแคชค้างขณะรีเฟรชทำงานในองค์ประกอบของฟังก์ชัน

เราสามารถทำเช่นเดียวกันสำหรับองค์ประกอบที่สอง นั่นคือ แปลงเป็นฟังก์ชันและใช้การแคชที่เก่าในขณะรีเฟรช ผลลัพธ์จะเหมือนกับที่เรามีในชั้นเรียน

แต่นั่นไม่ได้ดีไปกว่าส่วนประกอบของคลาสใช่ไหม มาดูกันว่าเราจะใช้พลังของ hook แบบกำหนดเองเพื่อสร้างตรรกะแบบโมดูลาร์ที่เก่า-ในขณะรีเฟรชที่เราสามารถใช้กับส่วนประกอบต่างๆ ได้อย่างไร

Hook ค้างขณะรีเฟรชแบบกำหนดเอง

ขั้นแรก ให้จำกัดตรรกะที่เราต้องการจะย้ายเข้าไปอยู่ใน hook แบบกำหนดเองให้แคบลง หากคุณดูโค้ดก่อนหน้านี้ คุณจะรู้ว่ามันคือส่วน useState และ useEffect โดยเฉพาะอย่างยิ่ง นี่คือตรรกะที่เราต้องการทำให้เป็นโมดูล

 const [users, setUsers] = useState([]); useEffect(() => { if (CACHE[page] !== undefined) { setUsers(CACHE[page]); } apiFetch(`https://reqres.in/api/users?page=${page}`).then(json => { CACHE[page] = json.data; setUsers(json.data); }); }, [page]);

เนื่องจากเราต้องทำให้เป็นแบบทั่วไป เราจึงต้องทำให้ URL เป็นไดนามิก ดังนั้นเราต้องมี url เป็นอาร์กิวเมนต์ เราจะต้องอัปเดตตรรกะการแคชด้วย เนื่องจากคำขอหลายรายการสามารถมีค่า page เดียวกันได้ โชคดีที่เมื่อรวม page ไว้กับ URL ปลายทาง ก็จะให้ค่าที่ไม่ซ้ำสำหรับคำขอที่ไม่ซ้ำทุกรายการ ดังนั้นเราจึงสามารถใช้ URL ทั้งหมดเป็นคีย์สำหรับการแคช:

 const [data, setData] = useState([]); useEffect(() => { if (CACHE[url] !== undefined) { setData(CACHE[url]); } apiFetch(url).then(json => { CACHE[url] = json.data; setData(json.data); }); }, [url]);

ประมาณนั้นครับ หลังจากห่อไว้ในฟังก์ชันแล้ว เราจะมี hook แบบกำหนดเอง ดูที่ด้านล่าง

 const CACHE = {}; export default function useStaleRefresh(url, defaultValue = []) { const [data, setData] = useState(defaultValue); useEffect(() => { // cacheID is how a cache is identified against a unique request const cacheID = url; // look in cache and set response if present if (CACHE[cacheID] !== undefined) { setData(CACHE[cacheID]); } // fetch new data apiFetch(url).then(newData => { CACHE[cacheID] = newData.data; setData(newData.data); }); }, [url]); return data; }

ขอให้สังเกตว่าเราได้เพิ่มอาร์กิวเมนต์อื่นที่เรียกว่า defaultValue เข้าไป ค่าเริ่มต้นของการเรียก API อาจแตกต่างกันหากคุณใช้ hook นี้ในหลายองค์ประกอบ นั่นเป็นเหตุผลที่เราได้ทำให้มันปรับแต่งได้

สามารถทำได้เช่นเดียวกันสำหรับคีย์ data ในออบเจ็กต์ newData หาก hook ที่กำหนดเองของคุณส่งคืนข้อมูลที่หลากหลาย คุณอาจต้องการเพียงแค่ส่งคืน newData ไม่ใช่ newData.data และจัดการการข้ามผ่านนั้นที่ฝั่งคอมโพเนนต์

ตอนนี้เรามี hook แบบกำหนดเองแล้ว ซึ่งทำหน้าที่จัดการแคชที่ค้างในขณะรีเฟรช ต่อไปนี้คือวิธีที่เราเสียบมันเข้ากับส่วนประกอบของเรา สังเกตจำนวนโค้ดที่เราสามารถลดได้ องค์ประกอบทั้งหมดของเราตอนนี้มีเพียงสามประโยคเท่านั้น นั่นเป็นชัยชนะครั้งใหญ่

 import useStaleRefresh from "./useStaleRefresh"; export default function Component({ page }) { const users = useStaleRefresh(`https://reqres.in/api/users?page=${page}`, []); const usersDOM = users.map(user => ( <p key={user.id}> <img src={user.avatar} alt={user.first_name} style={{ height: 24, width: 24 }} /> {user.first_name} {user.last_name} </p> )); return <div>{usersDOM}</div>; }

เราสามารถทำเช่นเดียวกันสำหรับองค์ประกอบที่สอง มันจะมีลักษณะดังนี้:

 export default function Component2({ page }) { const cats = useStaleRefresh(`https://reqres.in/api/cats?page=${page}`, []); // ... create catsDOM from cats return <div>{catsDOM}</div>; }

ง่ายที่จะดูว่าเราสามารถบันทึกโค้ดสำเร็จรูปได้มากเพียงใดหากเราใช้เบ็ดนี้ รหัสดูดีขึ้นเช่นกัน หากคุณต้องการดูการทำงานของแอปทั้งหมด ตรงไปที่ CodeSandbox นี้

การเพิ่มตัวระบุการโหลดเพื่อใช้ useStaleRefresh

ตอนนี้เรามีพื้นฐานที่ตรงประเด็นแล้ว เราสามารถเพิ่มคุณสมบัติเพิ่มเติมให้กับ hook แบบกำหนดเองของเราได้ ตัวอย่างเช่น เราสามารถเพิ่มค่า isLoading ใน hook ที่เป็นจริงเมื่อใดก็ตามที่มีการส่งคำขอที่ไม่ซ้ำ และเราไม่มีแคชที่จะแสดงในขณะเดียวกัน

เราทำสิ่งนี้โดยมีสถานะแยกต่างหากสำหรับ isLoading และตั้งค่าตามสถานะของ hook นั่นคือ เมื่อไม่มีเนื้อหาเว็บที่แคชไว้ เราจะตั้งค่าให้ true ไม่เช่นนั้นเราจะตั้งค่าเป็น false

นี่คือเบ็ดที่อัปเดต:

 export default function useStaleRefresh(url, defaultValue = []) { const [data, setData] = useState(defaultValue); const [isLoading, setLoading] = useState(true); useEffect(() => { // cacheID is how a cache is identified against a unique request const cacheID = url; // look in cache and set response if present if (CACHE[cacheID] !== undefined) { setData(CACHE[cacheID]); setLoading(false); } else { // else make sure loading set to true setLoading(true); } // fetch new data apiFetch(url).then(newData => { CACHE[cacheID] = newData.data; setData(newData.data); setLoading(false); }); }, [url]); return [data, isLoading]; }

ตอนนี้เราสามารถใช้ค่า isLoading ใหม่ในส่วนประกอบของเราได้แล้ว

 export default function Component({ page }) { const [users, isLoading] = useStaleRefresh( `https://reqres.in/api/users?page=${page}`, [] ); if (isLoading) { return <div>Loading</div>; } // ... create usersDOM from users return <div>{usersDOM}</div>; }

โปรดสังเกตว่าเมื่อทำเสร็จแล้ว คุณจะเห็นข้อความ "กำลังโหลด" เมื่อมีการส่งคำขอที่ไม่ซ้ำเป็นครั้งแรกและไม่มีแคชอยู่

แอนิเมชั่นที่แสดงส่วนประกอบด้วยตัวบ่งชี้การโหลดที่ใช้งาน

ทำให้ useStaleRefresh รองรับฟังก์ชั่น async ใด ๆ

เราสามารถทำให้ hook ที่กำหนดเองของเรามีประสิทธิภาพมากยิ่งขึ้นโดยทำให้สนับสนุนฟังก์ชัน async ใด ๆ มากกว่าแค่คำขอเครือข่าย GET แนวคิดพื้นฐานที่อยู่เบื้องหลังจะยังคงเหมือนเดิม

  1. ใน hook คุณเรียกใช้ฟังก์ชัน async ที่คืนค่าหลังจากผ่านไประยะหนึ่ง
  2. การเรียกใช้ฟังก์ชัน async ที่ไม่ซ้ำกันแต่ละครั้งจะถูกแคชไว้อย่างเหมาะสม

การต่อกันของ function.name และ arguments อย่างง่ายจะทำงานเป็นคีย์แคชสำหรับกรณีการใช้งานของเรา เมื่อใช้สิ่งนี้ ตะขอของเราจะมีลักษณะดังนี้:

 import { useState, useEffect, useRef } from "react"; import isEqual from "lodash/isEqual"; const CACHE = {}; export default function useStaleRefresh(fn, args, defaultValue = []) { const prevArgs = useRef(null); const [data, setData] = useState(defaultValue); const [isLoading, setLoading] = useState(true); useEffect(() => { // args is an object so deep compare to rule out false changes if (isEqual(args, prevArgs.current)) { return; } // cacheID is how a cache is identified against a unique request const cacheID = hashArgs(fn.name, ...args); // look in cache and set response if present if (CACHE[cacheID] !== undefined) { setData(CACHE[cacheID]); setLoading(false); } else { // else make sure loading set to true setLoading(true); } // fetch new data fn(...args).then(newData => { CACHE[cacheID] = newData; setData(newData); setLoading(false); }); }, [args, fn]); useEffect(() => { prevArgs.current = args; }); return [data, isLoading]; } function hashArgs(...args) { return args.reduce((acc, arg) => stringify(arg) + ":" + acc, ""); } function stringify(val) { return typeof val === "object" ? JSON.stringify(val) : String(val); }

อย่างที่คุณเห็น เรากำลังใช้การรวมกันของชื่อฟังก์ชันและอาร์กิวเมนต์ที่เป็นสตริงเพื่อระบุการเรียกใช้ฟังก์ชันโดยไม่ซ้ำกัน และแคชดังกล่าว วิธีนี้ใช้ได้กับแอปธรรมดาของเรา แต่อัลกอริธึมนี้มีแนวโน้มที่จะเกิดการชนกันและการเปรียบเทียบที่ช้า (ด้วยอาร์กิวเมนต์ที่ไม่สามารถซีเรียลไลซ์ได้ มันจะไม่ทำงานเลย) ดังนั้นสำหรับแอปที่ใช้งานจริง อัลกอริธึมการแฮชที่เหมาะสมจึงเหมาะสมกว่า

สิ่งที่ควรทราบอีกประการหนึ่งที่นี่คือการใช้ useRef useRef ใช้เพื่อคงข้อมูลไว้ตลอดวงจรชีวิตทั้งหมดขององค์ประกอบที่ล้อมรอบ เนื่องจาก args เป็นอาร์เรย์—ซึ่งเป็นอ็อบเจ็กต์ใน JavaScript— ทุกการแสดงผลซ้ำของส่วนประกอบโดยใช้ hook จะทำให้ตัวชี้อ้างอิง args เปลี่ยนไป แต่ args เป็นส่วนหนึ่งของรายการการพึ่งพาใน useEffect ครั้งแรกของเรา ดังนั้น การเปลี่ยน args สามารถทำให้ useEffect ของเราทำงานแม้ว่าจะไม่มีอะไรเปลี่ยนแปลงก็ตาม เพื่อตอบโต้ เราทำการเปรียบเทียบอย่างลึกซึ้งระหว่าง args เก่าและปัจจุบันโดยใช้ isEqual และอนุญาตให้ใช้การเรียกกลับ useEffect หาก args เปลี่ยนแปลงจริง

ตอนนี้เราสามารถใช้ useStaleRefresh hook ใหม่ได้ดังนี้ สังเกตการเปลี่ยนแปลงใน defaultValue ที่นี่ เนื่องจากเป็น hook ที่ใช้งานทั่วไป เราไม่ได้อาศัย hook ของเราในการส่งคืนคีย์ data ในวัตถุการตอบสนอง

 export default function Component({ page }) { const [users, isLoading] = useStaleRefresh( apiFetch, [`https://reqres.in/api/users?page=${page}`], { data: [] } ); if (isLoading) { return <div>Loading</div>; } const usersDOM = users.data.map(user => ( <p key={user.id}> <img src={user.avatar} alt={user.first_name} style={{ height: 24, width: 24 }} /> {user.first_name} {user.last_name} </p> )); return <div>{usersDOM}</div>; }

คุณสามารถค้นหาโค้ดทั้งหมดได้ใน CodeSandbox นี้

อย่าให้ผู้ใช้ต้องรอ: ใช้เนื้อหาแคชอย่างมีประสิทธิภาพด้วย Hooks ที่ค้างขณะรีเฟรชและตอบสนอง

useStaleRefresh hook ที่เราสร้างขึ้นในบทความนี้เป็นการพิสูจน์แนวคิดที่แสดงให้เห็นว่า React Hooks ทำอะไรได้บ้าง ลองเล่นกับรหัสและดูว่าคุณสามารถใส่ลงในแอปพลิเคชันของคุณได้หรือไม่

อีกวิธีหนึ่ง คุณยังสามารถลองใช้ไลบรารี่โอเพนซอร์สที่เก่าและไม่ได้รับการดูแลเป็นอย่างดี เช่น swr หรือ react-query ทั้งสองเป็นไลบรารีที่ทรงพลังและรองรับคุณสมบัติมากมายที่ช่วยในคำขอ API

React Hooks เป็นตัวเปลี่ยนเกม ช่วยให้เราสามารถแบ่งปันตรรกะขององค์ประกอบได้อย่างสวยงาม สิ่งนี้ไม่เคยเกิดขึ้นมาก่อน เนื่องจากสถานะของส่วนประกอบ เมธอดวงจรชีวิต และการเรนเดอร์ทั้งหมดถูกจัดแพ็กเกจเป็นเอนทิตีเดียว: คอมโพเนนต์ของคลาส ตอนนี้ เราสามารถมีโมดูลที่แตกต่างกันสำหรับโมดูลทั้งหมดได้ เหมาะอย่างยิ่งสำหรับความสามารถในการเขียนโค้ดและการเขียนโค้ดที่ดีขึ้น ฉันกำลังใช้ส่วนประกอบของฟังก์ชันและขอเกี่ยวสำหรับโค้ด React ใหม่ทั้งหมดที่ฉันเขียน และฉันขอแนะนำสิ่งนี้ให้กับนักพัฒนา React ทุกคน

ที่เกี่ยวข้อง: การสร้างแอป React ด้วย Redux Toolkit และ RTK Query