Wstrzykiwanie zależności w React przy użyciu InversifyJS
InversifyJS to rozbudowana, a zarazem lekka i łatwa w użyciu biblioteka do wstrzykiwania zależności w JavaScript. Niestety, ze względu na naturę Reacta, wykorzystanie jej w komponentach nie należy do bardzo oczywistych. Wstrzykiwanie zależności w InversifyJS polega na wstrzykiwaniu w konstruktory, podczas gdy React nie pozwala nam rozszerzać konstruktorów komponentów. W tym artykule pokażę, na przykładzie prostego projektu, jak możemy obejść to ograniczenie, by móc korzystać z odwrócenia sterowania w projektach Reactowych.
Tomasz Świstak.Absolwent informatyki na Politechnice Wrocławskiej. Od 3 lat pracuje jako .NETowy full-stack developer w firmie Synergy Codes, gdzie tworzy zaawansowane wizualizacje danych w aplikacjach webowych. Oprócz tego interesuje się tematyką sztucznej inteligencji i gamedevem.
Spis treści
Przykładowy projekt ReactNative
Na potrzeby artykułu stworzyłem prosty projekt Reactowy w TypeScript. Z punktu widzenia artykułu najciekawsze dla nas są:
- Inicjalizacja odwrócenia sterowania (w skrócie: IoC).
- Klasa
NameProvider
, która dostarczy tekst do wyświetlenia przez komponent Reactowy.
import * as React from "react"; import { IProvider } from "./providers"; export class Hello extends React.Component { private readonly nameProvider: IProvider<string>; render() { return <h1>Hello {this.nameProvider.provide()}!</h1>; } }
import "reflect-metadata"; import * as React from "react"; import { render } from "react-dom"; import { Hello } from "./Hello"; const App = () => ( <div> <Hello /> </div> ); render(<App />, document.getElementById("root"));
import { Container } from "inversify"; import { IProvider, NameProvider } from "./providers"; export const container = new Container(); container.bind<IProvider<string>>("nameProvider").to(NameProvider);
import { injectable } from "inversify"; export interface IProvider<T> { provide(): T; } @injectable() export class NameProvider implements IProvider<string> { provide() { return "World"; } }
To, co widzimy na listingu 1, jest typowym wykorzystaniem InversifyJS. Tworzymy nowy kontener IoC, do którego przypisujemy klasę pod ustalonym identyfikatorem (tutaj jest to string
, ale może też być Symbol
lub typ). Jest to deklaracja w tak zwanym krótkim zasięgu (ang. transient scope), co oznacza, że z każdym wstrzyknięciem tworzymy nową instancję klasy. Dostępne są także zasięgi: singleton (zawsze ta sama instancja przy każdym wstrzyknięciu) i żądania (ang. request scope – ta sama instancja w trakcie pojedynczego wywołania container.get
).
Każda klasa, która może być wstrzyknięta przez Inversify, musi posiadać dekorator injectable
. Przy zwykłym użyciu, zależności byłyby wstrzykiwane przy użyciu dekoratora inject
wewnątrz konstruktora lub na poszczególnych właściwościach. Jednak w przypadku Reacta nie jest to możliwe, toteż musimy to zrobić w inny sposób.
Reszta projektu to prosta aplikacja Reactowa. Hello
to komponent, który wykorzystuje provider do wyświetlenia nazwy w nagłówku. W index wykonujemy renderowanie całości w DOMie. Na obecną chwilę kod nie będzie działać, ponieważ nameProvider
jest pusty. W tym właśnie miejscu wstrzykniemy instancję klasy NameProvider
.
Wykorzystanie inversify-inject-decorators
Jest to “oficjalny” sposób użycia Inversify, w przypadku gdy nie możemy skorzystać ze wstrzykiwania do konstruktorów. Biblioteka ta udostępnia nam cztery dodatkowe dekoratory: lazyInject
, lazyInjectNamed
, lazyInjectTagged
, lazyMultiInject
. Jak widać po nazwach, wykonują one wstrzykiwanie z zastosowaniem leniwej ewaluacji. Oznacza to, że zależność nie jest dostarczona w momencie inicjalizacji obiektu (jak to się dzieje w przypadku domyślnego inject
), a dopiero podczas jej pierwszego użycia i jest przechowywana do późniejszych użyć. Przechowywanie może być wyłączone poprzez ustawienie odpowiedniej wartości boolowskiej przy inicjalizacji dekoratorów.
Użycie jest bardzo proste. Pierwsze, co należy zrobić, to wykonać funkcję getDecorators
, która zwróci wspomniane wyżej dekoratory dla podanego kontenera. W przypadku naszego przykładu użyjemy jedynie lazyInject
, ze względu na to, że nie używamy nazw, tagów, a także nie przypisujemy wielu różnych klas pod jeden identyfikator. Następnie, jedyne co musimy zrobić, to użyć dekoratora lazyInject
na właściwości nameProvider
. Możesz zobaczyć te zmiany na Listingu 2.
import * as React from "react"; import { lazyInject } from "./ioc"; import { IProvider } from "./providers"; export class Hello extends React.Component { @lazyInject("nameProvider") private readonly nameProvider: IProvider<string>; render() { return <h1>Hello {this.nameProvider.provide()}!</h1>; } }
import { Container } from "inversify"; import getDecorators from "inversify-inject-decorators"; import { IProvider, NameProvider } from "./providers"; const container = new Container(); container.bind<IProvider<string>>("nameProvider").to(NameProvider); const { lazyInject } = getDecorators(container); export { lazyInject };
Biblioteka jest bardzo prosta w użyciu i osobiście miałem okazję używać ją w wielu projektach, które jednocześnie korzystały z Reacta i InversifyJS. Niestety, korzystając z niej można bardzo łatwo doprowadzić do sytuacji, gdzie mamy zależności cykliczne. Najczęściej miałem je wtedy, gdy dzieliłem kontener na moduły, które trzymałem w oddzielnych plikach. Jeżeli masz podobną strukturę kodu i natrafiłeś na zależność cykliczną, najprostszym sposobem na jej uniknięcie jest rozdział ładowania modułów do kontenera i eksportowania lazyInject
do oddzielnych plików.
Cały przykład można znaleźć tutaj.
Wykorzystanie inversify-react
Innym sposobem jest użycie inversify-react. Jest to biblioteka, która wykonuje wstrzykiwanie zależności w nieco inny sposób. Udostępnia nam ona komponent Reactowy – Provider, który przechowuje kontener IoC i przekazuje go w dół drzewa Reactowego. Razem z nim otrzymujemy cztery dekoratory – provide
, provide.singleton
, provide.transient
i resolve
. W naszym przykładzie będziemy potrzebować jedynie resolve
, aby otrzymać zależność oraz Provider, aby przekazać kontener. resolve
działa w podobny sposób jak lazyInject
z poprzednio opisywanego sposobu, jednak nie wymaga żadnej inicjalizacji.
Aby użyć biblioteki, musimy umieścić naszą aplikację wewnątrz komponentu Provider
, do którego przekazujemy kontener IoC. W Hello
używamy dekoratora resolve na nameProvider
i przekazujemy mu odpowiedni identyfikator. ioc.ts pozostaje bez zmian (w przeciwieństwie do poprzedniego sposobu). Zmiany zostały pokazane na Listingu 3.
import * as React from "react"; import { resolve } from "inversify-react"; import { IProvider } from "./providers"; export class Hello extends React.Component { @resolve("nameProvider") private readonly nameProvider: IProvider<string>; render() { return <h1>Hello {this.nameProvider.provide()}!</h1>; } }
import "reflect-metadata"; import * as React from "react"; import { render } from "react-dom"; import { Provider } from "inversify-react"; import { Hello } from "./Hello"; import { container } from "./ioc"; const App = () => ( <Provider container={container}> <div> <Hello /> </div> </Provider> ); render(<App />, document.getElementById("root"));
Moim zdaniem ten sposób również jest bardzo prosty w użyciu. Uwagę zwraca fakt, że tym razem nie musimy wykonywać żadnych zmian w IoC, a jedyna rzecz jaką robimy, to przekazywanie kontenera w drzewie Reactowym i pobieranie zależności z użyciem odpowiedniego dekoratora. Jednak w trakcie pisania tego artykułu, biblioteka była we wczesnym stadium developmentu. Jej autor nawet ostrzega nas, że jej API może zmienić się w każdym momencie, bez ostrzeżenia. Kolejną wadą jest to, że dostajemy jedynie najprostszy scenariusz wstrzykiwania zależności. Jest to oczywiście najpopularniejszy przypadek, jednak nie zawsze jest on wystarczający. W moim przypadku, w niektórych projektach potrzebowałem wstrzykiwania wielu klas zdefiniowanych pod jednym identyfikatorem, czego nie mogłem uzyskać korzystając z tej biblioteki.
Cały przykład można znaleźć tutaj.
Wykorzystanie react-inversify
Mamy tu do czynienia z podobną nazwą, ale z zupełnie innym podejściem. W tej bibliotece otrzymujemy dwie rzeczy: komponent Provider (zbliżony do tego z inversify-react) oraz higher-order component connect
, który wstrzykuje zależności do propsów.
Użycie jest zupełnie inne niż w poprzednich sposobach. Nie dostajemy tutaj żadnych dekoratorów do wykorzystania wewnątrz komponentów. Zamiast tego tworzymy klasę, gdzie wstrzykujemy zależności w zwykły sposób, korzystając z inject
. Następnie, wykorzystując HOC connect
, używamy instancję tej klasy, w celu dostarczenia zależności do propsów naszego komponentu. Użycie zostało pokazane w Listingu 4.
import * as React from "react"; import { inject, injectable } from "inversify"; import { connect } from "react-inversify"; import { IProvider } from "./providers"; type Props = { nameProvider: IProvider<string>; }; @injectable() class Dependencies { constructor( @inject("nameProvider") public readonly nameProvider: IProvider<string> ) {} } @connect( Dependencies, deps => ({ nameProvider: deps.nameProvider }) ) export class Hello extends React.Component<Props> { render() { return <h1>Hello {this.props.nameProvider.provide()}!</h1>; } }
import "reflect-metadata"; import * as React from "react"; import { render } from "react-dom"; import { Provider } from "react-inversify"; import { Hello } from "./Hello"; import { container } from "./ioc"; const App = () => ( <Provider container={container}> <div> <Hello /> </div> </Provider> ); render(<App />, document.getElementById("root"));
Jak widać, użycie jest zdecydowanie bardziej skomplikowane. Musimy tworzyć klasę i opakować nasz komponent w HOC. W pokazanym przykładzie użyłem HOC jako dekoratora, jednak można go też użyć tradycyjnie jako funkcję. Generalnie, sposób ten zdaje się być najbardziej „Reactowy” z racji, że przekazujemy obiekty przez propsy, zamiast inicjalizować cokolwiek wewnątrz komponentu. Inną zaletą jest to, że wykorzystujemy wprost funkcjonalności InversifyJS, więc nie jesteśmy ograniczani możliwościami biblioteki. Jednak wszystko to wiąże się z tym, że ostatecznie musimy napisać dużo więcej kodu.
Cały przykład można znaleźć tutaj.
Podsumowując
W artykule przedstawiłem trzy sposoby na użycie InversifyJS w projektach reactowych. Każdy z nich ma swoje wady i zalety, więc nie mogę jasno polecić konkretnego. Liczę, że porównanie to pomoże Ci w wyborze biblioteki najlepiej pasującej do Twojego projektu.
Artykuł został pierwotnie opublikowany na synergycodes.com.