Khi làm việc với React, hẳn bạn đã nghe đến tính bất biến của dữ liệu (immutability). Đặc tính này, nói một cách đơn giản, là khả năng giá trị của dữ liệu không bị thay đổi sau khi đã được khai báo. Tính bất biến giúp cho chương trình trở nên dễ dự đoán, ít xảy ra lỗi và trong một số trường hợp còn tăng hiệu suất của ứng dụng. Do đó, React (và Redux) đều khuyến khích mọi người viết code để hướng đến đặc tính này.

Tuy vậy, nếu mới làm quen với JavaScript và React, có thể bạn sẽ bỡ ngỡ không biết làm thế nào cho “chuẩn nhất”. Bài viết dưới đây giới thiệu một số kỹ thuật giúp bạn hướng đến immutability một cách dễ dàng hơn. Chúng ta sẽ nói về hàm thuần khiết, các thao tác xử lý dữ liệu trên mảng và object. Cuối cùng, bạn có thể áp dụng ngay những “tuyệt chiêu” vừa học để xây dựng một demo nhỏ bằng React.

Lưu ý: kí hiệu ? được sử dụng trong bài viết để thể hiện đây là kết quả/hiệu ứng mong muốn.

Chúng ta bắt đầu thôi!

Luôn dùng const khi khai báo dữ liệu

Lời khuyên đầu tiên và cũng là căn bản nhất, luôn dùng const khi khai báo. Chắc bạn cũng đã biết, let và const được giới thiệu từ phiên bản ES6, cho phép khai báo biến có tầm vực theo khối và không thực hiện hoisting. Điểm khác biệt giữa let và const là bạn có thể thay đổi giá trị của biến khai báo với let, trong khi không thể với const.

let foo = 1;
foo = 2 // Không thành vấn đề
 
const bar = 1;
bar = 2;// Error: Assignment to constant variable.

Do đó, trong hầu hết các trường hợp bạn nên khai báo bằng const để tránh xảy ra lỗi khi giá trị của khai báo bị thay đổi bất ngờ. Cũng cần lưu ý là khi khai báo objects với const, mặc dù bạn không thể gán giá trị mới cho object nhưng giá trị của các thuộc tính vẫn có thể bị thay đổi.

const obj = { name: 'foo' };
obj = { name: 'bar' }; // Error: Assignment to constant variable.
 
// Nhưng bạn có thể...
obj.name = 'bar';
console.log(obj); // { name: 'bar' }

Thao tác trên object

Như vậy để thay đổi giá trị của object mà vẫn đảm bảo tính chất bất biến, chúng ta cần sử dụng Object.assign(target, ...sources). Hàm này có tác dụng sao chép thuộc tính của các đối tượng sources vào target. Ví dụ như:

const a = { name: 'foo' }
const b = Object.assign({}, a, { name: 'bar', age: 1 }, { id: 9 })
console.log(b) // { name: 'bar', age: 1, id: 9 }

Cần lưu ý để đảm bảo tính bất biến thì tham số target nên luôn là {}, vì khi đó các giá trị của sources sẽ được sao chép vào đối tượng mới. Một cách dùng sai là:

const a = { name: 'foo' }
const b = Object.assign(a, { name: 'bar', age: 1 }, { id: 9 })
console.log(b) // { name: 'bar', age: 1, id: 9 }
console.log(a) // Giá trị của a cũng bị thay đổi thành { name: 'bar', age: 1, id: 9 }
console.log(a === b) // true

Ngoài Object.assign(), bạn cũng có thể dùng cú pháp spread cho object. Ví dụ:

Lưu ý là cú pháp này hiện vẫn đang được đề xuất và chưa được hỗ trợ trên hầu hết các trình duyệt. Nếu muốn xài thì bạn phải dùng Babel hay Bublé để chuyển đổi nhé.

Một số thao tác thường gặp khác trên object

Lấy tên các thuộc tính của một object

const obj = { name: 'bar', age: 1, id: 9 }
Object.keys(obj) // ['name', 'age', 'id']

Lấy giá trị của các thuộc tính của một object

Lấy cặp [thuộc tính, giá trị] của một object

Thêm một phần tử vào mảng

Xóa một phần tử ở đầu mảng, cuối mảng hay ở bất cứ vị trí nào

Hàm add() được xem là thuần khiết vì add(2, 3) luôn trả về giá trị 5. Hàm Math.random() thì ngược lại, vì giá trị trả về luôn thay đổi. Bên cạnh đó, addAndShow() cũng bị xem là không thuần khiết, vì nó in kết quả ra console.

Do đó khi làm việc với React, bạn nên xem render() như một hàm thuần khiết với dữ liệu đầu vào là this.props. Hạn chế viết logic bên trong hàm render() mà thay vào đó nên đưa các bước thao tác dữ liệu thành từng phương thức riêng.

Không nên

class Cart extends React.Component {
  render() {
    let total = 0
    for (let i = 0; i < this.props.cart.products; i++) {
      total = this.props.cart.products[i].price + total
    }
 
    return (
      <div>
        <h1>Cart total: {total}</h1>
        <ProductList products={this.props.sort((a, b) => b.price - a.price)} />
      </div>
    )
  }
}

Nên

class Cart extends React.Component {
  getSortedProducts(cart) {
    return [..cart.products].sort((a, b) => b.price - a.price)
  }
 
  getCartTotal(cart) {
    return cart.products.reduce((acc, product) => acc + product.price, 0)
  }
 
  render() {
    const { cart } = this.props
 
    const total = this.getCartTotal(cart)
    const sortedProducts = this.getSortedProducts(cart)
 
    return (
      <div>
        <h1>Cart total: {total}</h1>
        <ProductList products={sortedProducts} />
      </div>
    )
  }
}

Hoặc thay vì sử dụng class components, bạn có thể dùng functional components, như kiểu: