Shoppingate Logo
HomePartnerSell on E-commAbout UsContact UsDevelopers
Documentation
Get started
  • Getting started
  • Environments
Mini-app development
  • Authoring
  • app.json config
  • Lifecycle
  • Routing & query
  • Host functions
  • · orderPayment
  • Permissions
  • Deep links
Backend integration
  • Authentication
  • Endpoints
  • Webhooks
Runtime

Mini-app lifecycle — app.js and pages

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.

Two levels

Level
File
Exists for
App
app.js
the whole mini-app — one instance, start to finish
Page
pages/x/x.js
one screen — created when opened, destroyed when closed

The app starts once. Pages come and go inside it.

1. App lifecycle — app.js

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'
    },
});

What options contains

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'
}
Field
Type
What it is
path
String
The page the app was opened on. Empty when opened on the home page.
query
Object
The query parameters, already parsed into key/value pairs.
scene
Number
A code for how the app was opened — host menu, scan, share, deep link. The host defines the values.
shareTicket
String
Present when opened from a share, for resolving the sharing group. Empty otherwise.
referrerInfo
Object
Who opened this app: { appId, extraData }. {} when it was not opened by another app.
apiCategory
String
Which API set the current context allows — '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.

globalData

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.

2. Page lifecycle — pages/x/x.js

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
    },
});
Hook
How often
Use it for
onLoad
once
read query params, first data fetch
onShow
every time visible
refresh data another page changed
onReady
once
anything that needs the rendered view
onHide
every time hidden
pause timers, video, polling
onUnload
once
cleanup — the page is gone

3. The firing order

This is the part worth memorising.

Opening the mini-app (cold start)

App.onLaunch          — once
App.onShow
Page.onLoad           — query params arrive here
Page.onShow
Page.onReady

Page A opens page B with navigateTo

A stays alive underneath B.

A.onHide
B.onLoad
B.onShow
B.onReady

The user goes back from B to A

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.

Page A opens page B with redirectTo

A is destroyed, not stacked.

A.onUnload
B.onLoad
B.onShow
B.onReady

The user backgrounds the app

Page.onHide
App.onHide

The user returns to the app

App.onShow
Page.onShow

The page is not reloaded. Only onShow fires.

4. The trap: onShow fires right after onLoad

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.

5. Always clean up in onUnload

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.

6. A late response can outlive the page

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.

7. Page event handlers

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
}

8. setData — the only way to change the screen

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');
});

Update one field, not the whole object

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 data

On a long list, sending the whole array on every change is the most common cause of a sluggish page.

Rules

  • Only JSON-serializable values — strings, numbers, booleans, objects, arrays. No functions, no Date, no undefined.
  • Never set a value to undefined — the field is skipped and you get confusing bugs. Use '' or null.
  • One setData call cannot exceed 1024 kB.
  • Keep non-rendered values off data entirely. Flags like this._timer or this._alive belong on the instance — every data field costs a render pass.

9. The page stack

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:

  • Never modify the stack. Pushing or splicing it corrupts routing and page state.
  • Never call it in 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();
    }
},

10. Where to put what

You want to…
Put it in
Read query parameters
Page.onLoad
Fetch the page's data the first time
Page.onLoad or onShow — not both
Refresh after the user navigates back
Page.onShow
Measure an element, draw on a canvas
Page.onReady
Pause a video or a timer
Page.onHide
Clear timers and connections
Page.onUnload
One-time app setup, read launch params
App.onLaunch
Resume polling when the app returns
App.onShow
Save state before the app is backgrounded
App.onHide
Share data between pages
app.globalData
Keep data across restarts
ft.setStorageSync

Debugging

Log 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'); },
});

Symptom index

Symptom
Cause
Data loads twice on first open
Fetching in both onLoad and onShow — §4
Screen stale after navigating back
Fetch is in onLoad, which does not run again — §3
Pull-to-refresh spinner never stops
Missing ft.stopPullDownRefresh() — §7
Pull-to-refresh does nothing
enablePullDownRefresh missing from the page .json — §7
Requests keep firing after leaving
No clearInterval in onUnload — §5
setData warning on a closed page
Late callback with no guard — §6
App-level onShow code runs at startup
onShow also fires after onLaunch — §1
Changed a value but the screen didn't update
Assigned this.data.x directly instead of setData — §8
A field silently refuses to update
Its value was undefined — §8
Scrolling is janky
setData inside onPageScroll, or resending a whole array — §7, §8
getCurrentPages() returns []
Called during App.onLaunch, before any page exists — §9
Share sheet shows the wrong text
onShareAppMessage returned nothing — §7

Need help? We’re here.

Our team is ready to support you in every step of your experience.

Email Us
[email protected]
Sell With SGContact UsTerms of ServicePrivacy PolicyFAQTravel FAQ
Connect with us
X / TwitterLinkedInInstagramFacebookTiktok

Copyright © Shoppingate 2026

Mada
Visa
Mastercard
Apple Pay
Tamara