Skip to content

Commit 49cbe1d

Browse files
johnfireclaude
andcommitted
fix(mobile): auto-refresh Home & Tags on focus / app foreground
The Home (notes) and Tags tabs loaded data only once on mount, so items/tags created on another device (e.g. the web app) didn't appear until a manual pull-to-refresh or app restart. Add useRefreshOnFocus (refetch on screen focus + return from background), matching the existing checklists tab pattern. No data was ever lost — the records are in the DB; the tabs just showed a stale cached list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent de446ad commit 49cbe1d

3 files changed

Lines changed: 54 additions & 4 deletions

File tree

packages/mobile/app/(tabs)/index.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next";
1515
import { SafeAreaView } from "react-native-safe-area-context";
1616
import { listItems, archiveItem } from "../../src/api/items";
1717
import { reportClientError } from "../../src/api/report";
18+
import { useRefreshOnFocus } from "../../src/lib/useRefreshOnFocus";
1819
import { ItemCard } from "../../src/components/ItemCard";
1920
import { useAuth } from "../../src/store/auth";
2021
import { colors, spacing, radius, font } from "../../src/theme";
@@ -79,6 +80,14 @@ export default function ItemsScreen() {
7980
load(1, search);
8081
}, [search, filter]);
8182

83+
// Silently refresh page 1 when the tab is re-focused or the app returns to the
84+
// foreground, so items added on another device show up. Initial mount load is
85+
// handled by the effect above, so the first focus is skipped.
86+
useRefreshOnFocus(() => {
87+
setPage(1);
88+
load(1, search);
89+
});
90+
8291
function onRefresh() {
8392
setRefreshing(true);
8493
setPage(1);

packages/mobile/app/(tabs)/tags.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from "react";
1+
import React, { useState } from "react";
22
import {
33
View,
44
Text,
@@ -23,6 +23,7 @@ import {
2323
setTagColor,
2424
} from "../../src/api/tags";
2525
import { reportClientError } from "../../src/api/report";
26+
import { useRefreshOnFocus } from "../../src/lib/useRefreshOnFocus";
2627
import { colors, spacing, radius, font } from "../../src/theme";
2728
import type { TagWithCount } from "@notes-world/shared";
2829

@@ -67,9 +68,9 @@ export default function TagsScreen() {
6768
}
6869
}
6970

70-
useEffect(() => {
71-
load();
72-
}, []);
71+
// Load on first focus and refresh whenever the tab is re-focused or the app
72+
// returns to the foreground, so tags created on another device appear.
73+
useRefreshOnFocus(load, { immediate: true });
7374

7475
function onRefresh() {
7576
setRefreshing(true);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useCallback, useEffect, useRef } from "react";
2+
import { AppState } from "react-native";
3+
import { useFocusEffect } from "expo-router";
4+
5+
/**
6+
* Refetch when the screen regains focus (tab switch / returning from a detail
7+
* screen) and when the app returns to the foreground, so data created on another
8+
* device (e.g. the web app) shows up without a manual pull-to-refresh.
9+
*
10+
* The latest callback is held in a ref so callers can pass an inline function
11+
* without re-subscribing every render. The first focus (initial mount) is
12+
* skipped by default so screens that already load on mount don't double-fetch;
13+
* pass { immediate: true } to also run on that first focus.
14+
*/
15+
export function useRefreshOnFocus(
16+
refetch: () => void,
17+
opts: { immediate?: boolean } = {},
18+
): void {
19+
const cb = useRef(refetch);
20+
cb.current = refetch;
21+
const firstFocus = useRef(true);
22+
const immediate = opts.immediate ?? false;
23+
24+
useFocusEffect(
25+
useCallback(() => {
26+
if (firstFocus.current) {
27+
firstFocus.current = false;
28+
if (!immediate) return;
29+
}
30+
cb.current();
31+
}, [immediate]),
32+
);
33+
34+
useEffect(() => {
35+
const sub = AppState.addEventListener("change", (state) => {
36+
if (state === "active") cb.current();
37+
});
38+
return () => sub.remove();
39+
}, []);
40+
}

0 commit comments

Comments
 (0)