(Phần 1) Quản lý state trong Tini App - Cái nhìn tổng quan và cách tiếp cận
Mini app đang là công nghệ được rất nhiều ông lớn trên toàn thế giới và cả Việt Nam đang theo đuổi. Hãy cùng mình tìm hiểu cách quản lý state - bài toán cơ bản của mọi ứng dụng front-end của Tini App Framework - Nền tảng mini app của Tiki.

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ể.
Xin chào các bạn, hôm nay mình xin giới thiệu đến các bạn tổng quan về quản lý state trong Tini App Framework. Nếu bạn chưa từng nghe tới Tini App hoặc mini app thì bạn có thể tìm đọc qua bài viết Tìm hiểu Tiki TiniApp Platform xem có gì hay ? của anh Lý Thành Nhân
Quay lại với định nghĩa của Tini App Framework ở trên, chúng ta có thể thấy được ở Tini App nói chung và Tini App Framework nói riêng tập trung vào 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ố đó.
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
IV. Kết luận
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.
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:
Component({
data: {
name: '',
image_url: '',
},
updateNewData() {
this.setData({
name: 'Iphone 13',
image_url: 'my.domain/iphone13.jpg',
});
},
});
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ị item và index. 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>
Và đây là kết quả:
Bạn có thể tham khảo source code tại đây.
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ư hình trên 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.
Đúng 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:
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é.
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.