Wraz z pojawieniem się wersji v16.3.0 Reacta, do naszych rąk trafiła całkiem nowa i myślę, że całkiem przydatna funkcjonalność. Mowa tutaj o tytułowym Context API, które pozwala na wprowadzenie do aplikacji pewnych globalnych ustawień, od których zależeć mogą niektóre komponenty. Dzięki temu implementacja w aplikacji, na przykład, wielu wersji kolorystycznych staje się dziecinnie prosta. Zresztą sam się za chwilę o tym przekonasz!

Tworzymy kontekst

Aby przekonać się jak można wykorzystać mechanizm kontekstu w React, stworzymy za chwilę prostą aplikację, w której możliwe będzie dynamiczne zmienianie koloru tekstu. Na początku oczywiście należy kontekst utworzyć.

API nowej wersji Reacta wzbogacone zostało o metodę createContext, której zadaniem jest, jak sama nazwa wskazuje, tworzenie kontekstu. Spójrzmy jak możemy z niej skorzystać (ja robię to w osobnym pliku o nazwie ColorContext.js, ponieważ wartość zwracana przez ten moduł będzie wykorzystywana w kilku miejscach):

import React from 'react';

export const ColorContext = React.createContext('red');

W powyższym przykładzie tworzona i eksportowana jest stała ColorContext, do której utworzenia posłużyła metoda createContext. Myślę, że warto wiedzieć, co dokładnie zwracane jest jako wynik działania tej funkcji. Szybki rzut oka na dokumentację, w której widnieje taki przykład:

const { Provider, Consumer } = React.createContext(defaultValue);

Oznacza to, że obiekt ColorContext, który utworzyliśmy w naszym przykładzie zawiera dwie właściwości (komponenty): Provider oraz Consumer. Za chwilę z nich skorzystamy - na razie napiszę jedynie, że komponent Provider służy do dostarczania kontekstu swoim dzieciom. Consumer natomiast to komponent umożliwiający odczytanie i wykorzystanie aktualnej wartości kontekstu dostarczanego przez najbliższy Provider (iterując w górę drzewa VirtualDOM).

Drugą istotną rzeczą w naszym przykładzie jest wartość tekstowa red przekazywana do metody createContext. Parametr ten jest wartością domyślną dla komponentu Consumer, która zostanie wykorzystana jedynie w przypadku gdy wśród jego rodziców “konsumera” nie zostanie znaleziony żaden Provider.

Jeśli powyższe nie jest zrozumiałe, nie martw się - za chwilę wszystko się trochę bardziej rozjaśni.

Komponent Provider

Jak wspomniałem powyżej, komponent Provider pozwala na dostarczenie kontekstu do komponentów dzieci. Spójrz na przykład:

import React, { Component } from 'react';
import { ColorContext } from './ColorContext';
import WelcomeText from './WelcomeText';

class App extends Component {
  state = { color: 'red' };

  onColorChange = () => {
    const { color } = this.state;
    const newColor = color === 'red' ? 'black' : 'red';

    this.setState({ color: newColor });
  }

  render = () => {
    const { Provider } = ColorContext;

    return (
      <Provider value={this.state.color}>
        <WelcomeText />
        <button onClick={this.onColorChange}>Toggle color!</button>
      </Provider>
    );
  }
}

export default App;

Zwróć uwagę, że w powyższym przykładzie importowany jest, z modułu ColorContext.js, utworzony przez nas wcześniej obiekt ColorContext. Następnie, w metodzie render komponentu App wykorzystywany jest komponent Provider dostępny w tym obiekcie.

Komponent Provider przyjmuje jeden atrybut - value - do którego przypisujemy aktualną wartość kontekstu. Wartość ta będzie dostępna dla wszystkich “konsumerów”, które zostaną użyte w komponentach-dzieciach (i ich dzieciach, itd.) - zresztą o tym za moment.

W naszym przykładzie do atrybutu value komponentu Provider przypisano wartość właściwości color przechowywanej w stanie komponentu App. Zauważ, że wartość ta jest zmieniana za każdym razem gdy użytkownik naciśnie przycisk (metoda onColorChange). W ten sposób zmieniany jest też aktualny kontekst aplikacji, a zmiana ta propagowana jest do “konsumerów” dzieci komponentu Provider.

Komponent Consumer

Wiemy już jak utworzyć kontekst i udostępnić go w aplikacji - czas teraz ten kontekst skonsumować. Do tego właśnie celu służy komponent Consumer, który obok komponentu Provider również dostępny jest jako część obiektu ColorContext zwracanego z modułu ColorContext.js. Komponent ten wykorzystamy w komponencie WelcomeText, który został wyrenderowany w komponencie App (spójrz na metodę render w poprzednim przykładzie kodu).

Spójrzmy na przykładową implementację komponentu WelcomeText:

import React from 'react';
import { ColorContext } from './ColorContext';

const WelcomeText = (props) => {
  const { Consumer } = ColorContext;

  return (
    <Consumer>
      {color => <p style={{ color }}>Hello world!!</p>}
    </Consumer>
  );
};

export default WelcomeText;

Na powyższym przykładzie widać, że dzieckiem komponentu Consumer jest funkcja (w przypadku Context API jest to wymagane). Funkcja ta przyjmuje jeden parametr, który posiada tę samą wartość, którą przekazaliśmy do komponentu Provider poprzez atrybut value. W naszym przykładzie wykorzystujemy go do pokolorowania tekstu.

Przekazywanie funkcji jako dziecko komponentu to implementacja techniki zwanej “render props”. Do tej pory nie pisałem o tym na blogu ale jeśli temat Cię interesuje to więcej znajdziesz w dokumentacji, pod tym linkiem.

Oczywiście zmiana wartości this.state.color w komponencie App powoduje propagację tej zmiany do wszystkich “konsumerów” komponentu Provider. Alternatywą dla opisywanego podejścia mogłoby być przekazywanie aktualnego koloru dzieciom komponentu App poprzez “propsy”. Staje się to jednak mało wygodne jeśli musimy przekazać tę wartość wiele poziomów “głębiej” - w takim przypadku zastosowanie mechanizmu Context API jest znacznie wygodniejsze.

Context API a sprawa Reduxa

Po przeanalizowaniu powyższych przykładów, możesz dojść do wniosku, że Context API to świetna alternatywa dla Reduxa (i innych implementacji architektury Flux). Dostarcza globalny stan dla aplikacji? Dostarcza! Mniej kodu trzeba napisać aby zaktualizować stan? Mniej! Pytanie więc czy faktycznie możemy używać tych dwóch podejść zamiennie - w końcu, jak mawiał klasyk, You Might Not Need Redux

Odpowiedź jak zwykle brzmi: “to zależy”… Wszystko jak zawsze w programowaniu uwarunkowane jest potrzebami - moim zdaniem, jeśli Twoja aplikacja jest relatywnie mała lub średnia, a globalny stan jest niewielki, możesz zamiast wprowadzać Reduxa zastosować Context API. Jeśli jednak wiesz, że aplikacja będzie duża (lub sporo jej stanu będzie musiało być dostępne globalnie), to Redux może okazać się lepszym rozwiązaniem, które poza dostarczaniem globalnego stanu dla aplikacji, dodatkowo pomaga lepiej ustrukturyzować całą aplikację.

Podsumowanie

Bardzo mi się podoba sposób, w jaki rozwija się React. Kolejne wersje tej biblioteki wprowadzają kolejne ciekawe usprawnienia, a Context API niewątpliwie do nich należy. Wydaje mi się, że warto znać tę technikę bo jest to prosty sposób na wprowadzenie globalnego stanu dla aplikacji.

P.S. Jak zwykle, przedstawione dziś przykłady kodu można znaleźć na GitHubie i samodzielnie je “poklikać”.