pátek 16. června 2017

React a Typescript

V poslední době se mě několik lidí ptalo, jestli píši v čistém javascriptu, nebo používám něco jiného. Odpověď je jednoduchá: "Mým hlavním jazykem je Typescript". Důvody proč právě Typescript, jsem zmiňoval v jednom z předešlých článků.

Dnes bych se chtěl zaměřit na to, jak vlastně využít Typescript s Reactem. Dnešní článek bude více zaměřen na jednotlivé ukázky, než na teoretickou část.

Kde začít?


V první řadě je nutné říci, že díky tomu, že React je Facebook technologie, primárně své ukázky uvádí buď v čistém javascriptu, nebo pomocí Flow. Důvod, proč právě Flow je zřejmý, je to technologie, která je také od Facebooku.

V té chvíli nastává problém, kde přesně zjistit, jak používat Typescript. Nezbývá tedy nic jiného, než že projdete několik blogů a ukázek na githubu.

Abych vám ušetřil čas, který byste museli trávit při hledání "správného řešení", zkusím projít jednotlivé části, na kterých ukážu, jak Typescript v Reactu použít. V případě ukázky v čistém javascriptu, bude použita ES6 specifikace.

Pojďme tedy na to....

Functional Stateless Components


První, na co se zaměřím jsou stateless komponenty. Jinými slovy, jsou to komponenty, které jsou v podstatě funkcí a neobsahují ani state, ani možnost využítí lifecycle. Jediné, co máte k dispozici jsou props.

React Stateless componenty byly představeny ve verzi 0.14 a veřte, že by vaše aplikace měla být složena z 95%, právě těmito komponentami.

Javascript - Stateless component
import React from 'react';

const HelloWorld = ({name}) => (
    <div>{`My name is ${name}`}</div>
);

export default HelloWorld;

Typescript - Stateless component
import * as React from 'react';

interface Props {
    readonly name: string;
}

export const HelloWorld: React.SFC<Props> = ({name}) => (
    <div>{`My name is ${name}`}</div>
);

První, čeho si můžete všimnout je import. Tato drobná změna není až tak důležitá. Stačí se podívat, jakým způsobem se importuje v Typescriptu a bude vám to jasné.

Druhou věcí je interface. To už je zajímavější část. Důvod, proč definujeme interface jakožto datový typ pro props, je ten, abychom měli danou komponentu "typově pod kontrolou". V rámci definice tohoto typu si můžete všimnout i klíčového slova "readonly". Osobně toto klíčové slovo vnímám jako jednu z ohromných výhod Typescriptu a umožňuje mi nastavovat, že daný atribut je immutable. A pokud víte, jak pracuje props v React komponentách, tak je to přesně vlastnost, kterou chcete využít. Dá se říci, že téměř všechny atributy v typech označuji tímto klíčovým slovem. Immutable stav je přesně to, co v Reactu chceme mít.

Další věcí je export. Sice máte možnost i Typescriptu použít export default, ale nedělejte to. Důvod proč, je vcelku jednoduchý. V případě, že použiji export default, tak v souboru, kam importuji tuto komponentu nemám pod kontrolou její název. Poté se dost zesložiťuje nejen refactoring kódu, ale také i případné dohledávání jednotlivých vazeb. Zjednodušeně řečeno, export default není zrovna dobrá volba.

Typescript - import with default
import NazevKteryJsemSiVymyslel from './HelloWorld';

Typescript - import without default
import {HelloWorld} from './HelloWorld';

Poslední věcí je definování typu pro React Stateless komponentu. K tomuto účelu existuje právě zmíněný deklarovaný typ SFC. Jde o zkratku "StatelessComponent", která je v React typové definici také. Osobně spíše využívám zkratku SFC, která mi šetří místo, které mohu využít pro výčet atributů z props přes destructuring assignment.

Stateful components


Druhou variantou jsou stateful komponenty. Tyto komponenty je vhodné využívat v případě kontejnerů, tedy komponent, které jsou napojeny na Redux. Dalším případem, kdy má stateful komponenta využití, je, pokud potřebujeme state či lifecycle.

Javascript - Stateful component
import React from 'react';

class HelloWorld extends React.Component {

    render() {
        return (
            <div>Hello world!</div>
        );
    }
}

export default HelloWorld;

Typescript - Stateful component
import * as React from 'react';

interface Props {

}

export class HelloWorld extends React.Component<Props, void> {

    render() {
        return (
            <div>Hello world!</div>
        );
    }
}

V tomto jednoduchém případě se příliš věcí nemění. Kromě již zmíněného importu je možné si všimnout, že díky Typescriptu mohu využít generiku, kde prvním parametrem jsou props a druhým state. V případě, že state nepoužíváte (což v případě Reduxu je požadovaný stav), je možné state nastavit na void.

Pojďme se podívat trochu dál....

Redux


Pokud ve své aplikaci používáte Redux, musíte splnit několik kroků, abyste Redux mohli používat. Nechci se zde zabývat tím, jak nastavit Redux, ale jak v rámci Reduxu používat Typescript.

První věc, kterou byste měli udělat, je vytvoření typové definice vašeho state. S největší pravděpodobností budete svojí aplikaci rozdělovat do menších celků, které na konci spojíte pomocí combineReducers.

Nejdříve si tedy navrhneme Redux state, který bude reprezentován následujícími typy. Pro zjednodušení je vše napsáno v jedné části, nicméně je dobré, aby kazdý interface byl ve vlastním souboru.

Typescript - Redux state
export interface UsersState {
    readonly list: User[];
    readonly isFetching: boolean;
    readonly lastFetched: Date;
}

export interface TenantsState {
    readonly list: Tenant[];
    readonly isFetching: boolean;
    readonly lastFetched: Date;
}

export interface State {
    readonly users: UsersState;
    readonly tenants: UsersState;
}

Další věcí jsou redux akce, které budeme chtít z komponent volat, aby modifikovaly redux pomocí reducerů. Definice akcí by mohla vypadat následovně:

Typescript - Redux actions
import {Dispatch} from 'redux';
import {State} from './State';
import {fetch} from 'tva-preferovana-knihovna';

const PREFIX = 'USERS_';

export const UsersActions = {
    FETCHING: `${PREFIX}FETCHING`,
    FETCHED: `${PREFIX}FETCHED`,
    fetchUsers() {
        return (dispatch: Dispatch<State>, getState: () => State) => {
            dispatch({type: UsersActions.FETCHING});
            fetch('Zde bude GraphQL dotaz - REST je mrtvy :)').then((result) => {
                dispatch({type: UsersActions.FETCHED, payload: result});
            }).catch((err) => {
                // Zpracovani chyby, klidne pres dalsi Redux akci
            });
        };
    },
};

Nyní mám vytvořenu akci fetchUsers(), která nejdříve odešle do reduceru akci, že se data nahrávají a poté v Promise callbacku provede druhou akci, která má již i payload, kde jsou stažena daná data.

Další část skládanky jsou samozřejmě reducery. V ukázce používám knihovnu redux-actions, kterou považuji za vhodnou, pokud se chcete vyhnout psaní pomocí switch-case.

Typescript - Reducers
import {handleActions} from 'redux-actions';
import {UsersState as State} from './UsersState';
import {UsersActions as Actions} from './UserActions';

const initialState = {
    list: [],
    isFetching: false,
    lastFetched: null,
} as State;

export const UsersReducer = handleActions<State, any>({

    [Actions.FETCHING]: (state: State): State => {
        return {...state, isFetching: true};
    },

    [Actions.FETCHED]: (state: State, action: Action<User[]>): State => {
        return {...state, isFetching: false, list: action.payload, lastFetched: new Date()};
    },

}, initialState);

Redux máme hotový. Teď už zbývá pouze daný Redux napojit na container.

React Containers


Jak už jsme si řekli, kontejnery jsou v podstatě React Stateful komponenty, které jsou napojené na Redux. Pojdmě si takový kontejner zkusit napsat a využít typovosti, kterou nám nabízí Typescript.

Typescript - React container
import * as React from 'react';
import {bindActionCreators, Dispatch} from 'redux';
import {connect} from 'react-redux';
import {State} from './State';
import {User} from './User';
import {Button} from './Button';
import {UsersList} from './UsersList';

interface OwnProps {
}

interface ConnectedState {
    readonly list: User[];
}

interface ConnectedDispatch {
    readonly fetchUsers: () => void;
}

const mapStateToProps = (state: State): ConnectedState => ({
    list: state.users.list,
});

const mapDispatchToProps = (dispatch: Dispatch<State>): ConnectedDispatch => {
    return bindActionCreators({
        fetchUsers: UsersActions.fetchUsers,
    }, dispatch);
};

class Container extends React.Component<ConnectedState & ConnectedDispatch & OwnProps, void> {

    componentWillMount(): void {
        const {fetchUsers} = this.props;
        fetchUsers();
    }

    handleOnClickRow = (user: User) => {
        console.log('Kliknul jsem na radek: ', user);
    };

    render() {
        const {list, fetchUsers} = this.props;
        return (
            <div>
                <Button onClick={fetchUsers}>Refresh</Button>
                <UsersList data={list} onClickRow={this.handleOnClickRow}/>
            </div>
        );
    }
}

export const UsersContainer = connect<ConnectedState, ConnectedDispatch, OwnProps>(mapStateToProps, mapDispatchToProps)(Container);

Uf, vypadá to šíleně, že? Ale pojďme si to rozebrat a vysvětlit, že to má svůj důvod :)

Co se týče importů, tak tam se nic zajímavého neděje, importujeme to, co v daném kontejneru budeme potřebovat.

Poté vytvoříme tři typy pro props.

První je OwnProps, který říká, že pokud bychom komponentu někdě importovali, je možné přes tento interface doplnit vlastní props. Například v případě, že je kontejner stránkou, na kterou se odkazujete přes React Router 4, bude interface vypadat následovně:

Typescript - Own props with React Router 4
import {RouteComponentProps} from 'react-router';

interface OwnProps extends RouteComponentProps<void> {
}

ConnectedState je typ, který definuje, co jsme z Reduxu vlastně získali. Tento interface je přímo spojen s funkcí mapStateToProps.

ConnectedDispatch je typ, který definuje Redux dispatch akce, které budeme v komponentě volat. Tento typ je přímo spojen s mapDispatchToProps.

V mapDispatchToProps si můžete všimnout funkce bindActionCreators, která přijímá dva parametry. Prvním je objekt obsahující dispatch funkce a v druhém je instance dispatch, která je nutná pro vykonání akce. Návratovou hodnotou je objekt obsahující jednotlivé akce, které jsou volány přes dispatch.

Samotná komponenta není až tak zajímavá. Zjednodušeně říká, že ve chvíli, kdy bude poprvé použita, automaticky načte data. Tyto data získáme z Reduxu do props a předáme je komponentě UsersList, která reprezentuje tabulku uživatelů.

Na konci si můžete všimnout, jakým způsobem se provede mapování na Redux. Metoda connect přijímá dva callbacky, což jsou mapStateToProps a mapDispatchToProps a vrací funkci, jejímž parametrem je naše komponenta, kterou chceme napojit.

V tomto případě tedy neexportujeme třídu, reprezentující naší komponentu, ale výsledek metody connect, který obsahuje naší komponentu, obohacenou o napojení na Redux. Zde také můžete vidět, proč existují tři typy pro props. Metoda connect přijímá tři generické typy, které jsme si definovaly na začátku.

Závěr


O Reactu s Typescriptem by se dalo napsat mnoho věcí. Nicméně, velikost článku by poté byl spíše kompletní příručka, což není cílem. Proto nezbývá, než toto téma rozdělit na víc částí. Příště zkusím vysvětlit, proč třeba nepoužívat funkce.bind(this) a jak se tomu vyhnout.

Žádné komentáře:

Okomentovat

Když programátor založí a řídí firmu

Jako malý jsem chtěl být popelářem. Ani ne tak proto, že bych měl nějaký zvláštní vztah k odpadkům, ale hrozně se mi líbilo, jak...