Some details on React's setState

Khi mới làm quen với React Component, có lẽ API đầu tiên bạn biết đến là setState(). Mỗi React Component có thể có state của riêng mình và để quản lý state của nó thì bạn dùng đến setState(). Bạn dùng setState() như thế này.

class Counter extends React.Component {
  // ...

  incrementCount () {
    this.setState({count : this.state.count + 1});
  }
}

Bạn truyền cho setState() một object với các giá trị bạn muốn update. State mới sẽ được merge vào state cũ. Đó là những gì chúng ta đã biết. Làm việc với React một thời gian, sẽ đến lúc bạn gặp một vài bug đau đầu với nó. Bạn sẽ nhận ra có điều gì đó không đúng. Có điều gì đó chúng ta vẫn chưa biết.

You are not the one who setState

Nếu bạn đã quen với cách làm việc truyền thống với DOM Element, bạn sẽ biết rằng mỗi lần bạn setAttribute của một element nào đó thì nó sẽ có tác dụng ngay lập tức. Tuy nhiên khi làm việc với React, mọi thay đổi của DOM Element đều do React quyết định thông qua lifecycle của React Component. Khi bạn setState, cũng là do React quyết định sẽ thay đổi state của nó thế nào luôn.

Để đảm bảo element được render đúng với state, React sẽ không cho phép thay đổi state trong khi đang render. Khi bạn gọi setState thì React đã bắt đầu vòng update rồi. Vậy nên khi bạn gọi setState nhiều lần trong 1 vòng update, những gì thật sự diễn ra có thể không giống như bạn nghĩ.

setState() is asynchronous

Trong document của React, phần State and Lifecycle có nói như thế này

React may batch multiple setState() calls into a single update for performance.

Nghĩa là nếu trong cùng một vòng update bạn gọi setState() nhiều lần thì thật ra nó sẽ được gộp lại chỉ còn 1 cái thôi.

Ví dụ đoạn code thế này

// Expect count = count + 6
this.setState({count : this.state.count + 1});
this.setState({count : this.state.count + 2});
this.setState({count : this.state.count + 3});

Thật ra khi chạy sẽ trông giống thế này

// Actually get count = count + 3
this.state = Object.assign(
    this.state,
    {count : this.state.count + 1}
    {count : this.state.count + 2}
    {count : this.state.count + 3}
);

Nghĩa là chỉ có giá trị cuối cùng của count được set thôi. Ở đây nó sẽ là count + 3 thay vì count + 6 như bạn muốn

Tất nhiên bạn ít khi gọi setState nhiều lần liên tục như thế trong một vòng update. Nhưng việc nó xảy ra khi bạn handle event (onClick, onChange .etc) và khi componentWillReceiveProps sẽ không phải là hiếm gặp.

Một cách dùng khác có thể thay thế cho setState với object cũng được nhắc đến trong document đó là thay vì truyền cho nó một object thì bạn truyền cho nó một function

It’s also possible to pass a function with the signature function(state, props) => newState.

Cái đó nghe thì rõ ràng chỉ là một cách dùng khác của setState, không rõ ràng và dễ dùng như cách trước với object. Không có vẻ quan trọng gì hết. Vì thế nên rất có thể bạn đã bỏ lỡ đoạn quan trọng phía sau.

This enqueues an atomic update that consults the previous value of state and props before setting any values.

Nghĩa là nếu bạn dùng cách này, khi setState được gọi nhiều lần, React sẽ xếp nó lại vào 1 cái queue theo thứ tự nó được gọi.

Ví dụ như thế này

this.setState((prevState, props) => ({count: prevState.count + 1}));
this.setState((prevState, props) => ({count: prevState.count + 2}));
this.setState((prevState, props) => ({count: prevState.count + 3}));

Thì nó sẽ được xếp vào 1 queue như thế này

updatesQueue = [
    (prevState) => ({count: prevState.count + 1}),
    (prevState) => ({count: prevState.count + 2}),
    (prevState) => ({count: prevState.count + 3})
]

Khi chạy thì nó sẽ trông giống thế này

this.state = updatesQueue.reduce((state, fn) => fn(state), this.state)

Bây giờ thì mỗi lần setState sau, bạn sẽ luôn dùng giá trị của state sau lần setState trước.

Nói tóm lại cho dễ hiểu thì chỉ khi dùng setState theo cách này thì bạn mới thực sự setState nhiều lần như bạn muốn. Nó không phải chỉ là một cách dùng khác mà đó là cách mà bạn phải dùng khi muốn setState mà dùng đến giá trị của state hay props hiện tại.

Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.

What's more

Tuy cách setState này trông có vẻ khó dùng hơn nhưng nó cũng đem lại một ứng dụng khá hay ho.

Chắc bạn đã từng có lúc viết một component với cả đống setState bên trong. Với cách viết setState bằng function, bạn có thể viết các thay đổi của state bên ngoài component class.

const increment = (state) => ({count: state.count + 1});

class Counter extends React.Component {
  // ...

  incrementCount () {
    this.setState(increment);
  }
}

Component của bạn không cần quan tâm đến state của nó cụ thể thay đổi thế nào nữa, nó chỉ cần biết nó cần kiểu thay đổi nào thôi. Cách viết này sẽ giúp cho component của bạn trông dễ đọc hơn một chút.

Nếu bạn cần truyền tham số cho nó thì sao. Hãy dùng đến higher-order function như thế này

const increment = (amount) => (state) => ({count: state.count + amount});

class Counter extends React.Component {
  // ...

  incrementCount () {
    this.setState(increment(1));
  }
}

Nếu thích bạn có thể tách hẳn nó ra một file khác rồi import

import {increment} from './stateChanges';

Nếu bạn đã biết về Redux, bạn sẽ thấy cái này có phần giống reducer của Redux. Chính là như vậy đấy. Nếu bạn chưa biết về Redux thì bạn cũng nên đi tìm hiểu đi nhé.