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!