forked from Tadrisino/MathApp_frontend
Smooth screen transitions: new screen waits hidden until its data arrives, no half-built flash
- Navigator stage: the next screen mounts hidden under the current one and is revealed only when its first data has arrived (ScreenGate held by useAsync/useGateHold), at most 450 ms; a thin top bar shows after 120 ms of waiting. Push slides from the left, back from the right, tabs only fade. - Session cache (stale-while-revalidate) for screen GETs: revisited screens draw instantly. - Home map and stats are fetched during the splash, so the first screen arrives complete. - Entrance pop-ins (mascot, islands, lesson cards) run on first view only, not on every revisit. - Shop status card shows placeholders instead of a wrong «نداری» while loading. - Re-tapping the open tab scrolls to top instead of reloading; sheets close with a swipe down. - Quiz: «۱ از ۴» instead of «۱/۴» (read as a fraction), varied praise, «N تا پشتِ هم درست», growth-mindset copy after a weak round, zero-reward tiles hidden. - Streak: a concrete next step when the streak is 0 or today is still empty. Checked in headless Chrome against the local backend (frame captures at 150 ms and 700 ms latency, rapid tab tapping, full quiz run). Not checked on a real phone or iOS Safari. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState, type DependencyList } from 'react';
|
||||
import { createContext, useCallback, useContext, useEffect, useRef, useState, type DependencyList } from 'react';
|
||||
|
||||
export interface AsyncState<T> {
|
||||
data: T | undefined;
|
||||
@@ -8,39 +8,104 @@ export interface AsyncState<T> {
|
||||
setData: (update: T | ((prev: T | undefined) => T | undefined)) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* کشِ همین نشست برای پاسخهای GET، با کلیدی که صفحه میدهد. صفحهای که دوباره باز میشود (برگشت، تعویضِ تب)
|
||||
* فوراً با دادهی قبلی رسم میشود و همان لحظه بیصدا از سرور تازه میشود (stale-while-revalidate). قبلاً هر بار
|
||||
* اول کارتِ خالی یا «نداری» دیده میشد و چند صد میلیثانیه بعد دادهی واقعی جایش میپرید.
|
||||
* با خروج از حساب پاک میشود (`clearAsyncCache`)، تا دادهی یک کاربر به کاربرِ بعدی نرسد.
|
||||
*/
|
||||
const cache = new Map<string, unknown>();
|
||||
|
||||
export function clearAsyncCache() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* «دروازه»ی صفحهی تازه: Navigator صفحهی بعدی را پنهان زیرِ صفحهی فعلی میسازد و تا وقتی دادهی اولش
|
||||
* نرسیده (یا سقفِ انتظار نگذشته) نشانش نمیدهد. هر useAsyncی که بارِ اولش بیکش است یک hold میگیرد.
|
||||
* بیرون از صفحهها (مثلاً جریانِ ورود) این context خالی است و کاری نمیکند.
|
||||
*/
|
||||
export interface ScreenGate {
|
||||
hold: () => () => void;
|
||||
}
|
||||
export const ScreenGateContext = createContext<ScreenGate | null>(null);
|
||||
|
||||
/**
|
||||
* برای صفحهای که دادهاش را بی useAsync میگیرد (آزمون، دستیار): تا `loading` درست است، صفحهی تازه پنهان
|
||||
* میماند. همان قاعدهی useAsync: دروازهی بازشده hold را نادیده میگیرد.
|
||||
*/
|
||||
export function useGateHold(loading: boolean) {
|
||||
const gate = useContext(ScreenGateContext);
|
||||
useEffect(() => (loading && gate ? gate.hold() : undefined), [loading, gate]);
|
||||
}
|
||||
|
||||
/**
|
||||
* دادهای که از سرور میآید، با حالتهای بارگذاری/خطا و «دوباره».
|
||||
* پاسخِ درخواستِ کهنه (بعد از عوض شدنِ deps) نادیده گرفته میشود.
|
||||
* با `key`، پاسخ در کشِ نشست میماند و بارِ بعد فوراً نشان داده میشود (بالای همین فایل).
|
||||
*/
|
||||
export function useAsync<T>(load: () => Promise<T>, deps: DependencyList): AsyncState<T> {
|
||||
const [data, setData] = useState<T | undefined>(undefined);
|
||||
export function useAsync<T>(load: () => Promise<T>, deps: DependencyList, opts: { key?: string } = {}): AsyncState<T> {
|
||||
const { key } = opts;
|
||||
const [data, setDataState] = useState<T | undefined>(() => (key !== undefined ? (cache.get(key) as T | undefined) : undefined));
|
||||
const [error, setError] = useState<unknown>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(() => key === undefined || !cache.has(key));
|
||||
const [nonce, setNonce] = useState(0);
|
||||
const loadRef = useRef(load);
|
||||
loadRef.current = load;
|
||||
const gate = useContext(ScreenGateContext);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
setLoading(true);
|
||||
const hit = key !== undefined && cache.has(key);
|
||||
if (hit) {
|
||||
// کلید عوض شده (مثلاً «هفتگی» ← «همیشه») و برای کلیدِ تازه کش هست: همان را نشان بده و بیلودر تازه کن.
|
||||
setDataState(cache.get(key) as T);
|
||||
setLoading(false);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
// دروازهای که یک بار باز شده hold را نادیده میگیرد؛ پس «دوباره» و تازهسازی صفحه را پنهان نمیکنند.
|
||||
const release = !hit && gate ? gate.hold() : null;
|
||||
loadRef.current().then(
|
||||
(value) => {
|
||||
if (!alive) return;
|
||||
setData(value);
|
||||
if (key !== undefined) cache.set(key, value);
|
||||
setDataState(value);
|
||||
setLoading(false);
|
||||
release?.();
|
||||
},
|
||||
(err: unknown) => {
|
||||
if (!alive) return;
|
||||
setError(err);
|
||||
if (hit) {
|
||||
// دادهی کششده روی صفحه است؛ شکستِ تازهسازی آن را با صفحهی خطا عوض نمیکند، ولی دیده میشود.
|
||||
console.warn(`refresh of ${key} failed; showing cached data`, err);
|
||||
} else {
|
||||
setError(err);
|
||||
}
|
||||
setLoading(false);
|
||||
release?.();
|
||||
},
|
||||
);
|
||||
return () => {
|
||||
alive = false;
|
||||
release?.();
|
||||
};
|
||||
}, [...deps, nonce]);
|
||||
}, [...deps, key, nonce]);
|
||||
|
||||
const reload = useCallback(() => setNonce((n) => n + 1), []);
|
||||
const setData = useCallback(
|
||||
(update: T | ((prev: T | undefined) => T | undefined)) => {
|
||||
setDataState((prev) => {
|
||||
const next = typeof update === 'function' ? (update as (p: T | undefined) => T | undefined)(prev) : update;
|
||||
if (key !== undefined) {
|
||||
if (next === undefined) cache.delete(key);
|
||||
else cache.set(key, next);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[key],
|
||||
);
|
||||
return { data, error, loading, reload, setData };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user