Redux

Zarządzanie stanem aplikacji SPA - Redux

Redux jako nowe rozwiązanie problemu przechowywania stanu aplikacji SPA. Omówienie najważniejszych koncepcji wraz z przykładami kodu.

Wstęp

W ciągu ostatnich kilku lat dużą popularność zyskały aplikacje internetowe typu SPA (Single Page Application). Ich działanie polega na tym, że cała strona jest ładowana tylko raz. Następnie podczas przechodzenia do kolejnych podstron, nie następuje już wysłanie standardowego zapytania skutkującego pobraniem całej podstrony, czyli kodu HTML, CSS i JS. W takiej sytuacji aplikacje SPA pobierają z serwera tylko niezbędne dane, najczęściej w formacie JSON, na podstawie których aplikacja wyświetla odpowiedni widok.

Głównym celem, dla którego powstały aplikacje SPA jest umożliwienie użytkownikom płynnego korzystania z aplikacji internetowych, tak aby używało się ich podobnie jak standardowych programów uruchamianych na komputerze. Popularnym przykładem SPA jest Gmail. Podczas korzystania w przeglądarce z tej poczty mamy wrażenie płynności działania, podobnej do tej, jaką spotykamy w desktopowych programach do obsługi poczty.

Problemy z przechowywaniem stanu

Tworząc aplikacje SPA dość trudne okazuje się zarządzanie wewnętrznym stanem aplikacji (ang. application state). Mówiąc o stanie mam na myśli nie tylko dane pobrane z serwera przez API, takie jak np. lista wiadomości email w przypadku Gmaila. Stan aplikacji powinien również zawierać informacje na temat aktualnego interfejsu oraz obecny adres url.

Część stanu związaną z aktualnym wyglądem strony możemy umieścić w obiekcie DOM np. poprzez nadanie odpowiedniej klasy elementowi. Jeśli chcemy zapisać informację, że użytkownik znajduje się na stronie odebranych wiadomości, to linkowi do tej podstrony możemy przypisać klasę css active. Następnie możemy z tej informacji korzystać tak, jak pokazano poniżej.

1
2
3
if ($('link-selector').hasClass('active')) {
    // wyświtlamy stronę odebranych wiadomości
}

Jeśli masz już doświadczenie w tworzeniu aplikacji SPA, to powyższa koncepcja na pewno nie przypadła Ci do gustu. Kod działający w ten sposób jak pokazano powyżej, spełni swoje zadanie, jednak umieszczanie informacji na temat stanu w DOM sprawdzi się przede wszystkim w małych aplikacjach, wraz z ich rozbudowywaniem będzie powstawał coraz większy bałagan.

Rozwiązanie takie jak pokazano w powyższym przykładzie, jest używane przede wszystkim podczas tworzenia aplikacji z użyciem biblioteki jQuery bez dodatkowych frameworków. Z uwagi na trudność utrzymania czystego kodu w bardziej rozbudowanych projektach, nie powstało wiele prawdziwych SPA korzystających tylko z jQuery.

Modele i Kolekcje

Momentem w którym dla wielu deweloperów rozpoczęła się prawdziwa przygoda z aplikacjami SPA, było pojawienie się frameworka Backbone.js pod koniec 2010 roku. W kwestii zarządzania stanem wprowadził on Modele (Models) i Kolekcje (Collections). Model jest po prostu reprezentacją jednego zasobu (np. model wiadomości email), który oferuje kilka przydatnych możliwości, takich jak chociażby domyślne wartości, czy możliwe do wykonania na modelu metody. Kolekcja jest zbiorem obiektów danego modelu. Można powiedzieć, że jest to właściwie tablica obiektów. Kolekcją może być lista wysłanych wiadomości i lista odebranych emaili.

Modele i kolekcje Backbona są zgodne z architekturą RESTful, dzięki czemu możemy wygodnie pobierać ich dane z serwera. Kolejną zaletą jest to, że w widokach można nasłuchiwać na zmiany w modelu lub kolekcji, dzięki temu wygląd strony może się automatycznie zmieniać po zmianie danych.

Kilka lat temu Backbone był bardzo powszechnie wykorzystywany, używały go takie strony jak Allegro, Gruppon, BitBucket, Trello, Pinterest. Wiele stron w dalszym ciągu korzysta z tego frameworku.

Z czasem okazało się jednak, że Backbone i jego sposób zarządzania stanem aplikacji to trochę za mało, w dalszym ciągu można było dość łatwo wprowadzić bałagan w aplikacji. Z uwagi na zdarzenia, które mogą się odpalić z różnych miejsc i wprowadzić zmiany, ciężko również było znaleźć źródło powstałych błędów.

React i Flux

Z uwagi na braki Backbone w różnych aspektach, stracił on swoją popularność na rzecz takich frameworków jak Angular i Ember. Aby nie przedłużać niepotrzebnie tego artykułu nie będziemy się zajmować ich działaniem.

Kolejnym ważnym wydarzeniem w kwestii tworzenia aplikacji SPA, było upublicznienie w roku 2013 stworzonej i używanej przez Facebooka biblioteki React. W roku 2015 zdobyła ona popularność na tyle dużą, iż możemy powiedzieć, że rok 2015 był właśnie “rokiem Reacta”. Jedną z jego największych zalet jest duża wydajność, uzyskana przede wszystkim dzięki koncepcji wirtualnego DOM.

Jeden artykuł to zdecydowanie za mało, aby dokładnie opisać Reacta, skupimy się więc głównie na sposobie, w jaki zarządza on stanem aplikacji. Wprowadzając Reacta, Facebook podzielił się również jednokierunkową koncepcją przepływu danych Flux. Flux jest bardziej dość ogólną koncepcją, niż konkretną implementacją.

Redux

Powstało wiele implementacji architektury Flux, jedne zyskały większą, a inne mniejszą popularność. Aktualnie jedną z najpopularniejszych jest Redux, co prawda nie jest on w stu procentach zgodny z założeniami Flux, jednak doskonale sprawdza się w zadaniach, które powinna spełniać ta architektura.

Co ciekawe Redux nie musi być używany wyłącznie z Reactem. Można go wykorzystać również w innych narzędziach tworzenia aplikacjach SPA (np. Angular) lub w aplikacjach natywnych. Wyjaśnię Ci teraz czym jest ta mała, ważąca zaledwie 2kB, biblioteka.

Redux jest kontenerem stanu aplikacji, jego główne zalety, to przewidywalność działania oraz łatwość testowania. Cały stan aplikacji jest przechowywany w jednym dużym obiekcie, o którym możemy powiedzieć, że jest to “jedyne źródło prawdy” (ang. Single source of truth).

Musisz wiedzieć, że Redux nie służy do niczego, poza przechowywaniem stanu. Co prawda ułatwia on wiele kwestii, jednak bezpośrednio nie jest używany do wyświetlania danych lub pobierania ich z serwera.

Aby móc korzystać z Reduxa należ zrozumieć kilka dość prostych koncepcji, które wyjaśniam poniżej.

Stan aplikacji

Tak jak wspominałem powyżej, stan aplikacji jest jednym dużym obiektem. Na przykład dla aplikacji TODO, służącej do obsługi listy zadań do zrobienia, może on wyglądać następująco.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Kupić prezent dla mamy.',
      completed: true,
    },
    {
      text: 'Posprzątać mieszkanie.',
      completed: false
    }
  ]
}

Jeśli chcesz połączyć Reduxa z Reactem, możesz skorzystać z biblioteki react-redux, która umożliwi Ci przekazywanie informacji z obiektu stanu, do konkretnych komponentów. Gdy stan ulegnie zmianie, do komponentów zostaną przesłane nowe dane, co umożliwi im wykonanie odpowiednich działań, zostanie również automatycznie uaktualniony wygląd strony.

Akcja

Każde zdarzenie, które wprowadza jakieś zmiany w aplikacji, czyli właściwie w jej stanie, jest akcją (ang. action). Nie ma znaczenia, czy jest to zdarzenie wywołane przez użytkownika (np. poprzez kliknięcie przycisku), czy przez przeglądarkę (np. otrzymanie danych z serwera). Każde takie zdarzenie wywołuje akcję, która jest następnie odpowiednio obsługiwana.

Akcja jest właściwie obiektem, który zawiera informację o zdarzeniu, które wystąpiło. Powinny się w niej znajdować wszystkie informacje o zdarzeniu, z których chcemy skorzystać. Każda akcja musi posiadać pole type, zawierające unikalny identyfikator akcji.

Najlepiej zrozumieć czym jest akcja, pokazując konkretny kod. Poniżej znajduje się akcja tworzona w momencie dodania przez użytkownika nowego zadania do zrobienia.

1
2
3
4
{
  type: 'ADD_TODO',
  text: 'Iść do sklepu po chleb.'
}

Tworzenie obiektów akcji, jest bardzo często realizowane przez funkcje, które mogą wyglądać tak jak poniżej.

1
2
3
4
5
6
function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}

Oczywiście same utworzenie obiektu akcji nie wystarczy, musimy jeszcze poinformować aplikację, że ma go obsłużyć. Aby to zrobić należy przekazać obiekt do funkcji dispatch.

1
dispatch(addTodo('Iść do sklepu po chleb.'))

Reducer

Reducer (pol. reduktor, chociaż to dziwnie brzmi :)) określa w jaki sposób aplikacja ma reagować na zgłoszoną akcję. Jest to zwykła funkcja, która przyjmuje aktualny stan aplikacji oraz akcję, na podstawie tych danych zwraca ona kolejny stan. Aby wszystko działało dobrze reducer nie powinien zmieniać obiektu starego stanu, ani wykonywać żadnych dodatkowych działań, takich jak np zapytania do serwera. Okazuje się, że dzięki temu reducery są bardzo przewidywalne i łatwe do testowania.

Dla naszej aplikacji TODO fragment reducera może wyglądać następująco.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function todoApp(state = initialState, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })    
    default:
      return state
  }
}

W powyższym przykładzie zakładamy, że istnieje zmienna initialState zawierająca początkowy stan aplikacji. Jak widzisz użyliśmy również metody Object.assign, która pozwala nam utworzyć nowy obiekt stanu, bez modyfikacji tego starego.

Działanie reducera jest bardzo proste, na podstawie typu przekazanej akcji action.type, wybiera on odpowiedni blok case z instrukcji switch. Następnie korzystając z danych poprzedniego stanu i przekazanej akcji, tworzy i zwraca obiekt nowego stanu.

Podsumowanie

Podsumowując, działanie Reduxa wygląda następująco. Podczas wystąpienia zdarzenia zostaje utworzona i zgłoszona odpowiednia akcja, zawierająca wszystkie informacje na temat zdarzenia, z których chcemy korzystać. Następnie uruchamiany jest reducer, czyli zwykła funkcja, która na podstawie poprzedniego stanu aplikacji i obiektu akcji która wystąpiła, tworzy i zwraca następny stan aplikacji. Po zmianie obiektu stanu, nowe dane zostają przekazane do komponentów, co skutkuje zaktualizowaniem koniecznych elementów widoku aplikacji, mogą również zostać podjęte dodatkowe działania.

Jak widzisz przepływ danych w Reduxie jest jednokierunkowy, nie występuje tu znany chociażby z Angulara mechanizm two way data binding. Dzięki takiemu podejściu aplikacja napisana z użyciem Reduxa jest przewidywalna, o wiele łatwiej możemy znaleźć błędy w działaniu oraz działa wydajnie. Trzeba jednak pamiętać, że jednokierunkowy przepływ danych wiąże się z koniecznością napisania większej ilości kodu.

Na tym kończę zwięzły opis tego, w jaki sposób Redux pozwala nam usprawnić zarządzanie stanem aplikacji. Oczywiście artykuł ten nie jest kompletnym tutorialem, bardziej chodzi o wyjaśnienie działania Reduxa. Jeśli jesteś zainteresowany Reduxem polecam skorzystać z dokumentacji, która zawiera szczegóły na temat jego działania.

Nawet jeśli nie tworzysz aplikacji SPA, możesz się zastanowić, czy sposób w jaki działa Redux nie nadaje się do wykorzystania, w tworzonym przez Ciebie projekcie. Być może będzie to dala Ciebie inspiracja do usprawnienia architektury.