Mục tiêu của bài này là:

  • Giải thích vì sao lại không nên mutate state.
  • Giải thích vì sao lại không nên sử dụng cloneDeep khi muốn update state.

1. Vì sao không nên mutate state

Ví dụ về mutate state:

import React, { Component } from 'react';

class App extends Component {
   constructor(props) {
    super(props)
    this.state = {
      obj: {
        x: {y: 100},
        a: {b: 2}
      }
    }
  }

  handleButtonClick = () => {
    const { obj } = this.state
    let newObj = obj
    newObj.a.b++
    this.setState({obj: newObj})
  }

  render() {
    return (
      <div>
        <h1>{this.state.obj.a.b}</h1>
        <button onClick={this.handleButtonClick}>Click me</button>
      </div>
    )
  }
}

export default App;

JavaScript

Ở ví dụ trên, khi bạn click vào Click me, hàm handleButtonClick sẽ thực thi và tăng giá trị của thuộc tính obj.a.b thêm 1.

Bạn có thể copy đoạn code trên vào đây để test thử: https://repl.it/languages/reactjs

Trong hàm handleButtonClick ta đã gán biến newObj = obj rồi gán newObj.a.b++ . Việc này khiến cho obj trong this.state cũng bị thay đổi trực tiếp, nên ta đã vô tình mutate state.

Vì sao lại không nên mutate state?

Bởi vì trong react component có một hàm quyết định có render lại hay không, đó là hàm shouldComponentUpdate. Hàm này sẽ nhận vào props và state mới rồi so sánh chúng với propsstate cũ để quyết định có rerender không.

Mặc định kết quả trả về của hàm này trong React Component là true nên mỗi khi bạn gọi setState hay props truyền xuống thay đổi thì hàm render() luôn được gọi. Nếu bạn muốn render chỉ được thực thi với điều kiện xác định nào đó, bạn có thể tùy biến hàm này như sau:

mport React, { Component } from 'react';

class App extends Component {
   constructor(props) {
    super(props)
    this.state = {
      obj: {
        x: {y: 100},
        a: {b: 2}
      }
    }
  }

  shouldComponentUpdate(nextProps, nextState) {
    const nextObj = nextState.obj
    const currentObj = this.state.obj
    // NOTE: tôi muốn render lại khi và chỉ khi obj.a.b trong
    // state mới khác với obj.a.b trong state hiện tại (state cũ)
    return nextObj.a.b !== currentObj.a.b
  }

  handleButtonClick = () => {
    const { obj } = this.state
    let newObj = obj
    newObj.a.b++
    this.setState({obj: newObj})
  }

  render() {
    return (
      <div>
        <h1>{this.state.obj.a.b}</h1>
        <button onClick={this.handleButtonClick}>Click me</button>
      </div>
    )
  }
}

export default App;

JavaScript

hàm shouldComponentUpdate phía trên sẽ trả về false chứ không phải trả về true khiến hàm render không được gọi lại.

Đó là bởi vì khi ta gán newObj.a.b++, giá trị của b trong obj.a cũng đã bị thay đổi theo (xem bài này)

Ví dụ trên có thể đơn giản đối với nhiều người và họ nhận ra ngay là vốn dĩ hàm shouldComponentUpdate đã trả về false rồi, nhưng trong các dự án thực tế, các state thường có nhiều tầng giá trị và truyền đi nhiều component khác nhau. Nếu ta vô tình mutate trong một component nào đó làm component khác không hoạt động theo ý muốn thì sẽ rất khó để debug ra lỗi ở đâu. Vì vậy nên phải tránh mutate state.

Để giải quyết vấn đề trên, nhiều lập trình viên đã xử lý bằng cách cloneDeep biến, xử lý với biến mới đó rồi lại update biến mới đó vào state.

2. Vì sao không nên sử dụng cloneDeep

import React, { Component } from 'react';

class App extends Component {
   constructor(props) {
    super(props)
    this.state = {
      obj: {
        x: {y: 100},
        a: {b: 2}
      }
    }
  }

  handleButtonClick = () => {
    const { obj } = this.state
    // NOTE: cloneDeep sử dụng JSON.parse(JSON.stringify())
    let newObj = JSON.parse(JSON.stringify(obj))
    // Hoặc dùng cloneDeep trong lodash như sau:
    // let newObj = cloneDeep(obj)
    newObj.a.b++
    this.setState({obj: newObj})
  }

  render() {
    return (
      <div>
        <h1>{this.state.obj.a.b}</h1>
        <button onClick={this.handleButtonClick}>Click me</button>
      </div>
    )
  }
}

export default App;

JavaScript

Nhưng với cách làm trên, ta đang lãng phí bộ nhớ, hiệu năng của chương trình. Với một app nhỏ, component không có dữ liệu phức tạp thì không vấn đề gì nhưng với app lớn và tài nguyên máy hạn chế thì ta không nên làm như trên.

https://reactjs.org/docs/update.html#the-main-idea :

Should not use cloneDeep in react
Ko nên dùng cloneDeep trong react
Dont use cloneDeep in react

Chúng ta đều đã biết dùng cloneDeep như trên là không hay nhưng vì sao nó lại không hay?

Nhiều người nghĩ rằng để không mutate state thì cứ phải cloneDeep toàn bộ state rồi xử lý trên state copy đó, nhưng bản chất không phải như vậy.

Bạn chỉ cần làm sao cho mọi địa chỉ của các dữ liệu trong state cũ không bị thay đổi là được.

Ví dụ như ở đoạn code trên, bạn phải đảm bảo làm sao để địa chỉ của obj, x, y, a, b phải giữ nguyên khi thao tác với newObj.

Ở đây dùng cloneDeep sẽ copy hoàn toàn giá trị của obj, x, y, a, b sang các địa chỉ khác và gán vào newObj, điều này đúng nhưng không tối ưu vì chúng ta chỉ thay đổi giá trị của b trong a trong obj mà thôi, ta không đả động gì đến x và y trong obj thì sao không tận dụng lại x và y đã có?

3. Sử dụng immutability-helper

Để khắc phục nhược điểm của cloneDeep, react khuyên nên sử dụng immutability-helper:

import React, { Component } from 'react';
import update from 'immutability-helper';

class App extends Component {
   constructor(props) {
    super(props)
    this.state = {
      obj: {
        x: {y: 100},
        a: {b: 2}
      }
    }
  }

  shouldComponentUpdate(nextProps, nextState) {
    const nextObj = nextState.obj
    const currentObj = this.state.obj
    return nextObj.a.b !== currentObj.a.b
  }

  handleButtonClick = () => {
    const { obj } = this.state
    const newValue = obj.a.b + 1
    const newObj = update(obj, {a: {b: {$set: newValue}}})
    this.setState({obj: newObj})
  }

  render() {
    return (
      <div>
        <h1>{this.state.obj.a.b}</h1>
        <button onClick={this.handleButtonClick}>Click me</button>
      </div>
    )
  }
}

export default App;

JavaScript

Cú pháp của immutability-helper khá giống với mongodb. Khi muốn thay đổi một giá trị nằm trong object thì sử dụng $set.

Bạn để ý tới câu lệnh: 

const newObj = update(obj, {a: {b: {$set: newValue}}})

JavaScript

Cú pháp câu lệnh này dễ gây hiểu lầm là ta chỉ thay đổi địa chỉ của b (để trỏ tới giá trị mới newValue) còn địa chỉ của a, obj, x, y thì vẫn giữ như cũ. Nhưng không phải, nếu địa chỉ của a, obj mà không thay đổi thì rõ ràng thuộc tính b trong a thay đổi thì chẳng phải đã khiến a, obj bị mutate rồi sao.

Vì thế nên mới nói “Nếu mọi thứ bên trong vẫn giữ nguyên thì hãy để địa chỉ giữ nguyên“, còn “Nếu có cái gì đó bên trong thay đổi thì phải thay đổi tất cả những đối tượng bọc ngoài nó“.

Ở trên thì vì b thay đổi và b nằm trong a và obj nên địa chỉ của a và obj giờ cũng phải trỏ sang chỗ khác.

Câu lệnh update ở trên đã làm điều đó cho chúng ta. Điều đặc biệt ở đây là câu lệnh trên sử dụng lại được địa chỉ x và y trong obj nên tận dụng được hiệu năng và bộ nhớ.

Mô tả bằng hình ảnh sẽ như sau: addr(x) viết tắt cho address of x

  • obj ban đầu sẽ có các địa chỉ như sau:
state address in react
  • Khi sử dụng cloneDeep thì các object mới tạo thành sẽ có địa chỉ như sau:
state address in react
  • Khi sử dụng immutability-helper thì các object mới sẽ như sau:
state address in react

Như bạn thấy, đúng với mục đích là không được thay đổi bất cứ địa chỉ của bất cứ thuộc tính nào trong obj và cả địa chỉ của obj.

Link tham khảo:

https://reactjs.org/docs/react-component.html#shouldcomponentupdate

https://reactjs.org/docs/update.html

https://medium.freecodecamp.org/handling-state-in-react-four-immutable-approaches-to-consider-d1f5c00249d5

Nguồn: blog.daovanhung.com