Zobrazují se příspěvky se štítkemGraphQL. Zobrazit všechny příspěvky
Zobrazují se příspěvky se štítkemGraphQL. Zobrazit všechny příspěvky

středa 18. července 2018

React a hrátky s TypeScriptem


V minulosti jsem se již několikrát zmiňoval, že používat JavaScript bez statických typů, je stejné jako jezdit na kole poslepu. Nemusí se Vám nic stát, ale také si můžete hezky ublížit. Jednou z variant, jak částečně předcházet problémům, je použití staticky typovaného jazyka. Už během psaní kódu je více viditelné, že "něco není v pořádku". TypeScript (dále jen TS) je jazyk, který nám k tomuto účelu může dobře posloužit.

Cílem dnešního článku jsou příklady, na kterých se pokusím demonstrovat vlastnosti jazyka, se kterými se lze setkat. Výčet určitě není kompletní, protože TS je velice sofistikovaný jazyk, který se nedá popsat ani jednou knihou.

Generické typy


Jak jednou řekl můj bývalý kolega: "Generické typy? To je ten zápis s kachníma zobákama, ne?" :)

Generické typy jsou součástí snad každého staticky typovaného jazyka. Generiky naleznete jak v Jave, C#, tak právě i v TS. Pokud v TS píšete, tak neexistuje téměř možnost, že byste se s generickými typy nesetkali.

Pojďme se podívat na jednoduchý příklad:
interface Props {
    firstName: string;
    lastName: string;
}
const User: React.SFC<Props> = ({firstName, lastName}) => (
    <div>
        {firstName} {lastName}
    </div>
);
const App = () => <User firstName={'Ales'} lastName={'Dostal'} />;

Zde je vidět, že je použita generika v React.SFC, což je typ, který definuje, že se jedna o React Stateless komponentu. Součástí tohoto typu je možnost uvést generický typ, tedy předem neznámý typ, který ovšem po deklaraci, bude kontrolován.

Zjednodušeně se dá říci, že pokud bychom v komponentě User definovali neznámý atribut, bude nám TS hlásit chybu:
Chybný atribut foo 
Jejich využití má největší přidanou hodnotu ve chvíli, kdy píšete kód, který má využití na více místech. Generické typy nabízí právě tu možnost, aby daný kód byl co nejvíce variabilní.

Od verze TS 2.9 je možné generické typy používat i přímo v JSX. Díky tomu je například snadnější typovat render props v Reactu.

Enum a string literal types


Často se dostáváme do situací, kdy je třeba definovat přesný výčet hodnot, které lze použít. 

Pojďme si rozšířit naší komponentu o novou vlastnost:
type UserType = 'admin' | 'guest';

interface Props {
    firstName: string;
    lastName: string;
    type: UserType;
}

const User: React.SFC<Props> = ({firstName, lastName}) => (
    <div>
        {firstName} {lastName}
    </div>
);
const App = () => <User firstName={'Ales'} lastName={'Dostal'} type={'admin'} />;

Nyní jsme přidali typ uživatele. Tím typem může být administrátor či host.

Pokud bychom do atributu type vložili jiný typ, tak TS zahlásí chybu:
Chybný typ uživatele
Kromě možnosti definovat výčet pomocí string literal types, tak je možné onen typ definovat i pomocí enum.

Příklad:
enum UserType {
    ADMIN = 'admin',
    GUEST = 'guest',
}
const App = () => <User firstName={'Ales'} lastName={'Dostal'} type={UserType.ADMIN} />;


Type queries a typeof


JavaScript disponuje klíčovým slovem typeof, který umí číst typy z jakéhokoliv objektu. Velice efektivně toho dá využít pro pro vytvoření typu z objektu.

Ano, zní to možná divně, ale pojďme se podívat na ukázku:
const initialState = {count: 0};

class User extends React.Component<Props, typeof initialState> {
    readonly state = initialState;

    handleOnClick = () => {
        this.setState(({count}) => ({count: count++}));
    };
    
    render() {
        const {firstName, lastName} = this.props;
        const {count} = this.state;
        return (
            <div onClick={this.handleOnClick}>
                {firstName} {lastName} | Count: {count}
            </div>
        );
    }
}

Komponentu User jsme přepsali do třídy a přidali State. Součástí toho je výchozí state, který komponenta očekává. V našem případě začíname na count = 0. Proto jsme využili toho, že podle nadefinovaného výchozí stavu, jsme vytvořili i daný typ State. Stejným způsobem bychom mohli použít typeof i v případě defaultních hodnot pro props.

Readonly a readonly


Nejen v Reactu bychom se měli snažit o to, abychom psali více funkcionálně. Současně s tím je důležité se zaměřit na immutable stav. Tedy najít způsob, jak zabezpečit, aby daná hodnota nešla přepisovat přímo, ale vždy se vytvářela pouze nová kopie.  K tomuto účelu můžeme využít readonly, který nám zajišťuje, že daný atribut v objektu bude immutable (alespoň z pohledu TS).

Pojďme na ukázku:
interface Props {
    readonly firstName: string;
    readonly lastName: string;
    readonly type: UserType;
}

const initialState = {count: 0};

class User extends React.Component<Props, Readonly<typeof initialState>> {
    readonly state = initialState;

    handleOnClick = () => {
        this.setState(({count}) => ({count: count++}));
    };

    render() {
        const {firstName, lastName} = this.props;
        const {count} = this.state;
        return (
            <div onClick={this.handleOnClick}>
                {firstName} {lastName} | Count: {count}
            </div>
        );
    }
}

V ukázce jsou použity dvě varianty. První variantou je definování u props, kde každý atribut je označen pomocí klíčového slova readonly. Druhou variantou je State, kde je použit typ Readonly. Pokud bychom nyní chtěli napřímo mutovat atributy Props či State, tak TS nám bude hlásit chybu, že daný atribut je readonly.

Pick


Další skvělou vlastností je typ Pick. Tento typ slouží k tomu, abychom si z předem definovaného typu, vytvořili nový typ, který bude obsahovat pouze ty atributy, které dopředu určíme.

Pojďmě opět na ukázku:
type UserType = 'admin' | 'guest';

interface UserDataQuery {
    readonly firstName: string;
    readonly lastName: string;
    readonly type: UserType;
}

interface Query {
    data: UserDataQuery;
    roles: string[];
}

interface Props extends Pick<Query, 'data'> {}

const initialState = {count: 0};

class User extends React.Component<Props, Readonly<typeof initialState>> {
    readonly state = initialState;

    handleOnClick = () => {
        this.setState(({count}) => ({count: count++}));
    };

    render() {
        const {data} = this.props;
        const {count} = this.state;
        return (
            <div onClick={this.handleOnClick}>
                {data.firstName} {data.lastName} | Count: {count}
            </div>
        );
    }
}

const App = () => <User data={{firstName: 'Ales', lastName: 'Dostal', type: 'admin'}} />;

Často se setkáváme s tím, že máme například z GraphQL vygenerovaný typový model, ale ten, na úrovni Query obsahuje všechny možné atributy, které lze získat. Zde se výborně hodí onen typ Pick, který nám vytvoří typ, který vychází z typu Query. Obsahovat bude pouze ty atributy, které si sami určíme. V našem případě tedy atribut data.

Omit


Pokud pomocí typu Pick můžeme definovat atributy, které nový objekt má obsahovat, jeho protějškem je typ Omit. Ten naopak umí definovat, které atributy nový typ obsahovat nebude.

Pojďme na ukázku (v ukázce je Omit použi z knihovny recompose):
interface UserQuestProps {
    data: Omit<UserDataQuery, 'type'>;
}

const UserGuest: React.SFC<UserQuestProps> = (props) => {
    return <User data={{...props.data, type: 'guest'}} />;
};

const App = () => (
    <div>
        <User data={{firstName: 'Ales', lastName: 'Dostal', type: 'admin'}} />
        <UserGuest data={{firstName: 'Ales', lastName: 'Dostal'}} />
    </div>
);

V tomto případě jsme vytvořili novou komponentu UserGuest, která ovšem umožňuje, že i když vychází ze stejného datového typu UserDataQuery, tak odstraňuje atribut type, který je přímo nastaven na hodnotu guest.

V tomto případě je na zvážení, zda by ona komponenta UserGuest, neměla být napsána spíše jako HOC (Higher-Order Component). Ale o tom až jindy :)

Závěr


Vybral jsem pár zajímavých vlastností TS jazyka, se kterými se lze často setkat a zároveň i věci, které nejsou až tak známé (viz Pick či Omit). Příště se zkusíme podívat na některé další vlasnosti.

Na závěr ještě celá ukázka našeho příkladu:
type UserType = 'admin' | 'guest';

interface UserDataQuery {
    readonly firstName: string;
    readonly lastName: string;
    readonly type: UserType;
}

interface Query {
    data: UserDataQuery;
    roles: string[];
}

interface Props extends Pick<Query, 'data'> {}

const initialState = {count: 0};

class User extends React.Component<Props, Readonly<typeof initialState>> {
    readonly state = initialState;

    handleOnClick = () => {
        this.setState(({count}) => ({count: count++}));
    };

    render() {
        const {data} = this.props;
        const {count} = this.state;
        return (
            <div onClick={this.handleOnClick}>
                {data.firstName} {data.lastName} | Count: {count}
            </div>
        );
    }
}

interface UserQuestProps {
    data: Omit<UserDataQuery, 'type'>;
}

const UserGuest: React.SFC<UserQuestProps> = (props) => {
    return <User data={{...props.data, type: 'guest'}} />;
};

const App = () => (
    <div>
        <User data={{firstName: 'Ales', lastName: 'Dostal', type: 'admin'}} />
        <UserGuest data={{firstName: 'Ales', lastName: 'Dostal'}} />
    </div>
);

středa 23. května 2018

React aplikace od začátku do konce

V poslední době jsme byli nuceni napsat více React aplikací, které vždy vychází ze stejného základu. Díky tomu jsem si uvědomil, že i když můžeme jednoduše provést setup čistého projektu, přeci jen je několik částí, které je nutné doplnit vlastním kódem. To mě nakonec dovedlo k myšlence, že jsem vzal čistý list papíru (gitu) a napsal example projekt, který slouží jako základní dev stack.

A tak jsem vytvořil "Aldu"

DEMO: https://alda.app
Git repozitář: https://github.com/ApiTreeCZ/alda

Co je cílem tohoto projektu?


Je to hlavně demonstrace toho, jak lze jednoduše napsat React aplikaci, která má snad vše, co je třeba. Díky tomu je zde přesah i do dalších oblastí jako je třeba nasazení do Kubernetes v Cloudu, CI/CD, apod.

Proč vlastní řešení?


Hlavním důvodem je, že díky rychlé evoluci JavaScript technologií se často dostáváme do stavu, kdy aktualizace jedné knihovny zapříčiní problémy v celém systému. Proto je snaha, aby tento projekt používal co nejaktuálnější knihovny a neustále dokola opravoval problémy, které vznikají s novými verzemi. Ano, svět JavaScriptu má i odvrácenou temnou stranu a tou je "jepičí život" některých knihoven a verzí.

Dalším důvodem je to, co již bylo zmíněno na začátku. Tedy využití Aldy pro start nových projektů.

A nakonec to nejdůležitější a tím je, že když chci propojit několik technologií, tak je velice malá pravděpodobnost, že najdu kompletní řešení. Vždy musím někde napsat nějakou část kódu, která nás nutí k tomu, abychom se stále probírali různými možnostmi, jak to vlastně udělat.

Co ten systém umí?


Tak hlavně je to React a to stačí :) .... Teď važně.

Jedná se React (už zase :)) aplikaci napsanou pomocí TypeScriptu. Celý systém je napsán přes Next.js, tudíž automaticky se jedná o isomorfní aplikaci, která podporuje server side rendering. Díky tomu jí lze nasadit na veřený web a není důvod se bát kvůli SEO a indexování od vyhledávačů.

Abychom trochu dodrželi štábní kulturu, tak v projektu je implementován TSLint a Prettier. Tedy kontrola na zápis kódu a zároveň automatické formátování podle předepsaných pravidel. Statická analýza kódu je v JavaScript světě nutností.

Celý systém je postaven na knihovně material-ui, která před pár dny vyšla oficiálně ve verzi 1.0. Díky tomu jsou CSS psány pomocí JavaScriptu a nakonec generovány jako css třídy.

Dále obsahuje Redux, do kterého si ukládá globálně důležité stavy. Díky tomu je zde redux-form, což je knihovna pro formuláře.

Pro lokalizaci je zde použitá knihovna react-intl, díky které je možné jednoduše používat překlady a to nejen textové, ale i číselné a datumové. Součástí toho je zde napsán vlastní systém generování výchozího jazyka a mergování do ostatních. V praxi to znamená, že když mám třeba výchozí angličtinu a přidám další jazyk, tak texty, které jsem v novém jazyce ještě neimplementoval, budou automaticky v angličtině.

Pro komunikaci s backendem je zde GraphQL a to v implementaci přes Apollo (sorry, ale Relay není ta cesta). Současně s tím je zde i malý backend server, který je vlastním GraphQL serverem. Vedle toho jsou zde navíc použity knihovny, které umožňují samotné GraphQL schéma generovat do TypeScriptu a tím pádem máte zadarmo celý API model a to jak na straně frontendu tak i backendu.

Další částí, která zatím není implementována, ale bude co nejdříve, je přihlašování pomocí JWT tokenu. Tedy autentizace přes login a heslo a autoriace přes JWT token.

Poslední důležitou součástí je příprava pro psaní testů pomocí knihovny Jest. Vedle toho je navíc integrace se službou Coveralls, která na základě výsledků testů provádí analýzu a říká nám, na kolik procent je náš kód pokrytý testy.

Nasazení do Cloudu


Pokud jsem na začátku zmínil, že jsem vytvořil základní stack pro vývoj React aplikací, tak je to jen 50% toho, co jsem udělal. Druhou částí je samotný způsob, jak aplikace funguje z pohledu DevOps.

Pokud to zjednoduším, tak Alda funguje tak, že pokud udělám přes GitHub nový release, tak se automaticky vytvoří Docker image, který se nasadí v Google Cloudu do Kubernetes clusteru přes Helm. Pokud nasadím aplikaci na novou doménu, tak automaticky vytvoří SSL certifikát přes Lets Encrypt. Je prostě cool, když nakonec jedním příkazem nasadíte celý ekosystém, který je škálovatelný. Jediné omezení je pak Vaše peněženka, ze které si musíte zaplatit tolik serverů, kolik uznáte za vhodné.

Celý proces nasazení je popsán v definici pro CircleCI. Ano, Alda používá CircleCI, což je jeden z nejlepších nástrojů pro CI/CD současnosti.

Plány do budoucna?


Na to je jednoduchá odpověď a tím jsou GitHub issues: https://github.com/ApiTreeCZ/alda/issues.

Jedna z věcí, kterou je třeba provést je samotný refactoring. Více implementovat vzory jako třeba: "Higher-Order Components" apod.

Pokud byste něco nového chtěli přidat, neváhejte poslat požadavek :)

Závěr


Hlavním důvodem, proč jsem chtěl sepsat tento článek není ani tak to, že bych chtěl naše řešení nějak propagovat, ale spíše pomoci ostatním, který řeší podobné problémy jako my. Ať už se jedná třeba o spojení material-ui s Next.js či o nasazení do Kubernetes.

Někdy příště popíši blíže celý projekt z pohledu samotného vývoje.

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...