Quản lý state trong Tini App - Cái nhìn tổng quan và cách tiếp cận (P1)

Quản lý state trong Tini App - Cái nhìn tổng quan và cách tiếp cận (P1)


Tini App Framework được thiết kế để cho phép các nhà phát triển xây dựng ứng dụng của mình với trải nghiệm native app trên nền tảng Tiki một cách dễ dàng và đa dạng tính năng nhất có thể.

Như định nghĩa ở trên, chúng ta có thể thấy được ở Tini App nói chung và Tini App Framework nói riêng có 2 đặc tính chính: Thứ nhất là mang lại trải nghiệm native app trên nền tảng Tiki và thứ hai cũng chính là điều mình đang muốn nói tới đó chính là sự dễ dàng và đa dạng. Dựa trên giá trị cốt lõi đó, Tini App Framework sẽ cung cấp những công cụ cần thiết và để cho developer có thể thỏa sức sáng tạo theo ngữ cảnh cũng như sở thích của mình. Việc quản lý state cũng nằm trong số đó.

Xin chào các bạn, hôm nay mình sẽ đồng hành cùng các bạn đi qua các nội dung chính sau:

  • I. Các thành phần cơ bản của một Tini App
  • II. State - Dữ liệu và linh hồn của page/component
  • III. Props - Công cụ kết nối
    • Pass data down
    • Pass event up
    • Sibling page/component and getApp() method
    • Vấn đề
  • IV. Kết luận

I. Các thành phần cơ bản của một Tini App


Dễ dàng không có nghĩa là đơn giản - Đa dạng nhưng phải có cấu trúc.

Một ứng dụng Tini App sẽ có các thành phần như sau:

  • App: Thể hiện của toàn ứng dụng.
  • Page: Đại diện cho một màn hình.
  • Component: Thành phần giao diện (và logic nếu có) độc lập, có thể tái sử dụng như header, footer, sidebar, modal, …

Trong một app sẽ có thể bao gồm một hoặc nhiều page, trong mỗi page có thể có một hoặc nhiều component và thậm chí trong một component có thể chứa một hoặc nhiều component khác.
Những thành phần trên tạo nên khung sườn của một Tini App, tuy nhiên để có một ứng dụng hoàn chỉnh chúng ta phải có dữ liệu và công cụ để dẫn truyền dữ liệu từ thành phần này đến các thành phần khác.


II. State - Dữ liệu và linh hồn của page/component


State hay còn được gọi là trạng thái của một page/component là một object chứa thông tin private của page/component đó. State có thể thay đổi và mỗi khi nó thay đổi, page/component sẽ được re-render.

Có thể hiểu đơn giản rằng các page/component là một hàm số F có input là state và output là UI hiển thị trên browser. Như vậy với mỗi giá trị của state ta sẽ thu được một UI tương ứng:

F(state1) = UI1
F(state2) = UI2

Người dùng có thể tương tác với UI nhưng không trực tiếp khiến nó thay đổi, thứ thực sự thay đổi chính là state.

Bất kì Tini App page hoặc component nào đều sẽ có một thuộc tính data chính là state và một phương thức setData để thực hiện thay đổi state.


III. Props - Công cụ kết nối


1. Pass data down

Cũng như tất cả các framework khác đang có mặt trong thế giới front-end thì Tini App Framework cũng cung cấp cho chúng ta khả năng truyền data từ page sang component con của nó và từ component cha sang component con thông qua props.

Props hay còn được gọi là thuộc tính của page/component cũng là một object chứa thông tin nhưng khác với state, props sẽ chứa thông tin nhận được từ page/component cha.

Props không thể bị thay đổi bởi component sử dụng nó.

Bài toán:
Giả sử mình có một page là product-list có state chứa products là một danh sách các sản phẩm.
→ Yêu cầu hiển thị danh sách tất cả các sản phẩm.

pages/product-list/index.js

Page({
  data: {
    products: [
      {
        id: '1',
        image_url: 'my.domain.com/iphone12.jpg',
        name: 'Iphone 12',
      },
      {
        id: '2',
        image_url: 'my.domain.com/macbook-pro.jpg',
        name: 'Macbook Pro',
      },
    ],
  },
});

Chúng ta sẽ dùng thuộc tính tiki:for để lặp qua mảng products và với mỗi phần tử trong mảng ta có thể truy xuất vào 2 giá trị itemindex. Từ đó mình sẽ truyền các giá trị của item vào component product-item. Các bạn có thể xem thêm về render list tại đây.

pages/product-list/index.txml

<view>
  <product-item
    tiki:for="{{products}}"
    tiki:key="{{item.id}}"
    id="{{item.id}}"
    image_url="{{item.image_url}}"
    name="{{item.name}}"
  />
</view>

Tuy nhiên mình muốn các bạn chú ý vào các thuộc tính id, image_url, name và cách mình sử dụng props để nhận dữ liệu trong product-item phía dưới đây:

components/product-item/index.js

Component({
  // Khai báo tên và giá trị mặc định của props
  props: {
    id: '',
    image_url: '',
    name: '',
  },
});

components/product-item/index.txml

<view>
  <image src="{{image_url}}" />
  <text>{{name}}</text>
</view>

Thêm một chút css cho dễ nhìn và đây là kết quả:



2. Pass event up

Mở rộng bài toán:
Ở page product-list mình sẽ có thêm một giỏ hàng. Giỏ hàng này là một mảng lưu thông tin sản phẩm được thêm vào và hiển thị ra bên ngoài tổng số lượng sản phẩm.
Ở mỗi product-item mình có một nút Add to cart.

→ Yêu cầu đặt ra là làm thế nào để mỗi khi bấm vào nút Add to cart này thì giỏ hàng ở page product-list của mình sẽ được cập nhật ?



Ý tưởng để giải quyết bài toán này là khi bấm vào nút Add to cart, ta sẽ truyền dữ liệu của sản phẩm lên cho page cha, từ đó push sản phẩm này vào mảng giỏ hàng.
Tuy nhiên, trước khi bắt tay vào làm, chúng ta cần lưu ý điều sau:

Dữ liệu của Tini App Framework được chỉ được truyền theo 1 chiều duy nhất.

Tức là trong Tini App Framework dữ liệu chỉ được truyền từ page/component cha đến component con, không có chiều ngược lại.
Vậy sẽ không có cách truyền trực tiếp thông tin từ component con sang cha, mà ta phải dùng một “kỹ thuật” tạm gọi là pass event up:

  • Từ page cha product-list ta định nghĩa một callback addToCart.
  • Truyền callback này xuống component product-item thông qua props.
  • Khi bấm vào nút Add to cart, component product-item sẽ gọi callback này với input là thông tin sản phẩm.
  • Khi đó callback ở page cha sẽ nhận được sản phẩm được chọn và tiến hành thêm vào mảng giỏ hàng.


pages/product-list/index.js

Page({
  data: {
    // Thêm mảng giỏ hàng
    cart: [],
    products: [
    // ...
    ],
  },

  // Thêm callback onAddToCart
  onAddToCart(product) {
    const { cart } = this.data;
    this.setData({
      cart: [...cart, product],
    });
  },
});

pages/product-list/index.txml

<view>
  <!-- Hiện số lượng sản phẩm -->
  <view >Cart: {{cart.length}}</view>

  <!-- Bổ sung sự kiện onAddToCart -->
  <product-item
    tiki:for="{{products}}"
    tiki:key="{{item.id}}"
    id="{{item.id}}"
    image_url="{{item.image_url}}"
    name="{{item.name}}"
    onAddToCart="onAddToCart"
  />
</view>

components/product-item/index.js

Component({
  props: {
    id: '',
    image_url: '',
    name: '',
  },

  methods: {
    // Bổ sung method _onAddToCart
    _onAddToCart() {
      this.props.onAddToCart(this.props);
    },
  },
});

components/product-item/index.txml

<view>
  <image src="{{image_url}}" />
  <text>{{name}}</text>

  <!-- Thêm event onTap -->
  <button onTap="_onAddToCart">Add to cart</button>
</view>

Bạn có thể tham khảo source code tại đây. Và chúng ta có kết quả:


Event up demo


3. Sibling page/component and getApp() method

Như đã tìm hiểu ở trên, ta có thể dùng props để truyền dữ liệu xuống dưới và dùng event để gửi dữ liệu lên trên. Tuy nhiên làm thế nào để truyền dữ liệu giữa hai page/component đồng cấp ?


Như ví dụ ở trên, nếu chúng ta có thêm một page/component top-nav đồng cấp với product-list và cũng có nhu cầu sử dụng cart.
Vậy làm sao để truyền cart từ product-list sang top-nav hoặc ngược lại ?
Nếu các bạn nhớ điều mà mình đã note ở trên thì chắc hẳn các bạn sẽ biết được câu trả lời.

Nhắc lại: Dữ liệu của Tini App Framework được chỉ được truyền theo 1 chiều duy nhất.

Vì vậy chúng ta không thể truyền trực tiếp data giữa 2 page/component đồng cấp. Vì dữ liệu chỉ được truyền từ page/component cha xuống con nên chúng ta phải tìm một page/component cha là điểm giao giữa 2 page/component này và đặt cart ở đó.

Và còn gì tuyệt hơn, trong trường hợp này là trong nhiều trường hợp khác là đặt chúng ở App - Nơi mà ta có thể thêm các thuộc tính và phương thức mới và là nơi mọi page/component đểu có thể truy cập tới thông qua getApp().

Tini App Framework cung cấp 1 hàm global là getApp() , có thể access ở cả page và component. Hàm getApp() trả về instance của application. Xem thêm tại đây.

app.js

App({
  // Đặt cart và onAddToCart ở app
  cart: [],
  onAddToCart(product) {
    this.cart.push(product);
  },
});

pages/product-list/index.js

Page({
  data: {
    cart: [],
    products: [
    // ...
    ],
  },

  // Xóa bỏ onAddToCart,
  // onAddToCart(product) {},

  // Cập nhật cart từ app sau khi được render
  onReady() {
    this.setData({
      cart: getApp().cart,
    });
  },
});

Bạn có thể tìm hiểu thêm về life cycle của page tại đây


pages/product-list/index.txml

<view>
  <!-- Hiện số lượng sản phẩm -->
  <view >Cart: {{cart.length}}</view>

  <!-- Xóa bỏ sự kiện onAddToCart -->
  <product-item
    tiki:for="{{products}}"
    tiki:key="{{item.id}}"
    id="{{item.id}}"
    image_url="{{item.image_url}}"
    name="{{item.name}}"
  />
</view>

components/product-item/index.js

Component({
  props: {
    id: '',
    image_url: '',
    name: '',
  },

  methods: {
    // Sử dụng onAddToCart từ app
    _onAddToCart() {
      getApp().onAddToCart(this.props);
    },
  },
});

Bạn có thể tham khảo code ở đây.

4. Vấn đề

Mọi thứ gần như rất hoàn hảo. Hãy cùng chạy thử code nào.


Demo issue getApp


Opps! tại sao cart lại không thay đổi nhỉ ? Hãy thử log ở hàm onAddToCart.

pages/product-list/index.js

App({
  cart: [],
  onAddToCart(product) {
    this.cart.push(product);

   // Log cart ra nào
    console.log('Cart: ', this.cart);
  },
});

Như chúng ta thấy app.cart đã thực sự thay đổi nhưng có 2 vấn đề ở đây:

  • Thứ nhất: Khi các properties ở app thay đổi, chúng không trigger event nào để báo cho các page/component sử dụng getApp() nhận biết.
  • Thứ hai: Cho dù ở app có trigger event thông báo sự thay đổi đi chăng nữa thì UI cũng sẽ không được cập nhật vì hiện tại ở các page/component ấy không có phương thức hay life cycle nào lắng nghe sự thay đổi để cập nhật lại state.

Vậy làm sao để giải quyết vấn đề này ? Hãy cùng nhau bàn luận ở phần 2 nhé.


IV. Kết luận


Nếu bạn đọc tới đây, rất cảm ơn bạn và cũng xin chúc mừng bạn đã cùng mình ôn lại/học hỏi những điều rất cơ bản nhưng rất quan trọng của một ứng dụng front-end nói chung và của một ứng dụng Tini App nói riêng:

  • Cấu trúc/thành phần của một ứng dụng Tini App.
  • State là gì ? Nó quyết định đến UI như thế nào ?
  • Props là gì ? Nó giúp ta truyền data như thế nào ?
  • Chúng ta biết về app - Nơi mọi page/component đều có thể truy cập và vấn đề của nó.

Vì bài viết đã dài nên mình xin kết thúc phần 1 ở đây. Xin hẹn gặp lại các bạn ở phần 2 và cùng nhau giải quyết những vấn đề đang bỏ ngỏ.

Chúc các bạn có thời gian vui vẻ với Tini App.

6 Likes