Reactjs - Bài 6 - mối quan hệ giữa props và state

Goal

Giới thiệu mối quan hệ của propsstate trong Reactjs

props là IF, state là trạng thái của Component.

  • state có nghĩa là ‘Trạng thái’. Từ bây giờ chúng ta mặc định dùng ‘state’ để chỉ trạng thái của Component.
Hãy xem xét ví dụ sau:
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv='Content-type' content='text/html; charset=utf-8'>
  <title>Basic Example Props</title>
</head>
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
  <script type="text/jsx">

    var Avatar = React.createClass({
      propTypes: {
        name: React.PropTypes.string.isRequired,
        width: React.PropTypes.number.isRequired,
        height: React.PropTypes.number.isRequired,
        like: React.PropTypes.bool.isRequired,
      },

      getInitialState() {
        return {
          liked: this.props.like
        };
      },

      onClick() {
        this.setState({liked: !this.state.liked});
      },

      render() {
        var textLike = this.state.liked ? 'like' : 'haven\'t liked';
        return (
          <div>
            <img  src={this.props.src} width={this.props.width} height={this.props.height} alt="alt" />
            <span>{this.props.name}</span>
            <button onClick={this.onClick}>{textLike}</button>
          </div>
        );
      }

    });

    var AvatarEl = <Avatar 
                      name="Foo" 
                      width={100} 
                      height={100} 
                      src='http://canime.files.wordpress.com/2010/05/mask-dtb.jpg' 
                      like={false}
                      />;

    var AvatarComponent = React.render(AvatarEl, document.body);
  </script>
</body>
</html>
Ví dụ trên được biên tập từ ví dụ trong bài trước nhưng có một số thay đổi như sau:
  1. image source không còn được xem như là state của Component Avatar nữa.
  2. state của Component là like.
  3. like có kiểu dữ liệu là boolean.
  4. Giá trị liked (của state) được truyền và gán vào ở method getInitialState().
Chúng ta có thể config Avatar thông qua props của nó như là name, width, height, src, like. Còn khi trạng thái của nút button thì được chứa trong state. Hình bên dưới sẽ minh họa rõ hơn về điều này:
props-state-01
Bây giờ bạn hãy chạy source code trên và thử click vào button, bạn sẽ thấy text của button thay đổi. Như vậy bên trong Component được quản lý bởi state còn props chỉ dùng để ‘config’.
Khi thiết kế Component, thì trước hết hãy thiết kế Prop như là I/F, rồi định nghĩa những giá trị có thay đổi mà Component quản lý như là State.

Props in getInitialState Is an Anti-Pattern

Sử dụng props truyền vào để khởi tại state trong getInitialState thường dẫn đến việc duplication của nguyên tắc Source Of Truth nữa, sẽ không biết là dữ liệu thực sự thì ở đâu. Việc này khiến cho chương trình lúc nào cũng phải compute value on-the-fly để đảm bảo dữ liệu không mất sự đồng bộ và gây nên vấn đề trong việc bảo trì.
Hãy xem lại ví dụ trên:
      getInitialState() {
        return {
          liked: this.props.like
        };
      },
    var AvatarEl = <Avatar 
                      name="Foo" 
                      width={100} 
                      height={100} 
                      src='http://canime.files.wordpress.com/2010/05/mask-dtb.jpg' 
                      like={false}
                      />;
Mặc dù tôi đã cố tình dùng like cho propsliked cho state, nhưng nó vẫn có khả năng nhầm lẫn.
Tuy nhiên, nó sẽ không phải là một anti-pattern nếu bạn làm rõ việc synchronization không phải là mục tiêu ở đây:
      getInitialState() {
        return {
           // naming it initialX clearly indicates that the only purpose
           // of the passed down prop is to initialize something internally
          liked: this.props.initialLike
        };
      },
    var AvatarEl = <Avatar 
                      name="Foo" 
                      width={100} 
                      height={100} 
                      src='http://canime.files.wordpress.com/2010/05/mask-dtb.jpg' 
                      initialLike={false}
Thao khảo thêm bài viết về điều này tại đây

Using State in parent, Prop as I/F for children.

Qua các ví dụ trên, chúng ta đã có một Component Avatar có chứa các thuộc tính như name, width, height, src, like (button like thì thực ra chúng ta nên tách nó ra thành một Component hoàn chỉnh). Trong thực tế, thông thường chúng ta sẽ có một list các Component. Hãy xem thử cấu trúc sau:
  • Avatar List
    • Avatar
      • name
      • height
      • width
      • src
      • initialLike
    • Avatar
      • name
      • height
      • width
      • src
      • initialLike
    • Avatar
      • name
      • height
      • width
      • src
      • initialLike
Ta xem Avatar List ở đây như làm một Component cha, còn Avatar là một Component con. Component cha sẽ truyền các giá trị vào Component con thông qua I/F props của Component con.
Chúng ta thử refactor code ở phần trước để minh họa cho điều này như sau:
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv='Content-type' content='text/html; charset=utf-8'>
  <title>Basic Example Props</title>
</head>
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
  <script type="text/jsx">

    var Avatar = React.createClass({
      propTypes: {
        name: React.PropTypes.string.isRequired,
        width: React.PropTypes.number.isRequired,
        height: React.PropTypes.number.isRequired,
        initialLike: React.PropTypes.bool.isRequired,
      },

      getInitialState() {
        return {
          liked: this.props.initialLike
        };
      },

      onClick() {
        this.setState({liked: !this.state.liked});
      },

      render() {
        var textLike = this.state.liked ? 'like' : 'haven\'t liked';
        return (
          <li>
            <img  src={this.props.src} width={this.props.width} height={this.props.height} alt="alt" />
            <span>{this.props.name}</span>
            <button onClick={this.onClick}>{textLike}</button>
          </li>
        );
      }

    });

    var Avatars = React.createClass({

      getInitialState() {
        return {
          avatars: [
            {name: "Avatar 1", height: 100, width: 100, initialLike: false, src: "http://canime.files.wordpress.com/2010/05/mask-dtb.jpg"},
            {name: "Avatar 2", height: 100, width: 100, initialLike: true, src: "http://z4.ifrm.com/30544/116/0/a3359905/avatar-3359905.jpg"},
            {name: "Avatar 3", height: 100, width: 100, initialLike: false, src: "http://www.dodaj.rs/f/O/IM/OxPONIh/134.jpg"}
          ]
        }
      },

      render() {  
        var avatars = this.state.avatars.map(function(avatar){
        var AvatarEl = <Avatar 
                      name={avatar.name}
                      width={avatar.width}
                      height={avatar.height}
                      src={avatar.src} 
                      initialLike={avatar.initialLike}
                      />;
          return AvatarEl;
        });
        return (
          <ul>
            {avatars}
          </ul>
        );
      }

    });

    var AvatarsComponent = React.render(<Avatars />, document.body);

  </script>
</body>
</html>
Vì để thấy được sự chuyển tiếp từ phần trước, cũng như cho mọi người dễ hình dung hơn về mối quan hệ cha-con, cho nên chuyển đoạn var AvatarEl ở ví dụ trước vào trong Component Avatars. Tuy nhiên trong thực tế thì phần source code đó, có thể viết gọn hơn như sau:
return <Avatar name={avatar.name} width={avatar.width} height={avatar.height} src={avatar.src}initialLike={avatar.initialLike} />;

Component cha handling event của Component con

Trong dạng list, chúng ta thường hay có các action như thêm item, sửa item, và xóa item. Bây giờ ta thử thêm chức năng xóa item. Khi xóa Component con thì list Component sẽ thay đổi, việc thay đổi này sẽ update lại list Component. Như vậy ở đây, chúng ta thấy một logic như sau:
- Click button delete trên Component con.
- Component con phát sự kiện delete lên Component cha.
- Component cha update lại list.
Source code:
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv='Content-type' content='text/html; charset=utf-8'>
  <title>Basic Example Props</title>
</head>
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
  <script type="text/jsx">

    var Avatar = React.createClass({
      propTypes: {
        id: React.PropTypes.number.isRequired,
        name: React.PropTypes.string.isRequired,
        width: React.PropTypes.number.isRequired,
        height: React.PropTypes.number.isRequired,
        initialLike: React.PropTypes.bool.isRequired,
        // Thêm Interface onDelete()
        onDelete: React.PropTypes.func.isRequired,
      },
      getInitialState() {
        return {
          liked: this.props.initialLike
        };
      },
      onClick() {
        this.setState({liked: !this.state.liked});
      },
      // Ủy quyền cho Component cha xử lý.
      _onDelete() {
        this.props.onDelete(this.props.id);
      },
      render() {
        var textLike = this.state.liked ? 'like' : 'haven\'t liked';
        return (
          <li key={this.props.id}>
            <span>{this.props.id}</span>
            <img  src={this.props.src} width={this.props.width} height={this.props.height} alt="alt" />
            <span>{this.props.name}</span>
            <button onClick={this.onClick}>{textLike}</button>
            <button onClick={this._onDelete}>Delete</button>
          </li>
        );
    }
    });
    var Avatars = React.createClass({

      getInitialState() {
        return {
          avatars: [
            {id: 1, name: "Avatar 1", height: 100, width: 100, initialLike: false, src: "http://canime.files.wordpress.com/2010/05/mask-dtb.jpg"},
            {id: 2, name: "Avatar 2", height: 100, width: 100, initialLike: true, src: "http://z4.ifrm.com/30544/116/0/a3359905/avatar-3359905.jpg"},
            {id: 3, name: "Avatar 3", height: 100, width: 100, initialLike: false, src: "http://www.dodaj.rs/f/O/IM/OxPONIh/134.jpg"}
          ]
        }
      },
      // Thêm method deleteItem() set lại State (chứa các Component con) cho Component cha này
      deleteItem(id) {
        this.setState({
          avatars: this.state.avatars.filter(function(avatar){
            return avatar.id !== id;
          })
        });
      },

      render() {  
        var avatars = this.state.avatars.map(function(avatar){
        // use below solution
        // map(function(){},this) 
        // or map(function(){}.bind(this)) 
        // or var that = this; onDelete = {that.deleteUser}
        // to pass this value to map function.
        // bind onDelete (event) to deleteUser.
          return <Avatar onDelete={this.deleteItem} id={avatar.id} name={avatar.name} width={avatar.width} height={avatar.height} src={avatar.src} initialLike={avatar.initialLike} />;
        }, this);
        return (
          <ul>
            {avatars}
          </ul>
        );
      }
    });
    var AvatarsComponent = React.render(<Avatars />, document.body);
  </script>
</body>
</html>
Hình bên dưới sẽ giúp bạn hiểu rõ hơn về mối quan hệ giữa event của Component con và Component cha.
props-event-state
Khi user click vào button Delete, gọi đến onDelete method Interface đã được binding với method deleteItem() ở Component cha. Việc update lại list Component sẽ được xử lý trong method này.