วิธีสร้าง React Hook แบบกำหนดเองเพื่อดึงและแคชข้อมูล

เผยแพร่แล้ว: 2022-03-10
สรุปอย่างรวดเร็ว ↬ มีความเป็นไปได้สูงที่ส่วนประกอบจำนวนมากในแอปพลิเคชัน React ของคุณจะต้องทำการเรียกไปยัง API เพื่อดึงข้อมูลที่จะแสดงต่อผู้ใช้ของคุณ เป็นไปได้ที่จะทำเช่นนั้นโดยใช้วิธีวงจรชีวิต componentDidMount() แต่ด้วยการแนะนำ Hooks คุณสามารถสร้าง hook แบบกำหนดเองที่จะดึงและแคชข้อมูลสำหรับคุณ นั่นคือสิ่งที่กวดวิชานี้จะครอบคลุม

หากคุณเป็นมือใหม่ใน React Hooks คุณสามารถเริ่มต้นด้วยการตรวจสอบเอกสารอย่างเป็นทางการเพื่อทำความเข้าใจ หลังจากนั้น ฉันขอแนะนำให้อ่าน "การเริ่มต้นใช้งาน React Hooks API" ของ Shedrack Akintayo เพื่อให้แน่ใจว่าคุณกำลังติดตาม มีบทความที่เขียนโดย Adeneye David Abiodun ซึ่งครอบคลุมแนวทางปฏิบัติที่ดีที่สุดเกี่ยวกับ React Hooks ซึ่งฉันแน่ใจว่าจะเป็นประโยชน์กับคุณ

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

แนวทางปฏิบัติที่ดีที่สุด

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

การดึงข้อมูลในส่วนประกอบที่ทำปฏิกิริยา

ก่อน React hooks เป็นเรื่องปกติที่จะดึงข้อมูลเริ่มต้นในวิธีวงจรชีวิต componentDidMount() และข้อมูลตามการเปลี่ยนแปลงของ prop หรือ state ในวิธี componentDidUpdate() lifecycle

นี่คือวิธีการทำงาน:

 componentDidMount() { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=JavaScript` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } componentDidUpdate(previousProps, previousState) { if (previousState.query !== this.state.query) { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${this.state.query}` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } }

วิธีการวงจรชีวิต componentDidMount จะถูกเรียกใช้ทันทีที่ส่วนประกอบได้รับการติดตั้ง และเมื่อทำเสร็จแล้ว สิ่งที่เราทำคือการส่งคำขอเพื่อค้นหา “JavaScript” ผ่าน Hacker News API และอัปเดตสถานะตามการตอบสนอง

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

เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

การดึงข้อมูลด้วย useEffect Hook

ตะขอ useEffect จะถูกเรียกใช้ทันทีที่ติดตั้งส่วนประกอบ หากเราต้องการให้ hook รันใหม่ตามการเปลี่ยนแปลงของ prop หรือ state เราจะต้องส่งผ่านมันไปยังอาร์เรย์การพึ่งพา (ซึ่งเป็นอาร์กิวเมนต์ที่สองของ useEffect hook)

มาสำรวจวิธีการดึงข้อมูลด้วย hooks:

 import { useState, useEffect } from 'react'; const [status, setStatus] = useState('idle'); const [query, setQuery] = useState(''); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]);

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

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

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

การสร้างตะขอแบบกำหนดเอง

“ฮุกที่กำหนดเองคือฟังก์ชัน JavaScript ที่มีชื่อขึ้นต้นด้วย 'use' และอาจเรียก Hooks อื่น ๆ ได้”

— ตอบโต้เอกสาร

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

คำจำกัดความจาก React Docs ได้มอบให้ไปแล้ว แต่มาดูกันว่ามันทำงานอย่างไรกับ hook แบบกำหนดเองที่เคาน์เตอร์:

 const useCounter = (initialState = 0) => { const [count, setCount] = useState(initialState); const add = () => setCount(count + 1); const subtract = () => setCount(count - 1); return { count, add, subtract }; };

ในที่นี้ เรามีฟังก์ชันปกติที่เราใช้อาร์กิวเมนต์ที่เป็นทางเลือก ตั้งค่าเป็นสถานะของเรา ตลอดจนเพิ่มวิธีการ add และการ subtract ที่สามารถใช้อัปเดตได้

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

นี่คือวิธีการทำงานในทางปฏิบัติ:

 import { useCounter } from './customHookPath'; const { count, add, subtract } = useCounter(100); eventHandler(() => { add(); // or subtract(); });

สิ่งที่เราทำที่นี่คือการนำเข้า hook แบบกำหนดเองของเราจากไฟล์ที่เราประกาศไว้ เพื่อให้เราสามารถใช้ประโยชน์จากมันในแอพของเรา เราตั้งค่าสถานะเริ่มต้นเป็น 100 ดังนั้นเมื่อใดก็ตามที่เราเรียก add() มันจะเพิ่ม count ขึ้น 1 และเมื่อใดก็ตามที่เราเรียก subtract() มันจะลดลง count 1

การสร้าง useFetch Hook

ตอนนี้เราได้เรียนรู้วิธีสร้าง hook แบบกำหนดเองอย่างง่ายแล้ว มาแยกตรรกะของเราเพื่อดึงข้อมูลไปยัง hook ที่กำหนดเองกัน

 const useFetch = (query) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]); return { status, data }; };

เกือบจะเป็นสิ่งเดียวกับที่เราทำข้างต้น ยกเว้นว่าเป็นฟังก์ชันที่ใช้ใน query และส่งคืน status และ data และนั่นคือ useFetch hook ที่เราสามารถใช้ได้ในหลายองค์ประกอบในแอปพลิเคชัน React ของเรา

ใช้งานได้ แต่ปัญหาของการใช้งานตอนนี้คือ เป็นเฉพาะสำหรับ Hacker News ดังนั้นเราอาจเรียกมันว่า useHackerNews สิ่งที่เราตั้งใจจะทำคือสร้าง useFetch hook ที่สามารถใช้เพื่อเรียก URL ใดก็ได้ มาปรับปรุงใหม่เพื่อใช้ใน URL แทน!

 const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch(url); const data = await response.json(); setData(data); setStatus('fetched'); }; fetchData(); }, [url]); return { status, data }; };

ตอนนี้ useFetch hook ของเราเป็นแบบทั่วไปและเราสามารถใช้งานได้ตามที่เราต้องการในส่วนประกอบต่างๆ ของเรา

นี่เป็นวิธีหนึ่งในการบริโภค:

 const [query, setQuery] = useState(''); const url = query && `https://hn.algolia.com/api/v1/search?query=${query}`; const { status, data } = useFetch(url);

ในกรณีนี้ หากค่าของ query คือ truthy เราตั้งค่า URL ต่อไป และหากไม่เป็นเช่นนั้น เราก็สามารถส่งต่อ undefined เนื่องจากจะได้รับการจัดการใน hook ของเรา เอฟเฟกต์จะพยายามเรียกใช้หนึ่งครั้งโดยไม่คำนึงถึง

บันทึกข้อมูลที่ดึงมา

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

หมายเหตุ : สำหรับบริบทเพิ่มเติม คุณสามารถดูคำอธิบายของ Wikipedia เกี่ยวกับ Memoization

มาดูกันว่าเราจะทำได้อย่างไร!

 const cache = {}; const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache[url]) { const data = cache[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

ที่นี่ เรากำลังจับคู่ URL กับข้อมูลของพวกเขา ดังนั้น หากเราขอดึงข้อมูลที่มีอยู่ เราจะตั้งค่าข้อมูลจากแคชในเครื่องของเรา ไม่เช่นนั้น เราจะดำเนินการตามคำขอและตั้งค่าผลลัพธ์ในแคช สิ่งนี้ทำให้แน่ใจได้ว่าเราจะไม่ทำการเรียก API เมื่อเรามีข้อมูลอยู่ในเครื่อง นอกจากนี้ เราจะสังเกตเห็นว่าเรากำลังกำจัดผลกระทบหาก URL เป็น falsy ดังนั้นจึงทำให้แน่ใจว่าเราจะไม่ดึงข้อมูลที่ไม่มีอยู่ต่อไป เราไม่สามารถทำได้ก่อน useEffect hook เนื่องจากมันจะขัดกับกฎของ hooks อย่างใดอย่างหนึ่ง ซึ่งก็คือการเรียก hooks ที่ระดับบนสุดเสมอ

การประกาศ cache ในขอบเขตที่ต่างกันนั้นใช้งานได้ แต่มันทำให้ฮุคของเราขัดกับหลักการของฟังก์ชันบริสุทธิ์ นอกจากนี้ เราต้องการให้แน่ใจว่า React ช่วยในการทำความสะอาดเมื่อเราไม่ต้องการใช้ส่วนประกอบอีกต่อไป เราจะสำรวจ useRef เพื่อช่วยเราในการบรรลุเป้าหมายนั้น

การบันทึกข้อมูลด้วย useRef

useRef เป็นเหมือนกล่องที่สามารถเก็บค่าที่ไม่แน่นอนใน .current property .current ของมัน”

— ตอบโต้เอกสาร

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

มาแทนที่การใช้แคชของเราด้วยเวทย์มนตร์ useRef !

 const useFetch = (url) => { const cache = useRef({}); const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache.current[url]) { const data = cache.current[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

ที่นี่แคชของเราอยู่ใน useFetch hook โดยมีอ็อบเจกต์ว่างเป็นค่าเริ่มต้น

ห่อ

ฉันได้ระบุว่าการตั้งค่าข้อมูลก่อนที่จะตั้งค่าสถานะการดึงข้อมูลเป็นความคิดที่ดี แต่มีปัญหาที่อาจเกิดขึ้นสองประการที่เราอาจมีด้วย:

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

มาทำความสะอาดขั้นสุดท้ายกับ useFetch hook ของเรา เราจะเริ่มต้นด้วยการเปลี่ยน useState ของเราเป็น useReducer มาดูกันว่ามันทำงานอย่างไร!

 const initialState = { status: 'idle', error: null, data: [], }; const [state, dispatch] = useReducer((state, action) => { switch (action.type) { case 'FETCHING': return { ...initialState, status: 'fetching' }; case 'FETCHED': return { ...initialState, status: 'fetched', data: action.payload }; case 'FETCH_ERROR': return { ...initialState, status: 'error', error: action.payload }; default: return state; } }, initialState);

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

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

เหลืออีกเพียงสิ่งเดียวเท่านั้น: การล้างผลข้างเคียงของเรา การดึงข้อมูลใช้ Promise API ในแง่ที่สามารถแก้ไขได้หรือปฏิเสธ หาก hook ของเราพยายามทำการอัปเดตในขณะที่ส่วนประกอบไม่ได้ต่อเชื่อมเนื่องจาก Promise บางอย่างเพิ่งได้รับการแก้ไข React จะส่งคืน Can't perform a React state update on an unmounted component.

มาดูกันว่าเราสามารถแก้ไขได้ด้วย useEffect clean-up!

 useEffect(() => { let cancelRequest = false; if (!url) return; const fetchData = async () => { dispatch({ type: 'FETCHING' }); if (cache.current[url]) { const data = cache.current[url]; dispatch({ type: 'FETCHED', payload: data }); } else { try { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; if (cancelRequest) return; dispatch({ type: 'FETCHED', payload: data }); } catch (error) { if (cancelRequest) return; dispatch({ type: 'FETCH_ERROR', payload: error.message }); } } }; fetchData(); return function cleanup() { cancelRequest = true; }; }, [url]);

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

บทสรุป

เราได้สำรวจแนวคิดของ hooks หลายอย่างเพื่อช่วยดึงและแคชข้อมูลในส่วนประกอบของเรา เรายังดำเนินการทำความสะอาด useEffect hook ซึ่งช่วยป้องกันปัญหาจำนวนมากในแอปของเรา

หากคุณมีคำถามใด ๆ โปรดทิ้งคำถามไว้ในส่วนความคิดเห็นด้านล่าง!

  • ดู repo สำหรับบทความนี้ →

อ้างอิง

  • “แนะนำ Hooks” React Docs
  • “การเริ่มต้นใช้งาน React Hooks API” Shedrack Akintayo
  • “แนวทางปฏิบัติที่ดีที่สุดด้วย React Hooks” Adeneye David Abiodun
  • “การเขียนโปรแกรมเชิงฟังก์ชัน: ฟังก์ชั่นล้วนๆ” Arne Brasseur