What runs, when, and in what order. Getting this wrong is why data loads twice, why a screen shows stale content, and why timers keep running after the user leaves.
app.jspages/x/x.jsThe app starts once. Pages come and go inside it.
App({
globalData: {
userId: '',
cart: [],
},
// Runs ONCE, when the mini-app starts. Never again while it stays open.
onLaunch(options) {
console.log('onLaunch', JSON.stringify(options));
// one-time setup: read launch params, restore storage, fetch user info
},
// Runs after onLaunch, AND every time the app returns to the foreground.
onShow(options) {
console.log('onShow', JSON.stringify(options));
// resume polling, refresh data that goes stale in the background
},
// The user backgrounded the app, or opened another mini-app.
onHide() {
console.log('onHide');
// stop polling, pause uploads, save anything unsaved
},
// Any uncaught JS error anywhere in the mini-app.
onError(msg) {
console.log('app error', msg);
},
// A navigation target that does not exist in app.json.
onPageNotFound(res) {
console.log('page not found', res.path);
ft.reLaunch({ url: '/pages/index/index' });
},
// A rejected Promise nobody caught. Without this they fail silently.
onUnhandledRejection(res) {
console.log('unhandled rejection', res.reason);
},
// The system switched between light and dark.
onThemeChange(res) {
console.log('theme', res.theme); // 'light' | 'dark'
},
});onLaunch and onShow both receive the same object. ft.getEnterOptionsSync() and ft.getLaunchOptionsSync() return it too:
{
path: 'pages/order-detail/order-detail',
query: { orderId: 'ORD-1789379008594' },
scene: 1001,
shareTicket: '',
referrerInfo: {},
apiCategory: 'default'
}{ appId, extraData }. {} when it was not opened by another app.'default' in a normal full-screen launch.Three of these are worth reading defensively. query is {} when the host sent nothing usable; referrerInfo is {} rather than undefined when there is no referrer; and path is empty on a plain home-page launch. Always guard:
const orderId = (options.query && options.query.orderId) || ''; const fromApp = (options.referrerInfo && options.referrerInfo.appId) || '';
onLaunch runs once; onShow runs many times. This is the distinction that matters. Put setup in onLaunch. Put "make this current again" in onShow.Be careful: onShow also fires immediately after onLaunch on a cold start. If you treat every onShow as "the user came back", you will run that code once at startup too.
The app instance is shared, so globalData is how pages pass data to each other:
// in any page const app = getApp(); app.globalData.selectedOrder = order; // write const order = app.globalData.selectedOrder; // read
It lives in memory only — it is gone when the mini-app closes. Use ft.setStorageSync for anything that must survive a restart.
Page({
data: {
items: [],
},
// ONCE, when the page is created. Receives the query parameters.
onLoad(options) {
console.log('onLoad', JSON.stringify(options));
// read params, fetch the data this page needs
},
// Every time the page becomes visible — including the first time,
// and every time the user navigates back to it.
onShow() {
console.log('onShow');
// refresh anything another page may have changed
},
// ONCE, after the first render is finished.
onReady() {
console.log('onReady');
// measure elements, start a canvas, focus an input
},
// The page is still in the stack but no longer visible.
onHide() {
console.log('onHide');
// pause a video, stop a timer
},
// The page is destroyed and removed from the stack.
onUnload() {
console.log('onUnload');
// clear intervals, close connections, cancel uploads
},
});This is the part worth memorising.
App.onLaunch — once App.onShow Page.onLoad — query params arrive here Page.onShow Page.onReady
A stays alive underneath B.
A.onHide B.onLoad B.onShow B.onReady
B is destroyed. A is not reloaded — onLoad does not run again.
B.onUnload A.onShow — the ONLY hook that fires on A
This is why "refresh after the user comes back" has to live in onShow. Putting it in onLoad means it never runs again.
A is destroyed, not stacked.
A.onUnload B.onLoad B.onShow B.onReady
Page.onHide App.onHide
App.onShow Page.onShow
The page is not reloaded. Only onShow fires.
You want a list to refresh when the user returns from a form. The obvious code:
onShow() {
this.loadList(); // also runs on first open, right after onLoad
},
onLoad() {
this.loadList(); // so the list loads TWICE on first open
},Two requests, two renders, a visible flicker.
Pick one of these:
// Option A — only onShow. Simple, and it covers both cases.
onLoad(options) {
this.setData({ orderId: options.orderId || '' });
},
onShow() {
this.loadList();
},// Option B — onLoad for the first fetch, onShow for returns only
onLoad(options) {
this.setData({ orderId: options.orderId || '' });
this.loadList();
},
onShow() {
// skip the onShow that fires immediately after onLoad
if (!this._shownOnce) {
this._shownOnce = true;
return;
}
this.loadList();
},Option A is usually right. Reach for B when the first load needs different handling — a skeleton screen, say — than a silent background refresh.
Anything you start must be stopped, or it keeps running after the page is gone and fires setData on a destroyed page.
Page({
onLoad() {
this._timer = setInterval(() => {
this.loadStatus();
}, 5000);
},
onUnload() {
clearInterval(this._timer);
this._timer = null;
},
});The same applies to ft.onBluetoothDeviceFound, socket connections, uploads, and location watchers. onHide pauses; onUnload tears down.
The user opens a page, taps back before the request finishes, and the callback still fires.
Page({
onLoad() {
this._alive = true;
ft.request({
url: 'https://api.example.com/orders',
success: (res) => {
if (!this._alive) return; // page is gone, do nothing
this.setData({ items: res.data });
},
});
},
onUnload() {
this._alive = false;
},
});Prefix these instance flags with _ and keep them out of data — data is for values the view renders, and every setData costs a render pass.
Not lifecycle, but they live in the same Page({ }) object:
Page({
// requires "enablePullDownRefresh": true in the page's .json
onPullDownRefresh() {
this.loadList();
ft.stopPullDownRefresh(); // you MUST call this, or the spinner never stops
},
// the user scrolled to the bottom — load the next page
onReachBottom() {
this.loadMore();
},
onPageScroll(e) {
// fires constantly — keep this cheap, never setData here
},
// only on tabBar pages
onTabItemTap(item) {
console.log('tab tapped', item.index);
},
// the user tapped Share in the top-right menu
onShareAppMessage(res) {
// res = { title, desc, imageUrl, path, from: 'menu', webViewUrl }
return {
title: 'Check out this order',
desc: 'Track your booking',
imageUrl: '/images/share.png',
path: '/pages/order-detail/order-detail?orderId=' + this.data.orderId,
};
},
});onShareAppMessage must return an object — whatever you return is passed through to the host app. Any field you leave out falls back to the value the host supplied in res.
onReachBottom fires only once per approach to the bottom. The trigger distance is onReachBottomDistance in the page's .json, or in window in app.json.
Pull-to-refresh can also be started from code with ft.startPullDownRefresh() — it runs the same animation and calls the same handler as a real pull.
Define onPageScroll only if you need it. Leaving an empty one in place still costs a message between the logic and rendering layers on every scroll frame.
onPullDownRefresh needs enablePullDownRefresh in that page's .json file, or it never fires:
{
"enablePullDownRefresh": true
}data is the page's initial state. Changing it later must go through setData, or the view never updates:
// WRONG — the screen does not change, and now data is out of sync
this.data.text = 'changed';
// RIGHT
this.setData({ text: 'changed' });setData updates this.data synchronously but repaints the view asynchronously. Pass a second argument if you need to run something after the repaint:
this.setData({ items: list }, () => {
console.log('rendered');
});A key can be a data path, so you can change one item instead of resending an entire array:
this.setData({ 'array[0].text': 'changed' }); // one array item
this.setData({ 'object.text': 'changed' }); // one object property
this.setData({ 'newField.text': 'new data' }); // works even if undeclared in dataOn a long list, sending the whole array on every change is the most common cause of a sluggish page.
Date, no undefined.undefined — the field is skipped and you get confusing bugs. Use '' or null.setData call cannot exceed 1024 kB.data entirely. Flags like this._timer or this._alive belong on the instance — every data field costs a render pass.const pages = getCurrentPages(); const current = pages[pages.length - 1]; // this page const previous = pages[pages.length - 2]; // the one underneath console.log(current.route); // 'pages/order-detail/order-detail'
The first element is the first page opened, the last is the current one. page.route is that page's path — useful for telling where you are without threading a flag through.
Two hard rules:
App.onLaunch — no page exists yet, so you get an empty array.Reading the previous page is the usual way to refresh a list after the user goes back:
onSubmitSuccess() {
const pages = getCurrentPages();
const previous = pages[pages.length - 2];
if (previous) previous.needsRefresh = true;
ft.navigateBack({ delta: 1 });
},…and in the list page, act on it in onShow, which is the only hook that fires on return:
onShow() {
if (this.needsRefresh) {
this.needsRefresh = false;
this.loadList();
}
},Page.onLoadPage.onLoad or onShow — not bothPage.onShowPage.onReadyPage.onHidePage.onUnloadApp.onLaunchApp.onShowApp.onHideapp.globalDataft.setStorageSyncLog every hook while you are learning a flow. The order tells you immediately why something runs twice or not at all:
Page({
onLoad(options) { console.log('LOAD', JSON.stringify(options)); },
onShow() { console.log('SHOW'); },
onReady() { console.log('READY'); },
onHide() { console.log('HIDE'); },
onUnload() { console.log('UNLOAD'); },
});onLoad and onShow — §4onLoad, which does not run again — §3ft.stopPullDownRefresh() — §7enablePullDownRefresh missing from the page .json — §7clearInterval in onUnload — §5setData warning on a closed pageonShow code runs at startuponShow also fires after onLaunch — §1this.data.x directly instead of setData — §8undefined — §8setData inside onPageScroll, or resending a whole array — §7, §8getCurrentPages() returns []App.onLaunch, before any page exists — §9onShareAppMessage returned nothing — §7