Định nghĩa Function trong React
Bài đăng này đã không được cập nhật trong 5 năm
1. Mở đầu
Chắc hẳn khi làm việc với React
chúng ta không còn lạ lẫm gì với việc khai báo các function và gán nó vào các sự kiện (event) trên giao diện của chúng ta. Bài viết sau đây của mình sẽ bàn về các cách mà chúng ta có thể sử dụng để định nghĩa function cũng như một số ưu, nhược điểm của chúng
2. Function
Giả sử ta có 1 class component như sau:
import ReactDOM from "react-dom";
import React, { Component } from "react";
const tasks = [
{ id: 1, content: "Write Viblo Post" },
{ id: 2, content: "Do 15000 push up" },
{ id: 3, content: "Fix some bugs" }
];
class App extends Component {
render() {
return (
<div>
<h1>TASK</h1>
{tasks.map(item => (
<div key={item.id}>
Task name:
<strong>
{item.content}
</strong>
</div>
))}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
Với đoạn code trên thì đây là những gì mà chúng ta nhận được trên giao diện:
Bây giờ giả sử chúng ta muốn mỗi khi chúng ta click vào tên một task (phần chữ in đậm) thì sẽ tiến hành alert phần nội dung đó in đậm đó lên. Để làm được điều này thì tất nhiên chúng ta sẽ cần một định nghĩa một function
và gán nó vào sự kiện onClick
của task đó. Sau để mình sẽ điểm lại một số cách mà bạn có thể sử dụng như sau:
a. Inline Function
Với cách này thì chúng ta sẽ viết lại như sau:
class App extends Component {
render() {
return (
<div>
<h1>TASK</h1>
{tasks.map(item => (
<div key={item.id}>
Task name:
<strong onClick={() => { alert(item.content) }>
{item.content}
</strong>
</div>
))}
</div>
);
}
}
Sau đó ta thử bấm vào phần nội dung của task thì sẽ thu được kết quả như mong muốn:
Tuy nhiên với cách làm như trên thì no sẽ sinh ra một vấn đề đó là môi khi hàm render()
được gọi thì nó sẽ phải tạo lại cái anonymous function
kia của chúng ta sau đó mới gán lại vào sự kiện onClick
mà bạn khai báo. Vậy nên đây là cách làm không hợp lý chút nào.
b. Arrow Function
Với cách làm này chúng ta sẽ định nghĩa phần alert thành một function mới như sau:
class App extends Component {
handleAlert = e => {
alert(e.target.innerHTML)
}
render() {
return (
<div>
<h1>TASK</h1>
{tasks.map(item => (
<div key={item.id}>
Task name:
<strong onClick={this.handleAlert}>
{item.content}
</strong>
</div>
))}
</div>
);
}
}
Với cách làm này ta cũng thu được kết quả tương tự như trên đồng thời tránh được việc hàm của chúng ta bị "tạo lại" thay vì "sử dụng lại" như mình đã đề cập ở phía trên. Đồng thời bằng việc sử dụng arrow function
như trên ta còn có thêm một lợi thế nữa đó là bên trong function nếu ta sử dụng this
thì nó sẽ tự biết this
chính là instance của component hiện tại và vì thế ta có thể sử dụng các hàm như this.setState()
, this.props
, this.state
, ... . Tuy vậy ở đây, trong trường hợp chúng ta muốn khi alert id của task đó thì sẽ chỉ có duy nhất một cách đó là tại phải thêm dữ liệu custom vào thẻ <strong>
của chúng ta vì mặc định khi dùng arrow function
như trên thì ta chỉ có thể sử dụng một biết duy nhất đó là e
hay event
như ở trên. Trong trường hợp bạn cố tình viết như sau:
<strong onClick={this.handleAlert(item.id)}>
{item.content}
</strong>
Thì sẽ dẫn đến function của chúng ta được gọi ngay lập tức khi lần đầu render()
đây hoàn toàn không đúng với những gì ta mong muốn vì với cách viết this.handleAlert(item.id)
chính xác là lợi gọi hàm chứ không phải việc bán đang gán hàm đó vào sự kiện onClick
nếu trên thực tế bạn viết như vậy sẽ gây ra bug cho dự án. Nên trong trường hợp bận muốn truyền thêm biến như tên thì đây sẽ không phải là cách hay. TUy nhiên ta vẫn có thể "hack"
như sau như mình đã nói ở trên như sau:
class App extends Component {
handleAlert = e => {
alert(e.target.dataset.id)
}
render() {
return (
<div>
<h1>TASK</h1>
{tasks.map(item => (
<div key={item.id}>
Task name:
<strong data-id={item.id} onClick={this.handleAlert}>
{item.content}
</strong>
</div>
))}
</div>
);
}
}
Tuy nhiên với mình cách này không hay lắm vì ta vẫn cứ mất công viết tuần tự e.target
rồi mới đến các dữ liệu kia và đồng thời ta lại phải tạo thêm custom attribute
cho thẻ của chúng ta. Sẽ hay hơn nếu ta có thể viết dưới dạng this.handleAlert(item.id)
.
c. Bind Function
Có lẽ đẫy sẽ là cách tạo function đầu tiên mà bạn biết được khi tiếp cận với React
vì theo mình thấy trên docs của React
cũng có hướng dẫn ta viết như vậy trong trường hợp ta muốn truyền thêm biến:
class App extends Component {
handleLogContent(index, e) {
console.log(index, e.target.innerHTML);
}
render() {
return (
<div>
<h1>TASK</h1>
{tasks.map((item, index) => (
<div key={item.id}>
Task name:
<strong onClick={this.handleLogContent.bind(this, item.id)}>
{item.content}
</strong>
</div>
))}
</div>
);
}
}
Với cách viết như sẽ vừa truyền được thêm biến bất kì, vừa sử dụng được e
và tất nhiên cũng sử dụng được từ khóa this
như arrow function
mà ta nói trên. Nhưng như bạn thấy điều này khiến việc ta gán function này vào event sẽ phải viết khá dài dòng vì phải thêm cả đoạn bind(this, ...)
. Với trường hợp bạn không cần truyền thêm biến thì sẽ có 2 trường hợp như sau:
- Không sử dụng từ khóa
this
: Với trường hợp này thì ta có thể viết như bình thường mà không cầnbind
gì cả:
class App extends Component {
handleLogContent(e) {
console.log(e.target.innerHTML);
}
render() {
return (
<div>
<h1>TASK</h1>
{tasks.map((item, index) => (
<div key={item.id}>
Task name:
<strong onClick={this.handleLogContent}>{item.content}</strong>
</div>
))}
</div>
);
}
}
Lưu ý: trong trường hợp function mà bạn tạo ra không cần sử dụng đến this
thì bạn sẽ không cần phải bind nó
Tuy nhiên trong trường hợp giả sử ta dùng từ khóa this
như sau:
class App extends Component {
handleLogContent(e) {
console.log(this)
console.log(e.target.innerHTML);
}
render() {
return (
<div>
<h1>TASK</h1>
{tasks.map(item => (
<div key={item.id}>
Task name:
<strong onClick={this.handleLogContent}>{item.content}</strong>
</div>
))}
</div>
);
}
}
Thì ta sẽ thu được this
là undefined
chú không phải là 1 App
component.
- Sử dụng
this
: Với trường hợp này thì ta sẽ phải tiến hànhbind
các function mà ta muốn sử dụng từ khóathis
trong đó:
class App extends Component {
constructor(props) {
super(props);
this.handleLogContent = this.handleLogContent.bind(this);
}
handleLogContent(e) {
console.log(this)
console.log(e.target.innerHTML);
}
render() {
return (
<div>
<h1>TASK</h1>
{tasks.map(item => (
<div key={item.id}>
Task name:{" "}
<strong onClick={this.handleLogContent}>{item.content}</strong>
</div>
))}
</div>
);
}
}
Lúc này thì this
sẽ đúng là App
component của chúng ta:
App {props: Object, context: Object, refs: Object, updater: Object, handleLogContent: function bound handleLogContent()…}
Nhưng có một lưu ý là nếu bạn muốn truyền thêm biến thì bắt buộc phải bind ở trong phần gán hàm:
onClick={this.handleLogContent.bind(this, item.id)}
Nói chung với các cách sử dụng bind
như trên ta hoàn toàn có thể xử lý được hết các trường hợp như sử dụng this
trong nội dung, truyền thêm biến vào function.
d. Khác nhau giữa arrow function và bind function
Như bạn thấy ở trên thì ngoài trường hợp muốn truyền thêm tham số vào hàm khi render
thì chúng ta có thể tùy chọn sử dụng giữa 2 cách:
class App extends React.Component {
handleEvent = () => {
// do something
}
}
// hoặc
class App extends React.Component {
constructor(props) {
super(props);
this.handleEvent = this.handleEvent.bind(this)
}
handleEvent() {
// do something
}
}
Cả hai cách viết trên khi chạy sẽ cho ta kết quả giống nhau tuy nhiên có một sử khác biệt như sau, giả sử ta có component Demo
và component App
để gọi đến Demo
như sau:
import React, { Component, Fragment } from 'react';
class Demo extends Component {
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('OK');
}
handleClickArrow = () => {
console.log('OK');
}
render() {
return (
<h1>DemoComponent</h1>
)
}
}
class App extends Component {
render() {
console.log(Demo.prototype);
return (
<Demo />
)
}
}
Ở đây khi bạn nhìn vào phần prototype
của component Demo
phần console.log()
sẽ thu được kết quả như sau:
Component {constructor: ƒ, handleClick: ƒ, render: ƒ}
constructor: class Demo
handleClick: ƒ handleClick()
isMounted: (...)
render: ƒ render()
replaceState: (...)
__proto__: Object
Nếu bạn để ý thì sẽ thấy trong chuỗi kế thừa prototype
của component Demo
của chúng ta sẽ chỉ có hàm handleClick
mà không có hàm handleClickArrow
. Theo mình tìm hiểu trong trường hợp ta có N instance của component Demo
và trong đó ta gọi đến hàm handleClick
trên mỗi instance thì vì hàm này được định nghĩa trong chuỗi prototype
nên nó sẽ được chia sẻ giữa các instance của component này. Tuy nhiên với trường hợp của handleClickArrow
thì với mỗi instance của component Demo
của chúng ta được tạo ra thì nó sẽ đồng thời tạo lại function này vì khi dùng arrow function
nó sẽ được khởi tạo như sau:
class App extends React.Component {
handleEvent = () => {
// do something
}
}
// Khi đi qua complie ra nó sẽ thành dạng
class App extends React.Component {
constructor(props) {
super(props);
this.handleEvent = () => {
console.log('OK');
};
}
Nghĩa là nó sẽ được khởi tạo lại trong contructor
nên sẽ giống như thuộc tính riêng của từng instance. Điều này sẽ làm ảnh hưởng đến performance của bạn trong trường hợp bạn có 1 danh sách 1000 component Demo đồng nghĩa với việc nó sẽ tạo ra 1000 hàm handleClickArrow
. Tóm lại với arrow function
và bind
khi chạy lên nó sẽ là
- 1000 component Demo
- => 1000
arrow function
được tạo - => 1
bind
function
- => 1000
Tuy nhiên bạn chỉ lo sợ vấn đề performance khi mà có đến 1000 component thôi nhé còn nếu chỉ có 1,2 thì có lẽ nó sẽ không gây ra quá nhiều vấn đề. Bạn có thể tham khảo thêm ở đây https://medium.com/@charpeni/arrow-functions-in-class-properties-might-not-be-as-great-as-we-think-3b3551c440b1.
3. Kết bài
Mong rằng qua bài viết của mình sẽ giúp các bạn hiểu hơn về việc định nghĩa function trong React và có thể kết hợp các cách nói trên trong công việc hằng ngày một cách hiệu quả.
All rights reserved