React + ES6 = Autobind does not work

Posted in Programming on December 14, 2016 by manhhomienbienthuy Comments
React + ES6 = Autobind does not work

React đã loại bỏ tính năng "autobinding" với các class component sử dụng cú pháp EcmaScript 2015 (ES6). Vì vậy, những cách truyền hàm như kiểu onClick={this.onClickHandler} sẽ không hoạt động nữa vì hàm onClickHandler không được gán cho đối tượng nào, this trong hàm đó sẽ không thể xác định được. Trong bài viết này, chúng ta sẽ tìm hiểu nguyên nhân cũng như một số biện pháp khắc phục.

ES6 Class

React đã từng sử dụng một phương thức để tạo các component là React.createClass() (tài liệu tham khảo). Hàm này là một tổ hợp các quá trình phức tạp để tạo ra một component mới, trong đó, nó có tính năng "autobinding".

Tuy nhiên, sau khi ES6 được công bố, nó cung cấp cho lập trình viên cú pháp mới cho phép tạo class đúng kiểu lập trình hướng đối tượng hơn, đó là sử dụng keyword Class. Và React cũng hỗ trợ điều đó. Điều này cho phép lập trình viên xây dựng ứng dụng dễ dàng hơn, nhưng kéo theo đó, là tính năng "autobinding" đã không còn nữa. Điều này có thể gây nhiều vấn đề với những người đã quen với autobinding rồi.

Phương thức và hàm

Một phương thức là một hàm được định nghĩa như một thuộc tính của đối tượng nào đó. Một hàm thì độc lập, không được gắn vào đối tượng nào cả. Đây cũng là sự khác biệt giữa phương thức và hàm ở phần lớn những ngôn ngữ lập trình hướng đối tượng. Ở đây, chúng ta chỉ quan tâm đến sự khác biệt đơn giản đó thôi. Hãy tạm thời bỏ qua những yếu tố kỹ thuật khác như trình dịch hiểu phương thức thế nào, hàm ra sao, hàm được lưu thế này, phương thức được lưu thế kia...

Với phương thức, this trong phương thức đó được dùng để tham chiếu đến đối tượng mà phương thức đó thuộc về.

Để hiểu rõ hơn về từ khóa this, bạn có thể tham khảo bài viết trước của tôi. Trong bài viết này, chúng ta chỉ nhắc lại một số điểm cơ bản.

var obj = {
    prop: 'Hello',
    greet: function() {
        console.log(this.prop);
    }
};

obj.greet(); // 'Hello'

Một ví dụ rất cơ bản và dễ hiểu. Nhưng mọi việc sẽ phức tạp hơn nếu chúng ta thay đổi cách tham chiếu phương thức:

var ref = obj.greet;
ref(); // "undefined"

Tại sao lúc này kết quả lại là undefined? Các hàm và cả phương thức trong JavaScript là những đối tượng first-class, có nghĩa là chúng ta có thể sử dụng chúng như những đối tượng thông thường, có thể gán chúng, truyền chúng cho các hàm khác như tham số. Khi chúng ta gán phương thức cho biến khác var ref = obj.greet, chúng ta đã làm mất mối quan hệ của phương thức với đối tượng. Khi chúng ta gọi nó, nó trở thành một hàm, chứ không phải phương thức nữa.

Khi hàm đó gọi đến this, nó không được gán cho đối tượng nào cả, nó sẽ trở nên rất khó để xác định và đó là thứ bạn không bao giờ muốn gặp phải.

obj.greet() vẫn là một phương thức. Cú pháp này của JavaScript báo cho trình dịch biết rằng trong phương thức greet, this chính là obj. Chúng ta chỉ đánh mất quan hệ này khi truyền phương thức cho đối tượng khác.

Bind this cho hàm

Một cách làm thông thường được sử dụng để gán this cho các hàm được truyền đi, đó là sử dụng bind như ví dụ sau:

var obj = {
    prop: 'Hello',
    greet: function() {
        console.log(this.prop);
    }
};

var newFunc = obj.greet.bind(obj);
newFunc(); // 'Hello'

Trong ví dụ trên, bind đã giúp chúng ta định nghĩa lại this trong hàm newFunc, khiến nó vẫn tham chiếu đến đối tượng cũ chứ không bị mất đi mối quan hệ này. Một điểm lưu ý rằng method.bind(this) chưa thực thi phương thức đó, mà nó chỉ tạo ra một hàm mới, trong đó this được định nghĩa lại theo cách của chúng ta. Khi hàm được gọi, this sẽ có giá trị của tham số trong bind.

Những điều này liên quan gì đến React?

Rất nhiều phương thức trong React vẫn hoạt động tốt mà không cần đến những thức phức tạp ở trên. Nhưng rắc rối sẽ phát sinh nếu chúng ta truyền phương thức ra ngoài đối tượng (mà điều này thường xuyên xảy ra). Hãy xem xét một component ví dụ như sau:

import React from 'react';

export default class ExampleComponent extends Component {
    onClickHander() {
        this.setState({clicked: true});
    }

    render() {
        return (
            <div onClick={this.onClickHandler} />
        );
    }
}

Đoạn code trên không hề có lỗi cú pháp, nhưng component sẽ không hoạt động đúng. Khi chúng ta click, chúng ta sẽ gặp lỗi

this.setState is not a function

Lý do rất đơn giản, khi this.onClickHandler được gọi, nó được gọi như một hàm chứ không phải phương thức, mặc dù chúng ta đã sử dụng cú pháp rất giống với việc gọi một phương thức. Vì là một hàm, lúc này this không còn là đối tượng mà phương thức được định nghĩa nữa, nó có thể là window hoặc undefined nếu sử dụng strict mode.

Một số giải pháp

Sử dụng bind khi render

Đây là cách đơn giản nhất:

render() {
    return (
        <div onClick={this.onClickHandler.bind(this)} />
    )
};

Ưu điểm:

  • Rất dễ dùng
  • Dễ dàng chuyển đối từ version cũ (sử dụng createClass) sang cú pháp mới

Nhược điểm:

  • Mỗi khi render được gọi, bind sẽ tạo ra một hàm mới (chiếm một vùng bộ nhớ mới). Sẽ có nhiều rác trong bộ nhớ nếu render được gọi nhiều lần. Tất nhiên, điều này chưa phải vấn đề lớn lắm, performance chưa bị ảnh hưởng nhiều, trừ khi bạn lập trình game bằng React và update state liên tục.
  • Bạn sẽ phải lặp đi lặp lại this.onClickHandler.bind(this) mỗi khi bạn cần đến onClickHandler trong render.

Sử dụng arrow function trong JSX

Đây cũng là một cách làm tương đối đơn giản

render() {
    return (
        <div onClick={() => this.onClickHandler()} />
    )
};

Sự khác biệt ở đây là gì? Đó là khi gọi onClickHandler chúng ta gọi nó là phương thức của this. Nhưng vì sử dụng arrow function, nên hàm này được định nghĩa ngữ cảnh sẵn, không phát sinh this của riêng nó. Vì vậy this vẫn có giá trị là đối tượng của component và mọi thứ sẽ chạy như chúng ta muốn

Ưu điểm

  • Rất dễ dùng
  • Dễ dàng chuyển đối từ version cũ (sử dụng createClass) sang cú pháp mới

Nhược điểm

  • Tương tự như bind, mỗi khi render được gọi, một hàm mới sẽ được tạo ra.
  • Bạn sẽ phải lặp đi lặp lại () => this.onClickHandler() mỗi khi gọi phương thức (dù không dài lắm)

Bind ngay khi khởi tạo

Chúng ta có thể bind sẵn các phương thức ngay từ khi khởi tạo component, ví dụ như sau:

import React from 'react';

export default class ExampleComponent extends Component {
    constructor() {
        super();
        this.onClickHandler = this.onClickHandler.bind(this);
    }
    ...
}

Với cách làm này, mỗi khi chúng ta gọi phương thức, nó đã được bind sẵn rồi. Chúng ta không cần lo đến this của nó nữa.

Ưu điểm:

  • Không cần thay đổi gì trong hàm render
  • Một số thư viện như underscore còn cho phép chúng ta bind nhanh hơn nữa, ví dụ:
_.bindAll(this, 'onClickHandler', ...)

Nhược điểm:

  • Có thể bị quên bind cho một phương thức nào đó
  • Chúng ta cần suy xét cẩn thận phương thức nào cần bind, phương thức nào không
  • Phương thức không thể "hot reload", nghĩa là khi Component được load lại, các phương thức của nó không cần thiết phải thực thi. Chúng ta cần phải bỏ tính năng này đi vì không thực thi lại constructor có thể gây lỗi.

Sử dụng thư viện cho phép autobinding

Một số thư viện như react-autobind hoặc autobind-decorator cho phép chúng ta nhanh chóng bind các phương thức của component một cách tự động. Ví dụ autobind-decorator cho chúng ta cú pháp khá giống với decorator của Python

import React from 'react';
import autobind from 'autobind-decorator';

export default class ExampleComponent extends Component {
    @autobind
    update() {
      ...
    }

    ...
}

Ưu điểm:

  • Rất dễ đọc và dễ hiểu
  • Có thể sử dụng hot reload

Nhược điểm:

  • Cần một số cài đặt với babel
  • Decorator không phải là cú pháp chuẩn của JavaScript, không ai biết tương lai của nó.

Sử dụng arrow function làm class method

Hiện nay ES6 vẫn chưa làm được, nhưng ES7 thì có thể. Chúng ta có thể định nghĩa các class method như là những arrow function. Hiện nay babel đã hỗ trợ cách làm này rồi. Chúng ta hãy xem qua ví dụ sau:

import React from 'react';

export default class ExampleComponent extends Component {
    update = () => {
      ...
    }

    ...
}

Ưu điểm:

  • Phương thức tự động được bind mà không cần thư viện bên thứ 3
  • Có thể hot reload

Nhược điểm:

  • Phương pháp này vẫn chưa được công bố chính thức, thậm chí nó mới là proposal. Nó có thể không được chấp nhận.

Sử dụng phương pháp cũ (createClass)

React vẫn hỗ trợ phương pháp React.createClass() để tạo các component. Nên nếu cần thiết, chúng ta vẫn có thể tiếp tục sử dụng chúng (cho đến khi nào React bỏ nó đi).

Ưu điểm:

  • Không thay đổi gì so với trước đây

Nhược điểm:

  • Cú pháp không đúng chuẩn hướng đối tượng
  • Có thể tương lai sẽ không dùng được nữa

Kết luận

Trong bài viết này, chúng ta đã tìm hiểu về một số khó khăn do cú pháp ES6 không còn autobinding. Nhưng chúng ta hoàn toàn có thể khắc phục được những vấn đề này.

I apologise for any typos. If you notice a problem, please let me know.

Thank you all for your attention.