Nhảy tới nội dung

Hiển thị giao diện chờ khi tải ứng dụng, fetch dữ liệu

Khi thực hiện một thao tác lấy dữ liệu tốn thời gian sau đó cập nhật lên view, có thể sử dụng Preloader, hay Skeleton tạo giao diện chờ, giúp ứng dụng chuyển đổi trạng thái trở nên mượt hơn, người dùng có thể nhận biết được trạng thái của ứng dụng hiện tại, tránh gây hiểu nhầm ứng dụng không hoạt động, hay hiện tượng trắng trang, giật khi render dữ liệu, từ đó tăng trải nghiệm người dùng

Hiển thị loading khi fetch dữ liệu

Khi truy cập một trang có chứa dữ liệu cần thời gian để lấy và xử lý, có thể dựng các loading skeleton có hình dạng, kích thước tương ứng với dữ liệu thật sau khi render, giúp trang chuyển trạng thái mượt hơn khi re-render lại view sau khi dữ liệu được cập nhật.

Hai giao diện dưới đây sử dụng Skeleton để hiển thị trạng thái loading khi fetch dữ liệu lầ đầu, infinite scroll để thực hiện tải thêm dữ liệu khi người dùng scroll đến cuối trang, pull to refresh làm mới trang, cập nhât lại dữ liệu mới nhất

Trang chủList Blogs
zmp deployzmp deploy

Ví dụ với màn hình list Blogs:

1. Tạo một store lưu trữ và phân phối dữ liệu

Tạo một store với:

  • State lưu các thông tin về phân trang, data, cờ hiển thị trạng thái loading,...
  • Getters lấy giá trị trong state
  • Actions thực hiện chức năng fetch data và cập nhật state
import { createStore } from "zmp-core/lite";
// service gọi api tương ứng
import { getCategories } from "./services/blogs";

const store = createStore({
state: {
loadingBlogs: false, // hiển thị Skeleton khi lấy dữ liệu lần đầ
latestBlogs: {
limit: 10, // số blogs cần lấy trong 1 lần gọi
skip: 0, // vị trí lấy
data: [], // danh sách blogs
hasMore: false, // Cho biết còn data
},
},
getters: {
latestBlogs({ state }) {
return state.latestBlogs;
},
loadingBlogs({ state }) {
return state.loadingBlogs;
},
},
actions: {
/* action lấy danh sách blogs mới nhất:
* skip, limit phân trang
* showSkeleton hiển thị skeleton trong lần fetch data đầu tiên
* reset làm mới dữ liệu
*/
async getLatestBlogs(
{ state },
{ skip, limit, showSkeleton, reset = false }
) {
if (showSkeleton) {
state.loadingBlogs = true;
}
const { blogs } = await getBlogs({ skip, limit });
state.latestBlogs = {
skip,
limit,
data: reset ? [...blogs] : [...state.latestBlogs.data, ...blogs],
hasMore: blogs.length && blogs.length === limit,
};
if (showSkeleton) {
state.loadingBlogs = false;
}
},
},
});

export default store;

2. Giao diện List Blogs

  • Dựng component BlogItem nhận vào prop loading để render blog item trong trường hợp loading hoặc đã có data

    BlogItem loadingBlogItem
    zmp deployzmp deploy
  • Dựa vào các state: showBlogsLoadingSkeleton, hasMore, data... được lưu trong store tạo phía trên để có xử lý render tương ứng

import React, { useState, useRef, useEffect, useCallback } from "react";
import BlogItem from "@components/BlogItem";
import { Title, Box, Page, List, useStore } from "zmp-framework/react";
import store from "../store";

const Blogs = ({ zmproute }) => {
const allowInfinite = useRef(true); // trạng thái cho phép xử lý infinite loading
const { data, skip = 0, limit = 10, hasMore } = useStore("latestBlogs"); // thông tin về dữ liệu
const loading = useStore("showBlogsLoadingSkeleton"); // hiển thị skeleton trong lần lấy dữ liệu đầu tiên

let blogsList = null;

useEffect(() => {
if (!data.length) {
// gọi fetch data lần đầu
store.dispatch("getLatestBlogs", {
skip: 0,
limit: 10,
showSkeleton: true,
});
}
}, []);

useEffect(() => {
allowInfinite.current = hasMore; // Sau mỗi lần data thay đổi, cập nhât lại xem còn dữ liệu không và cho phép infinite loading
}, [data]);

// xử lý tải thêm cho infinite loading
const loadMore = () => {
if (!allowInfinite.current) return;
allowInfinite.current = false;
if (hasMore) {
store.dispatch("getLatestBlogs", {
skip: skip + limit,
limit,
showSkeleton: false,
});
}
};

// làm mới trang với pull to refresh
const refreshPage = (done) => {
store
.dispatch("getLatestBlogs", {
skip: 0,
limit,
showSkeleton: false,
reset: true,
})
.finally(() => {
done(); // done là callback giúp xác nhận quá trình refresh đã xong
});
};

if (loading) {
// giao diện skeleton
blogsList = (
<div className="blog-list">
<BlogItem loading />
<BlogItem loading />
<BlogItem loading />
</div>
);
} else {
// giao diện danh sách blogs
blogsList = (
<List noHairlines className="blog-list" noHairlinesBetween>
{/* ... render list blogs */}
{data.map((blog) => (
<BlogItem key={blog.id} {...blog} />
))}
</List>
);
}
return (
<Page
ptr
onPtrRefresh={refreshPage}
infinite
infiniteDistance={50}
infinitePreloader={!loading && hasMore}
onInfinite={loadMore}
>
<Box className="blog-list" p="10" m="0">
<Box m="0" flex flexDirection="row" justifyContent="space-between">
<Title size="normal" className="font-extrabold text-blue-dark">
Latest
</Title>
</Box>
{blogsList}
</Box>
</Page>
);
};

export default Blogs;
Tips

Với async action như ở ví dụ trên, khi dispatch action tại component, để thực hiện một tác vụ sau khi action này thực hiện hoàn tất bằng cách

store.dispatch("async_action_name").finally(() => {
// xử lý tại đây
});

Preview

Bạn có thể clone repository này để chạy thử trên máy của mình: Demo Source Code

preview

Sử dụng Zalo quét mã QR trên để xem