React.js w przykładach: filtrowanie statycznej listy

Ten wpis jest 11 częścią z 16 w kursie React.js

W jednym z komentarzy ktoś zasugerował mi, abym pokazywał jak najwięcej praktycznych przykładów. Inna osoba pytała konkretnie o przykład filtrowania listy na podstawie tekstu wpisywanego w input. Stwierdziłem, że warto skorzystać z tych sugestii 🙂 Oto powstaje seria wpisów, które będą się przeplatały z kursem Reacta jako takim. Tutaj będę pokazywał konkretne przykłady i implementacje, bez tłumaczenia teorii. Pierwszym przykładem będzie właśnie taka lista — na razie wersja prosta, ze statycznymi danymi i synchronicznym wyszukiwaniem. Do dzieła!

Plan działania

Chcesz stworzyć listę (np. kontaktów) i wyrenderować ją. Łatwizna. Do tego potrzebujemy input, który będzie wyszukiwarką. Wpisanie czegoś w input ma powodować filtrowanie listy. Dodatkowo jeśli nic nie znaleziono — ma wyświetlić się komunikat.

Myślę, że ten przykład w całości zamknie się w jednym/dwóch komponentach.

HTML

Najpierw sam HTML:

<input type="search">
<ul>
  <li>Michał</li>
  <li>Ania</li>
  <li>Kasia</li>
  <li>Tomek</li>
</ul>

No łatwiej chyba się nie da 😉

React.js

Lista użytkowników

Tworzę dwa komponenty: App oraz UsersList. UsersList ma być typowym komponentem „głupim” — tzn. jego renderowanie zależy tylko od przekazanych propsów. Przekażę tam tablicę z listą kontaktów już po przefiltrowaniu, którą zmapuję na listę elementów:

const UsersList = ({ users }) => {
  return (
    <ul>
      {users.map(user => <li key={user}>{user}</li>)}
    </ul>
  );
};

Pamiętaj o dodaniu unikalnego atrybutu key do każdego elementu zawsze gdy renderowana jest tablica!

A co gdy brak wyników?

Dodaję jeden warunek i renderuję co innego. A więc ostatecznie ten komponent wygląda tak:

const UsersList = ({ users }) => {
  if (users.length > 0) {
    return (
      <ul>
        {users.map(user => <li key={user}>{user}</li>)}
      </ul>
    );
  }

  return (
    <p>No results!</p>
  );
};

Komponent App

Logikę filtrowania oraz obsługę zdarzeń zamknę w komponencie App. Mógłbym się pokusić o dalszy podział na mniejsze komponenty, ale przy tak prostym przykładzie nie widzę w tym sensu. Przefiltrowanych użytkowników przechowuję w state i przekazuję do UsersList. Do inputa podpinam obsługę jednego zdarzenia onInput:

class App extends React.Component {
  constructor() {
    super();

    this.state = {
      filteredUsers: allUsers
    };
  }

  render() {
    return (
      <div>
        <input onInput={this.filterUsers.bind(this)} />
        <UsersList users={this.state.filteredUsers} />
      </div>
    );
  }
};

Stała allUsers pochodzi „z zewnątrz” — w sumie nieistotne skąd, bo nie mam tutaj żadnego API, store’a ani nic takiego. Po prostu zdefiniuj ją sobie gdziekolwiek. Przynajmniej w tym przykładzie 🙂

Implementacja filtrowania

Samo filtrowanie sprowadza się do:

  1. Pobrania wpisywanego tekstu z inputa
  2. Przefiltrowania tablicy wedle pewnych kryteriów (*)
  3. Ustawienia state

Punkt 2 oznaczyłem gwiazdką, bo to będzie jeszcze osobna funkcja. Kroki 1-3 łatwo zaimplementować:

filterUsers(e) {
  const text = e.currentTarget.value;
  const filteredUsers = this.getFilteredUsersForText(text)
  this.setState({
    filteredUsers
  });
}

Implementacja getFilteredUsersForText

W tym przypadku ta funkcja działa dość prosto. Filtruję zawartość tablicy na podstawie porównania elementu z wpisanym tekstem. Jeśli element zawiera fragment wpisanego tekstu to zostaje 🙂 Ważne: Porównanie jest niezależnie od wielkości znaków:

getFilteredUsersForText(text) {
  return allUsers.filter(user => user.toLowerCase().includes(text.toLowerCase()))
}

Rezultat

Ostatecznie stworzona aplikacja wygląda tak:

See the Pen React.js w przykładach: Filtrowanie listy by Michał Miszczyszyn (@mmiszy) on CodePen.

Pytania?

Jak wrażenia? Jeśli masz jakiekolwiek pytania albo coś jest niejasne — pisz w komentarzu! To dla mnie cenna informacja zwrotna.

Jeśli chcesz na bieżąco śledzić kolejne części kursu React.js to koniecznie polub mnie na Facebooku i zapisz się na newsletter.

Ćwiczenie

Ćwiczenie: Jak zmieniłby się kod, gdyby filtrowanie użytkowników działo się np. na backendzie i było asynchroniczne? Czy musiałabyś modyfikować komponent  UsersList? Spróbuj to zaimplementować, przyjmując, że funkcja getFilteredUsersForText jest asynchroniczna i ma taką postać:

getFilteredUsersForText(text) {
  return new Promise(resolve => {
    const time = (Math.random() + 1) * 250;
    setTimeout(() => {
      const filteredUsers = allUsers.filter(user => user.toLowerCase().includes(text.toLowerCase()));
      resolve(filteredUsers);
    }, time) ;
  });
}

Ćwiczenie*: Spróbuj szybko wpisać coś w input. Czy zawsze wyświetlają się poprawne dane? Takie problemy to tzw. race conditions. Czy umiesz je tutaj jakoś rozwiązać? Pamiętaj, że React.js to tylko biblioteka do budowania widoków, a wiedza na temat rozwiązywania ogólnych problemów jest uniwersalna i bardzo przydatna z dowolnym frameworkiem 🙂

  • Jarosław Osmólski

    Myślę, że od samego początku fajnie byłoby uczyć bindowania poza funkcja render().

    • Dlaczego?

      • Jarosław Osmólski

        Bo łatwiej zapobiegać niż leczyć 🙂

        • No może i tak 😉 W tak prostych przykładach to nie powinno mieć znaczenia, ale rzeczywiście chyba lepiej od razu uczyć dobrych praktyk.

          Sam nie używam bind w ogóle — tylko arrow functions.

  • Marcin Bogucki

    super.. 🙂 fajnie że taki przykład się pojawia… i że oprócz tematyki nauki React są także przykłady ze świata rzeczywistego… Brawo 🙂

  • Mieszko

    Z datalist i odpowiednim ”zbindowaniem’ do inputa api przeglądarki filtruje za nas. Można też o tym wspomnieć 🙂

    • Tylko że wsparcie dla datalist jest słabe, a jego możliwości bardzo ograniczone 😉 To nie kurs HTML5 tylko React 😛

  • > Pamiętaj o dodaniu unikalnego atrybutu key do każdego elementu zawsze gdy renderowana jest tablica!

    Albo pamiętaj o skonfigurowaniu lintera do kodu i pozwól sobie przypominać na bieżąco 😉 Myślę, że warto mówić o dobrych praktykach od razu.

  • Myślę, że nie powinno być problematyczne — a jeśli jest to radzę najpierw przerobić jakikolwiek kurs ES, a potem dopiero Reacta 🙂

    const allUsers = ['Michal', 'Kasia', 'Jacek', 'Marta', 'Tomek', 'Ania']; jest zdefiniowane na samej górze kodu na Codepenie (ostatni przykład).

  • Kamil Walczak

    Hej, czy rozwiązanie problemu z ćwiczenia polega na wywołaniu metody filtrującej, a potem dodać then i we wnętrzu tego dopisku przypisać wartość do state-a? To rozwiązanie działa, ale nie wiem czy jest poprawne 🙂 https://uploads.disquscdn.com/images/e915e20395f05d58703b0d3a3add076e8e393c7f4b59954bcb629279972d15f4.png

  • Whitcik

    Myślę, że lepszym rozwiązaniem byłoby w state trzymać text który ma być filtrowany, a na podstawie textu napisać metodę która zwróci prze filtrowane dane w renderze.

    • Nie jest to lepszy pomysł 🙂
      W takim przypadku przy każdym wywołaniu render tworzona byłaby nowa tablica z przefiltrowanymi imionami z identyczną zawartością (ale inną referencją!). Miałoby to potencjalnie negatywny wpływ na wydajność — a na pewno jest zupełnie niepotrzebne. Poza tym to też mało przyszłościowe rozwiązanie — dane rzadko są pobierane synchronicznie (patrz ćwiczenie).

  • Whitcik

    Dobra praktyka jest bind owanie w konstruktorze, ponieważ dzięki temu nie jest tworzona za każdym razem nowa funkcja. Wtedy jest twirzona tylko w momencie zamontowania. W tym przypadku nie ma to dużego znaczenia, ale jeśli byłby przekazywany handler do komponentu jako callback za każdym renderem będzie tworzona nowa funkcja, a co za tym idzie nowa referencja, co spowoduje niepotrzebne renderorowanie komponentu jeśli propsy przekazane do komponentu się nie zmieniły i używamy pureCompoent

    • Zrobiłem specjalnie osobny wpis tylko na temat bindowania i czemu robienie tego w funkcji render jest złe. Będzie na dniach