การสร้างตารางที่จัดเรียงได้ด้วย React
เผยแพร่แล้ว: 2022-03-10การจัดเรียงตารางเป็นปัญหาที่ยากมากที่จะทำให้ถูกต้อง มีการโต้ตอบมากมายให้ติดตาม การกลายพันธุ์ของ DOM ที่ต้องทำมากมาย และแม้แต่อัลกอริธึมการเรียงลำดับที่ซับซ้อนด้วย เป็นเพียงหนึ่งในความท้าทายที่ยากจะแก้ไข ใช่ไหม?
แทนที่จะดึงไลบรารีภายนอก มาลองทำสิ่งต่างๆ ด้วยตัวเอง ในบทความนี้ เราจะสร้างวิธีการจัดเรียงข้อมูลแบบตารางของคุณใน React แบบใช้ซ้ำได้ เราจะลงรายละเอียดแต่ละขั้นตอนอย่างละเอียด และเรียนรู้เทคนิคที่มีประโยชน์มากมายไปพร้อมกัน
เราจะไม่พูดถึงรูปแบบพื้นฐานของ React หรือ JavaScript แต่คุณไม่จำเป็นต้องเป็นผู้เชี่ยวชาญใน React เพื่อติดตาม
การสร้างตารางด้วย React
ขั้นแรก มาสร้างส่วนประกอบตารางตัวอย่างกัน จะยอมรับอาร์เรย์ของผลิตภัณฑ์และส่งออกตารางพื้นฐานโดยแสดงรายการแถวต่อผลิตภัณฑ์
function ProductTable(props) { const { products } = props; return ( <table> <caption>Our products</caption> <thead> <tr> <th>Name</th> <th>Price</th> <th>In Stock</th> </tr> </thead> <tbody> {products.map(product => ( <tr key={product.id}> <td>{product.name}</td> <td>{product.price}</td> <td>{product.stock}</td> </tr> ))} </tbody> </table> ); }
ที่นี่ เรายอมรับผลิตภัณฑ์หลายประเภทและรวมไว้ในตารางของเรา มันคงที่และไม่สามารถจัดเรียงได้ในขณะนี้ แต่ตอนนี้ก็ใช้ได้
การเรียงลำดับข้อมูล
หากคุณเชื่อว่าผู้สัมภาษณ์ไวท์บอร์ดทั้งหมด คุณคิดว่าการพัฒนาซอฟต์แวร์เป็นอัลกอริธึมการเรียงลำดับเกือบทั้งหมด โชคดีที่เราจะไม่ดูการเรียงลำดับอย่างรวดเร็วหรือการเรียงลำดับฟองที่นี่
การเรียงลำดับข้อมูลใน JavaScript ค่อนข้างตรงไปตรงมา ต้องขอบคุณฟังก์ชันอาร์เรย์ใน sort()
มันจะจัดเรียงอาร์เรย์ของตัวเลขและสตริงโดยไม่มีอาร์กิวเมนต์เพิ่มเติม:
const array = ['mozzarella', 'gouda', 'cheddar']; array.sort(); console.log(array); // ['cheddar', 'gouda', 'mozzarella']
หากคุณต้องการสิ่งที่ฉลาดกว่านี้อีกเล็กน้อย คุณสามารถส่งต่อฟังก์ชันการจัดเรียงได้ ฟังก์ชันนี้มีให้สองรายการในรายการเป็นอาร์กิวเมนต์ และจะวางรายการหนึ่งไว้ข้างหน้าอีกรายการตามสิ่งที่คุณตัดสินใจ
เริ่มต้นด้วยการเรียงลำดับข้อมูลที่เราได้รับตามตัวอักษรตามชื่อ
function ProductTable(props) { const { products } = props; let sortedProducts = [...products]; sortedProducts.sort((a, b) => { if (a.name < b.name) { return -1; } if (a.name > b.name) { return 1; } return 0; }); return ( <Table> {/* as before */} </Table> ); }
เกิดอะไรขึ้นที่นี่? ขั้นแรก เราสร้างสำเนาของพร็อพผลิตภัณฑ์ ซึ่งเราสามารถเปลี่ยนแปลงและเปลี่ยนแปลงได้ตามต้องการ เราจำเป็นต้องทำเช่นนี้เพราะฟังก์ชัน Array.prototype.sort
จะเปลี่ยนอาร์เรย์เดิมแทนที่จะส่งคืนสำเนาที่จัดเรียงใหม่
ต่อไป เราเรียก sortedProducts.sort
และส่งต่อฟังก์ชัน sorting
เราตรวจสอบว่าคุณสมบัติของ name
ของอาร์กิวเมนต์แรก a
อยู่ก่อนอาร์กิวเมนต์ที่สอง b
หรือไม่ และถ้าเป็นเช่นนั้น ให้คืนค่าเป็นลบ สิ่งนี้บ่งชี้ว่า a
ควรมาก่อน b
ในรายการ ถ้าชื่ออาร์กิวเมนต์แรกอยู่หลังชื่ออาร์กิวเมนต์ที่สอง เราจะคืนค่าเป็นจำนวนบวก ซึ่งบ่งชี้ว่าเราควรวาง b
ก่อน a
หากทั้งสองมีค่าเท่ากัน (กล่าวคือ ทั้งคู่มีชื่อเหมือนกัน) เราจะคืนค่า 0
เพื่อรักษาลำดับ
ทำให้ตารางของเราจัดเรียงได้
ตอนนี้เราสามารถตรวจสอบให้แน่ใจว่าตารางถูกจัดเรียงตามชื่อแล้ว — แต่เราจะเปลี่ยนลำดับการจัดเรียงด้วยตัวเองได้อย่างไร?
หากต้องการเปลี่ยนฟิลด์ที่เราจัดเรียง เราต้องจำฟิลด์ที่จัดเรียงอยู่ในปัจจุบัน เราจะทำอย่างนั้นด้วย useState
hook
hook เป็นฟังก์ชันพิเศษที่ช่วยให้เรา "เชื่อมต่อ" กับฟังก์ชันหลักบางอย่างของ React เช่น การจัดการสถานะและทริกเกอร์ผลข้างเคียง ตะขอพิเศษนี้ช่วยให้เรารักษาสถานะภายในในส่วนประกอบของเรา และเปลี่ยนแปลงได้หากต้องการ นี่คือสิ่งที่เราจะเพิ่ม:
const [sortedField, setSortedField] = React.useState(null);
เราเริ่มต้นด้วยการไม่เรียงลำดับอะไรเลย ต่อไป ให้เปลี่ยนส่วนหัวของตารางเพื่อรวมวิธีการเปลี่ยนเขตข้อมูลที่เราต้องการจัดเรียง
const ProductsTable = (props) => { const { products } = props; const [sortedField, setSortedField] = React.useState(null); return ( <table> <thead> <tr> <th> <button type="button" onClick={() => setSortedField('name')}> Name </button> </th> <th> <button type="button" onClick={() => setSortedField('price')}> Price </button> </th> <th> <button type="button" onClick={() => setSortedField('stock')}> In Stock </button> </th> </tr> </thead> {/* As before */} </table> ); };
ตอนนี้ เมื่อใดก็ตามที่เราคลิกส่วนหัวของตาราง เราจะอัปเดตฟิลด์ที่เราต้องการจัดเรียงตาม เรียบร้อย-โอ้!
เรายังไม่ได้ทำการจัดเรียงจริง ๆ ดังนั้นเรามาแก้ไขกัน จำอัลกอริธึมการเรียงลำดับจากเมื่อก่อนได้หรือไม่? นี่คือการเปลี่ยนแปลงเพียงเล็กน้อยเพื่อให้ทำงานกับชื่อฟิลด์ของเรา
const ProductsTable = (props) => { const { products } = props; const [sortedField, setSortedField] = React.useState(null); let sortedProducts = [...products]; if (sortedField !== null) { sortedProducts.sort((a, b) => { if (a[sortedField] < b[sortedField]) { return -1; } if (a[sortedField] > b[sortedField]) { return 1; } return 0; }); } return ( <table>
ก่อนอื่นเราต้องตรวจสอบให้แน่ใจว่าเราได้เลือกฟิลด์ที่จะเรียงลำดับตาม และถ้าเป็นเช่นนั้น เราจะเรียงลำดับผลิตภัณฑ์ตามฟิลด์นั้น
จากน้อยไปมาก vs จากมากไปน้อย
คุณลักษณะต่อไปที่เราต้องการเห็นคือวิธีการสลับระหว่างลำดับจากน้อยไปมากและจากมากไปน้อย เราจะสลับระหว่างลำดับจากน้อยไปมากและจากมากไปน้อยโดยคลิกที่ส่วนหัวของตารางอีกครั้ง
ในการดำเนินการนี้ เราจำเป็นต้องแนะนำส่วนที่สอง — ลำดับการจัดเรียง เราจะปรับโครงสร้างตัวแปรสถานะ sortedField
ปัจจุบันของเราเพื่อให้ทั้งชื่อฟิลด์และทิศทาง แทนที่จะมีสตริง ตัวแปรสถานะนี้จะมีอ็อบเจ็กต์ที่มีคีย์ (ชื่อฟิลด์) และทิศทาง เราจะเปลี่ยนชื่อเป็น sortConfig
ให้ชัดเจนขึ้นเล็กน้อย
นี่คือฟังก์ชันการเรียงลำดับใหม่:
sortedProducts.sort((a, b) => { if (a[sortConfig.key] < b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? -1 : 1; } if (a[sortConfig.key] > b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? 1 : -1; } return 0; });
ทีนี้ ถ้าทิศทางคือ 'จากน้อยไปมาก' เราจะทำเหมือนที่เคยทำมา ถ้าไม่ใช่ เราจะทำตรงกันข้าม ให้เราเรียงลำดับจากมากไปน้อย
ต่อไป เราจะสร้างฟังก์ชันใหม่ — requestSort
— ซึ่งจะยอมรับชื่อฟิลด์และอัปเดตสถานะตามลำดับ
const requestSort = key => { let direction = 'ascending'; if (sortConfig.key === key && sortConfig.direction === 'ascending') { direction = 'descending'; } setSortConfig({ key, direction }); }
นอกจากนี้เรายังต้องเปลี่ยนตัวจัดการการคลิกเพื่อใช้ฟังก์ชันใหม่นี้
return ( <table> <thead> <tr> <th> <button type="button" onClick={() => requestSort('name')}> Name </button> </th> <th> <button type="button" onClick={() => requestSort('price')}> Price </button> </th> <th> <button type="button" onClick={() => requestSort('stock')}> In Stock </button> </th> </tr> </thead> {/* as before */} </table> );
ตอนนี้เราเริ่มที่จะดูมีฟีเจอร์ที่สมบูรณ์แล้ว แต่ยังมีเรื่องใหญ่ที่ต้องทำอีกเรื่องหนึ่ง เราจำเป็นต้องตรวจสอบให้แน่ใจว่าเราจัดเรียงข้อมูลของเราเมื่อจำเป็นเท่านั้น ขณะนี้ เรากำลังจัดเรียงข้อมูลทั้งหมดของเราในทุกการแสดงผล ซึ่งจะนำไปสู่ปัญหาด้านประสิทธิภาพทุกประเภท ให้ใช้เบ็ด useMemo
ในตัวเพื่อบันทึกส่วนที่ช้าทั้งหมดแทน!
const ProductsTable = (props) => { const { products } = props; const [sortConfig, setSortConfig] = React.useState(null); React.useMemo(() => { let sortedProducts = [...products]; if (sortedField !== null) { sortedProducts.sort((a, b) => { if (a[sortConfig.key] < b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? -1 : 1; } if (a[sortConfig.key] > b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? 1 : -1; } return 0; }); } return sortedProducts; }, [products, sortConfig]);
หากคุณไม่เคยเห็นมาก่อน useMemo
เป็นวิธีแคชหรือบันทึก - การคำนวณที่มีราคาแพง ด้วยอินพุตเดียวกัน จึงไม่ต้องจัดเรียงผลิตภัณฑ์สองครั้ง หากเราแสดงส่วนประกอบของเราใหม่ด้วยเหตุผลบางประการ โปรดทราบว่าเราต้องการทริกเกอร์การจัดเรียงใหม่ทุกครั้งที่ผลิตภัณฑ์ของเราเปลี่ยนแปลง หรือฟิลด์หรือทิศทางที่เราจัดเรียงตามการเปลี่ยนแปลง
การห่อโค้ดของเราในฟังก์ชันนี้จะมีผลอย่างมากต่อการจัดเรียงตารางของเรา!
ทำให้ทุกอย่างนำกลับมาใช้ใหม่ได้
สิ่งที่ดีที่สุดอย่างหนึ่งเกี่ยวกับขอเกี่ยวคือความง่ายในการทำให้ตรรกะนำกลับมาใช้ใหม่ได้ คุณอาจจะจัดเรียงตารางทุกประเภทในแอปพลิเคชันของคุณ และต้องใช้งานสิ่งเดิมซ้ำๆ อีกครั้ง ดูเหมือนเป็นการลาก
React มีคุณสมบัตินี้เรียกว่า hooks แบบกำหนดเอง ฟังดูหรูหรา แต่ทั้งหมดนั้นเป็นฟังก์ชันปกติที่ใช้ตะขออื่นๆ ในตัว มาปรับโครงสร้างโค้ดของเราใหม่เพื่อให้อยู่ใน hook ที่กำหนดเองกัน เพื่อให้เราสามารถใช้งานได้ทุกที่!
const useSortableData = (items, config = null) => { const [sortConfig, setSortConfig] = React.useState(config); const sortedItems = React.useMemo(() => { let sortableItems = [...items]; if (sortConfig !== null) { sortableItems.sort((a, b) => { if (a[sortConfig.key] < b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? -1 : 1; } if (a[sortConfig.key] > b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? 1 : -1; } return 0; }); } return sortableItems; }, [items, sortConfig]); const requestSort = key => { let direction = 'ascending'; if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') { direction = 'descending'; } setSortConfig({ key, direction }); } return { items: sortedItems, requestSort }; }
นี่เป็นการคัดลอกและวางจากโค้ดก่อนหน้าของเรา โดยมีการเปลี่ยนชื่อเล็กน้อย useSortableData
ยอมรับรายการ และสถานะการจัดเรียงเริ่มต้นที่เป็นทางเลือก ส่งกลับวัตถุที่มีรายการที่เรียงลำดับ และฟังก์ชันเพื่อเรียงลำดับรายการใหม่
รหัสตารางของเราตอนนี้มีลักษณะดังนี้:
const ProductsTable = (props) => { const { products } = props; const { items, requestSort } = useSortableData(products); return ( <table>{/* ... */}</table> ); };
สัมผัสสุดท้าย
มีชิ้นส่วนเล็กๆ หายไปหนึ่งชิ้น — วิธีระบุวิธีการจัดเรียงตาราง เพื่อระบุว่าในการออกแบบของเรา เราต้องส่งคืนสถานะภายในด้วย — sortConfig
กลับมาใช้กันอีกครั้ง และใช้เพื่อสร้างสไตล์ที่เรานำไปใช้กับส่วนหัวของตารางได้!
const ProductTable = (props) => { const { items, requestSort, sortConfig } = useSortableData(props.products); const getClassNamesFor = (name) => { if (!sortConfig) { return; } return sortConfig.key === name ? sortConfig.direction : undefined; }; return ( <table> <caption>Products</caption> <thead> <tr> <th> <button type="button" onClick={() => requestSort('name')} className={getClassNamesFor('name')} > Name </button> </th> {/* … */} </tr> </thead> {/* … */} </table> ); };
เท่านี้เราก็เสร็จแล้ว!
ห่อ
ปรากฏว่าการสร้างอัลกอริธึมการเรียงลำดับตารางของคุณเองไม่ใช่สิ่งที่เป็นไปไม่ได้เลย เราพบวิธีสร้างแบบจำลองสถานะของเรา เราเขียนฟังก์ชันการจัดเรียงทั่วไป และเราเขียนวิธีอัปเดตการตั้งค่าการจัดเรียงของเรา เราทำให้แน่ใจว่าทุกอย่างมีประสิทธิภาพและปรับโครงสร้างใหม่ทั้งหมดให้เป็นตะขอแบบกำหนดเอง สุดท้าย เราได้จัดเตรียมวิธีการระบุลำดับการจัดเรียงให้กับผู้ใช้
คุณสามารถดูการสาธิตของตารางใน CodeSandbox นี้: