pondělí 26. června 2017

React a Typescript podruhé - proklaté this

V předchozím článku jsem ukázal jak používat Typescript v Reactu. Dneska se pojďme podívat na zbylou část, která se bude motat kolem klíčového slova this.

Javascript a this

Spoléhání na this v javascriptu, je stejné, jako hrát ruskou ruletu. Do funkce lze totiž ono this podstrčit a tím změnit kontext. Do toho se ještě zamíchá function.bind, use strict a arrow functions a už z toho máme slušný guláš.

Dnešní článek se bude snažit vám odhalit, jak to s tím samotným this vlastně je a proč je v JSX bind(this) tou nejhorší možnou volbou.

Funkce a this

Lepší, než tisíc slov, je vždy malá ukázka. Pokud máme následující kód, zkuste uhádnout, co bude jeho výsledkem:
function test() {
    return this;
}
console.log(test());

Pokud tento příklad spustíte ve webu, bude dané this obsahovat globální objekt window. Pokud daný kód spustíte v node.js, bude obsahovat globální objekt global. O tom, co je globální objekt, se můžete dočíst zde.

Nyní pojďme náš kód trochu upravit:
function test() {
    'use strict';
    return this;
}
console.log(test());

V současné chvíli bude this undefined. Proč tomu tak je? Důvodem je přidání strict módu. Zjednodušeně se dá říci, že strict mód nám modifikuje chování javascriptu. Tento strict mod vznikl v ES5 a pokud vás zajímá, co všechno modifikuje, můžete si danou věc pročíst zde.

Pojďme si ukázat další variantu:
function test() {
    return this;
}
console.log(test.bind({invasion:'Hello'})());

Výsledkem bude, že this bude obsahovat podstrčený objekt {invasion: 'Hello'}. Toto je přesně ta ukázka, kterou ve svém projektu nechceme používat, ač je to častý jev. Důvod proč, rozeberu v následující ukázce:
import * as React from 'react';

interface Props {
    onClick: (origin: string) => void;
}

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

    handleOnClick() {
        this.props.onClick('From button component');
    }
    
    render() {
        return (
            <button onClick={this.handleOnClick.bind(this)}>Click me!</button>
        )
    }
}

Co zde vidíme? Máme komponentu s názvem Button, která vrací callback s názvem onClick. Pokud bychom v JSX nepoužili this.handleOnClick.bind(this), tak by naše funkce nemohla použít this.props.onClick(origin). Jak jsme si ukázali výše, tak this je ve funkci buď globální objekt, nebo undefined. Podle toho, zda jsme v režimu strict či nikoli.
Ono by na tomto příkladě nic tak špatného nebylo, až na to, že pokud si uvědomíme, jak funguje React a metoda render, tak zjistíme, že jsme napsali dost neefektivní kód. Metoda render se totiž bude vykonávat velice často a to vždy, když je komponenta poprvé renderována a také ve chvíli, kdy jí předek změní props či se modifikuje state. To od Reactu přeci očekáváme, že takto bude fungovat. Nicméně v případě metody bind se dostáváme k tomu, že vlastně znovu a znovu a znovu vytváříme další funkci. Finále je, že jen dost zatěžujeme Garbage Collector, který musí znovu a znovu zahazovat něco, co jsme vytvořili, aniž bychom o tom věděli.

Jak z toho ven?

První variantou je to, že bind aplikujeme v konstruktoru. Je to efektivní způsob, jak se vyhnout tomu, aby nám nevznikalo příliš funkcí, ale také to je způsob, který bych označil spíše za nedoporučovaný. Důvod proč, je zřejmý. V případě nové handler funkce musíte provést její registraci v konstruktoru.

Pojďme se podívat na ukázku:
import * as React from 'react';

interface Props {
    onClick: (origin: string) => void;
}

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

    constructor(props: Props, context: any) {
        super(props, context);
        this.handleOnClick = this.handleOnClick.bind(this)
    }

    handleOnClick() {
        this.props.onClick('From button component');
    }

    render() {
        return (
            <button onClick={this.handleOnClick}>Click me!</button>
        )
    }
}

Paráda. Sice jsme se zbavili bind v JSX, ale že by to byla taková výhra se říct nedá.

Druhou a lepší variantou jsou arrow functions. Než se dostaneme k výslednému řešení, pojďme se podívat jak to je s this v arrow functions.

Arrow functions a this

Arrow functions byly přidány v ES6 a dá se říci, že díky nim se nám více přiblížilo funkcionální programování. Arrow functions zjednodušují zápis a také mají jednu důležitou vlastnost a tou je právě zmíněné this.

Pojďme se podívat na ukázku:
const test = () => this;
console.log(test());

Skvěle. Máme stejnou funkci test, akorát přepsanou pomocí arrow function. Co bude jejím výsledkem? Bude to prázdný objekt {}. Skvělé na zamotání hlavy :)

Arrow functions mají v this současný kontext. Abychom to lépe pochopili, pojďme se podívat ještě na jednu ukázku:
this.user = {
    id: 1,
    firstName: 'Ales',
    lastName: 'Dostal',
};
const test = () => this;
console.log(test());

Výsledkem bude, že this obsahuje objekt s atributem user.

Nyní si pojďme tuto znalost aplikovat na React komponentu user.

Výsledkem bude následující kód:
import * as React from 'react';

interface Props {
    onClick: (origin: string) => void;
}

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

    handleOnClick = () => {
        this.props.onClick('From button component');
    };

    render() {
        return (
            <button onClick={this.handleOnClick}>Click me!</button>
        );
    }
}

Díky arrow functions již nepotřebujeme ani konstruktor, ani funkci bind.

Závěr

O this v javascriptu je napsáno spoustu článků. Existují i nové návrhy, které by mohly více přispět tomu, abychom se samotnému bind vyhnuli. Asi nejblíže tomu je samotný návrh pomocí dokorátoru @autobind, což jsou v podstatě metadata nad funkcemi, které určují, jak se budou v daném kontextu chovat. Jestli budou dekorátory, které se v nové ECMA specifikaci připravují, tou správnou volbou, to ukáže čas.

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