Podstawy Reacta

W ciągu ostatnich dwóch tygodni miałem okazję zaznajomić się z biblioteką React. Jest to narzędzie do tworzenia UI dla aplikacji webowych za pomocą JavaScriptu. Ja będę używał akurat TypeScriptu, żeby moje programy były bardziej poprawne. W tym artykule chcę opisać podstawy Reacta i jego testowania.

Jak działa React? Biblioteka utrzymuje w pamięci wirtualne drzewo DOM, które składa się z wirtualnych komponentów. Każdy komponent jest renderowany do postaci tagów HTML. Komponent dostaje parametry i może też utrzymywać swój stan. React śledzi zmiany stanu i odświeża odpowiednie elementy strony.

Żeby utworzyć aplikację React polecam użyć polecenia

npx create-react-app app-name --typescript

Dostaniemy szablon aplikacji i będziemy mogli używać menadżera pakietów yarn. Można albo uruchamiać npx yarn jeśli mamy zawsze internet, albo zainstalować go sobie lokalnie npm i -g yarn.

Żeby było wygodniej, do tworzenia aplikacji w Reactcie używa się rozszerzenia JSX, które zamienia tagi bliskie tym w HTML na wywołania metod JavasScript. Ponieważ tag jest wywołaniem metody, to nie możemy zwrócić kilku tagów na najwyższym poziomie zagnieżdżenia - zawsze jest jeden korzeń i potem może być już wiele dzieci - kolejnych parametrów tej głównej metody. Jeśli nie chcesz opakowywać elementów w <div> to możesz je zapakować w <React.Fragment>, który po prostu wyrenderuje swoje dzieci, samemu znikając.

Dobra, przejdźmy do jakiegoś przykładu:

import React from "react";

interface HelloProps { name: string; }

export function Hello(props: HelloProps) {
    return (
        <div>
            <h1>This is a sample component</h1>
            <p>Hello {props.name}!</p>
        </div>
    );
}

Jest to prosty komponent funkcyjny który przyjmuje parametry w swoim argumencie i renderuje powitalną wiadomość. Jeśli w JSX użyjemy klamer {} to w środku umieszczamy wyrażenie, które zostanie obliczone i jego wynik umieszczony na stronie.

Żeby użyć tego komponentu, należy przekazać mu parametr

<Hello name="World" />

Parametry typu string można podać bezpośrednio tak jak tutaj, a wszelkie inne muszą być opatrzone w klamry:

<Button size={4} style={ { display: "block" } }>Search</Button>

Jeśli chcemy w komponencie funkcyjnym użyć stanu, to możemy użyć funkcji

const [state, setState] = React.useState(initialValue);

Dostajemy stały obiekt state i funkcję do jego modyfikacji

setState( oldState => newState )

Drugim typem komponentów są komponenty klasowe. Te będą dziedziczyć po klasie React.Component.

interface P { initial: int; }
interface S { counter: int; }

export class Counter extends React.Component<P, S> {
    constructor(props: P) {
        super(props);
        this.state = { counter: props.initial };
    }

    public inc() {
        this.setState(s => ({...s, counter: s.counter + 1 }));
    }

    public render() {
        return (
            <button onClick={this.inc.bind(this)}>
                {this.state.counter}
            </button>
        );
    }
}

Przy większych komponentach klasy są wygodniejsze.

Testowanie

Powiedzmy, że mamy jakiś abstrakcyjny interfejs

interface IDataProvider {
    getData(): Promise<string[]>;
}

I mamy komponent

interface P { dataProvider: IDataProvider; }

export class DataList extends React.Component<P, string[]> {
    public async componentDidMount() {
        const data = await this.props.dataProvider.getData();
        this.setState(data);
    }

    public render() {
        return (
            <React.Fragment>
                {
                    this.state.map((s, i) => (
                        <p key={i}>{s}</p>
                    ))
                }
            </React.Fragment>
        );
    }
}

Ta pierwsza metoda to jest taki trochę onLoad dla komponentów. Podczas renderowania zauważmy, że w jeśli tworzymy tablicę tagów, to każdy musi dostać unikatowy parametr key, taki jest wymóg Reacta.

No i chcemy teraz przetestować ten komponent. Utworzymy sobie plik testowy DataList.test.tsx

import Enzyme, {mount, shallow} from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import React from "react";
import { IDataProvider } from "./IDataProvider.ts";
import { DataList } from "./DataList.tsx";

Enzyme.configure({adapter: new Adapter() });

const data = ["Hello"];

function mockDataProvider() {
    const Mock = jest.fn<IDataProvider, []>(() => ({
        getData: jest.fn(() => data)
    }));
    return Mock;
}

describe("DataList component", () => {
    it("when mounted gets data", () => {
        const mockDP = mockDataProvider()();
        const list = shallow<DataList>(
            <DataList dataProvider={mockDP} />
        );

        setTimeout(() => {
            const mockGet = mockDP.getData as jest.Mock<Promise<string[]>, []>;
            expect(mockGet).toHaveBeenCalled();
            expect(list.state()).toEqual(data);

            done();
        });
    })
})

Dobra, trochę tu jest do odpakowania. Więc korzystam z biblioteki Jest do testów i biblioteki Enzyme do pracy z komponentami Reacta. Enzyme potrzebuje adaptera do wersji Reacta, której używam.

Tworzymy mock naszego interfejsu - obiekt z funkcją, którą możemy odpytać o to czy była wywołana, ile razy i z jakimi argumentami.

Ponieważ funkcja zwraca Promise to musimy wywołać część testu za chwilę, po tym jak asynchroniczna kontynuacja montowania zostanie wykonana. Na koniec wołamy done(), żeby powiadomić środowisko o zakończeniu testu.

Enzyme daje nam dwie funkcje: shallow i mount. Pierwsza tworzy komponent ale nie tworzy jego dzieci. Druga tworzy komponent wraz z dziećmi i pozwala wyciągać dzieci przez .find(ComponentName) i wywoływać na nich metody lub eventy.

No i to tyle w tym temacie. Miłego programowania!