Po małej dygresji na temat pracy zdalnej dziś wracamy do ReactJS. Wiesz już co nieco na temat architektury Flux oraz Reduxa czyli jednej z jej najpopularniejszych implementacji. Dziś postaram się poszerzyć trochę Twoją wiedzę na temat tego zagadnienia i pokażę Ci co to jest Redux middleware…

Co to jest Redux middleware - definicja

Jeśli zajrzysz do oficjalnej dokumentacji Redux to znajdziesz tam takie oto wyjaśnienie tego czym jest Redux middleware:

Redux middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

Ogólnie rzecz biorąc znaczy to, że Redux middleware to nic innego jak funkcja, która wywoływana jest pomiędzy rozgłoszeniem akcji a momentem, w którym akcja ta zostaje obsłużona przez “reducer”.

Z tego też powodu, Redux middleware to idealne miejsce aby dodać do aplikacji, na przykład mechanizm logowania wywoływanych akcji.

Od siebie dodam, że jak dla mnie wykorzystanie Redux middleware jest dość podobne do programowania aspektowego - jest to specjalna funkcja, która jest transparentna zarówno dla akcji jak i “reducera” i uruchamiana jest w momencie nastąpienia jakiejś innej akcji.

Redux middleware - przykład

No dobra… Skoro wiemy już z definicji co to jest Redux middleware to czas przejść do “mięcha” czyli przykładu użycia tego mechanizmu. Wspomniałem przed momentem o wykorzystaniu go do logowania. Dlatego też właśnie taki przykład za chwilę przeanalizujemy.

Myślę, że nie ma sensu wymyślać koła na nowo, spójrzmy więc na przykład middleware służącego do logowania, który dostępny jest w dokumentacji Reduxa:

const logger = store => next => action => {
  console.log('dispatching', action);
  let result = next(action);
  console.log('next state', store.getState());
  return result;
}

export default logger;

Dygresja - zagnieżdżenie funkcji

Pierwsze co, to zwróć uwagę na zapis w stylu ES6, który tutaj wykorzystano. Domyślam się, że coś takiego jak poniżej może być trochę niezrozumiałe dla “świeżaków”:

const logger = store => next => action => { ... }

Spieszę więc z wyjaśnieniem. Powyższy kod to tak na prawdę zagnieżdżenie funkcji, jedna w drugiej. Jeśli zamiast “arrow functions” użyłbym zwykłych funkcji to kod ten można by zapisać jak poniżej:

function logger(store) {
  return function wrapDispatch(next) {
    return function dispatch(action) {
      ...
    }
  }
}

Takie zagnieżdżanie funkcji (czy też inaczej zwracanie funkcji przez funkcje) wywodzi się z języków funkcjonalnych i zwane jest tam jako “currying”.

No dobra, ale jak to działa? No więc… funkcja logger to generalnie nasze middleware. Pobiera on jako parametr obiekt “store”, które później wstrzykiwane jest tutaj przez funkcję applyMiddleware (za chwilę napiszę o tym więcej).

Funkcja logger zwraca jako rezultat następną funkcję, która z kolei jako parametr przyjmuje funkcję next. Funkcja ta pozwala na rozgłoszenie kolejnej akcji.

Ostatnia, najbardziej wewnętrzna funkcja pobiera w parametrze obiekt akcji. Wszystkie parametry przykazane do wcześniejszych, bardziej zewnętrznych funkcji są oczywiście dostępne wewnątrz tej funkcji. Są to wszystkie niezbędne elementy potrzebne do manipulacji akcjami i stanem. Użyjemy ich za chwilę do implementacji naszego middleware.

Implementacja Redux middleware

No dobra, ale wróćmy do tematu… Spójrzmy na kod implementujący nasze middleware (ciało najbardziej wewnętrznej funkcji):

console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;

W pierwszej linii po prostu logujemy na konsolę akcję jaka w danym momencie jest rozgłaszana. Z poprzednich moich wpisów na temat Redux wiesz już, że jest to zwykły literał obiektu zawierający co najmniej jedną właściwość: type.

W kolejnej linii wywołujemy funkcję next, do której przekazujemy obiekt akcji. Powoduje to “dispatchowanie” tej akcji dzięki czemu w kolejnej linii, po wywołaniu store.getState() dostajemy stan aplikacjijuż po rozgłoszeniu tej akcji.

Na koniec po prostu zwracamy rezultat wywołania funkcji next.

Konfiguracja uwzględniająca middleware

Kiedy mamy już zaimplementowane nasze własne middleware, musimy jeszcze poinformować “store” Reduxa, że ma go używać. Można to zrobić, przekazując rezultat funkcji applyMiddleware jako parametr wywołania funkcji createStore. Spójrz na przykład:

import { createStore, applyMiddleware } from 'redux';
import logger from './logger'; // our custom middleware
import reducers from './reducers'; // combined reducers

const store = createStore(reducers, applyMiddleware(logger));

Jak widzisz, funkcja applyMiddleware jest częścią biblioteki Redux. Zwróć uwagę, że do funkcji tej przekazujemy jako parametr zdefiniowaną przez nas funkcję logger. Jeśli chcemy wstrzyknąć większą liczbę middleware (własnych lub będących częścią jakichś zewnętrznych bibliotek) to wystarczy przekazać je jako kolejne parametry wywołania funkcji applyMiddleware.

Co to jest Redux middleware - podsumowanie

To tyle na dziś. Mam nadzieję, że udało mi się w miarę jasno i przystępnie przedstawić co to jest Redux middleware. W następnym wpisie omówię temat aktualizowania stanu Reduxa za pomocą wywołań asynchronicznych. W tym kontekście pokażę też specjalny middleware: redux-thunk (z tego powodu uznałem, że trzeba najpierw pokazać co to w ogóle jest middleware). Zapraszam!