Tworzenie tabel, które można sortować w React

Opublikowany: 2022-03-10
Krótkie podsumowanie ↬ Możliwość sortowania tabel w React może wydawać się trudnym zadaniem, ale nie musi być zbyt trudne. W tym artykule zaimplementujemy wszystko, czego potrzebujesz, aby uporządkować wszystkie potrzeby związane z sortowaniem tabel.

Sortowanie tabel zawsze było dość trudnym problemem. Istnieje wiele interakcji do śledzenia, rozbudowane mutacje DOM, a nawet skomplikowane algorytmy sortowania. To tylko jedno z tych wyzwań, które trudno sprostać. Prawidłowy?

Zamiast ściągać zewnętrzne biblioteki, spróbujmy zrobić rzeczy sami. W tym artykule stworzymy sposób wielokrotnego użytku do sortowania danych tabelarycznych w React. Omówimy szczegółowo każdy krok i po drodze nauczymy się wielu przydatnych technik.

Nie będziemy przechodzić przez podstawową składnię Reacta lub JavaScript, ale nie musisz być ekspertem w React, aby podążać dalej.

Tworzenie tabeli za pomocą React

Najpierw utwórzmy przykładowy składnik tabeli. Przyjmie tablicę produktów i wygeneruje bardzo prostą tabelę, zawierającą wiersz na produkt.

 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> ); }
Więcej po skoku! Kontynuuj czytanie poniżej ↓

Tutaj akceptujemy szereg produktów i umieszczamy je w naszej tabeli. W tej chwili jest statyczny i nie można go sortować, ale na razie to w porządku.

Sortowanie danych

Jeśli uwierzysz wszystkim ankieterom, pomyślałbyś, że tworzenie oprogramowania to prawie wszystkie algorytmy sortujące. Na szczęście nie będziemy tutaj zajmować się sortowaniem szybkim ani bąbelkowym.

Sortowanie danych w JavaScript jest dość proste dzięki wbudowanej funkcji tablicowej sort() . Posortuje tablice liczb i ciągów bez dodatkowego argumentu:

 const array = ['mozzarella', 'gouda', 'cheddar']; array.sort(); console.log(array); // ['cheddar', 'gouda', 'mozzarella']

Jeśli chcesz czegoś bardziej sprytnego, możesz przekazać temu funkcję sortowania. Ta funkcja otrzymuje jako argumenty dwa elementy na liście i umieści jeden przed drugim w zależności od tego, co zdecydujesz.

Zacznijmy od posortowania otrzymanych danych alfabetycznie według nazwy.

 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> ); }

Więc co się tutaj dzieje? Najpierw tworzymy kopię rekwizytu produktów, którą możemy dowolnie zmieniać i zmieniać. Musimy to zrobić, ponieważ funkcja Array.prototype.sort zmienia oryginalną tablicę zamiast zwracać nową posortowaną kopię.

Następnie wywołujemy sortedProducts.sort i przekazujemy mu funkcję sorting . Sprawdzamy, czy właściwość name pierwszego argumentu a znajduje się przed drugim argumentem b , a jeśli tak, zwracamy wartość ujemną. Oznacza to, że a powinno znajdować się przed b na liście. Jeśli nazwa pierwszego argumentu występuje po nazwie drugiego argumentu, zwracamy liczbę dodatnią, wskazując, że powinniśmy umieścić b przed a . Jeśli oba są równe (tj. oba mają tę samą nazwę), zwracamy 0 , aby zachować kolejność.

Możliwość sortowania naszego stołu

Teraz możemy się upewnić, że tabela jest posortowana według nazwy — ale jak sami możemy zmienić kolejność sortowania?

Aby zmienić pole, według którego sortujemy, musimy zapamiętać aktualnie posortowane pole. Zrobimy to za pomocą haka useState .

Podpięcie to specjalny rodzaj funkcji, która pozwala nam „zahaczyć się” o niektóre z podstawowych funkcji Reacta, takie jak zarządzanie stanem i wywoływanie efektów ubocznych. Ten konkretny hook pozwala nam zachować część stanu wewnętrznego w naszym komponencie i zmienić go, jeśli chcemy. Oto co dodamy:

 const [sortedField, setSortedField] = React.useState(null);

Zaczynamy od tego, że w ogóle niczego nie sortujemy. Następnie zmieńmy nagłówki tabeli, aby uwzględnić sposób zmiany pola, według którego chcemy sortować.

 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> ); };

Teraz za każdym razem, gdy klikamy nagłówek tabeli, aktualizujemy pole, według którego chcemy sortować. Zgrabnie!

Jednak nie robimy jeszcze żadnego sortowania, więc naprawmy to. Pamiętasz algorytm sortowania sprzed? Oto ona, tylko nieznacznie zmieniona, aby działała z dowolną z naszych nazw pól.

 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>

Najpierw upewniamy się, że wybraliśmy pole do sortowania, a jeśli tak, sortujemy produkty według tego pola.

Rosnąco vs Malejąco

Następną funkcją, którą chcemy zobaczyć, jest sposób na przełączanie między kolejnością rosnącą i malejącą. Przełączamy się między kolejnością rosnącą i malejącą, klikając jeszcze raz nagłówek tabeli.

Aby to zaimplementować, musimy wprowadzić drugi element stanu — porządek sortowania. Zmodyfikujemy naszą bieżącą zmienną stanu sortedField , aby zachować zarówno nazwę pola, jak i jego kierunek. Zamiast zawierać ciąg, ta zmienna stanu będzie zawierać obiekt z kluczem (nazwą pola) i kierunkiem. Zmienimy jego nazwę na sortConfig , aby była nieco jaśniejsza.

Oto nowa funkcja sortowania:

 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; });

Teraz, jeśli kierunek jest „wznoszący”, zrobimy tak, jak poprzednio. Jeśli tak nie jest, zrobimy odwrotnie, dając nam kolejność malejącą.

Następnie utworzymy nową funkcję — requestSort — która zaakceptuje nazwę pola i odpowiednio zaktualizuje stan.

 const requestSort = key => { let direction = 'ascending'; if (sortConfig.key === key && sortConfig.direction === 'ascending') { direction = 'descending'; } setSortConfig({ key, direction }); }

Aby korzystać z tej nowej funkcji, będziemy musieli również zmienić nasze moduły obsługi kliknięć!

 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> );

Teraz zaczynamy wyglądać całkiem kompletnie, ale wciąż pozostaje jedna ważna rzecz do zrobienia. Musimy się upewnić, że sortujemy nasze dane tylko wtedy, gdy jest to konieczne. Obecnie sortujemy wszystkie nasze dane przy każdym renderowaniu, co prowadzi do różnego rodzaju problemów z wydajnością. Zamiast tego użyjmy wbudowanego haczyka useMemo , aby zapamiętać wszystkie wolne części!

 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]);

Jeśli nie widziałeś tego wcześniej, useMemo to sposób na buforowanie — lub zapamiętywanie — kosztownych obliczeń. Tak więc przy tych samych danych wejściowych nie trzeba dwukrotnie sortować produktów, jeśli z jakiegoś powodu ponownie wyrenderujemy nasz komponent. Pamiętaj, że chcemy uruchamiać nowe sortowanie za każdym razem, gdy zmieniają się nasze produkty lub pole lub kierunek sortujemy według zmian.

Zawijanie naszego kodu w tę funkcję będzie miało ogromny wpływ na wydajność naszego sortowania tabel!

Możliwość wielokrotnego użytku

Jedną z najlepszych rzeczy w hookach jest łatwość ponownego wykorzystania logiki. Prawdopodobnie będziesz sortować wszystkie typy tabel w całej aplikacji, a konieczność ponownego zaimplementowania tych samych rzeczy brzmi jak przeciąganie.

React ma tę funkcję zwaną niestandardowymi hookami. Brzmią fantazyjnie, ale wszystko to są zwykłe funkcje, które wykorzystują w sobie inne haki. Zrefaktoryzujmy nasz kod, aby był zawarty w niestandardowym haczyku, abyśmy mogli go używać w dowolnym miejscu!

 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 }; }

Jest to w zasadzie kopiowanie i wklejanie z naszego poprzedniego kodu, z niewielką zmianą nazwy. useSortableData akceptuje elementy i opcjonalny początkowy stan sortowania. Zwraca obiekt z posortowanymi elementami oraz funkcję ponownego sortowania elementów.

Nasz kod tabeli wygląda teraz tak:

 const ProductsTable = (props) => { const { products } = props; const { items, requestSort } = useSortableData(products); return ( <table>{/* ... */}</table> ); };

Ostatni dotyk

Brakuje jednego malutkiego elementu — sposobu na wskazanie sposobu sortowania tabeli. Aby wskazać, że w naszym projekcie musimy zwrócić również stan wewnętrzny — sortConfig . Zwróćmy to również i użyjmy go do wygenerowania stylów, które możemy zastosować do naszych nagłówków tabel!

 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> ); };

I z tym skończyliśmy!

Zawijanie

Jak się okazuje, stworzenie własnego algorytmu sortowania tabel nie było w końcu niemożliwym wyczynem. Znaleźliśmy sposób na modelowanie naszego stanu, napisaliśmy ogólną funkcję sortującą i napisaliśmy sposób aktualizowania naszych preferencji sortowania. Upewniliśmy się, że wszystko działa sprawnie i przekształciliśmy to wszystko w niestandardowy hak. Na koniec udostępniliśmy sposób na wskazanie użytkownikowi kolejności sortowania.

Możesz zobaczyć demo tabeli w tej CodeSandbox: