Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useColorPalette } from "@/context/ColorPaletteContext";
import { useFilters } from "@/context/FilterContext";
import { useMutation } from "@/context/MutationContext";
import { useSettings } from "@/context/SettingsContext";
import { useMutationVersionEffect } from "@/hooks/useMutationVersionEffect";
import { TodoEditingProvider } from "@/hooks/useTodoEditing";
import {
AgendaEntry,
Expand Down Expand Up @@ -171,7 +172,7 @@ export default function AgendaScreen() {
const { groupByCategory, multiDayRangeLength, multiDayPastDays } =
useSettings();
const { getCategoryColor } = useColorPalette();
const isInitialMount = useRef(true);
const requestIdRef = useRef(0);
const { width } = useWindowDimensions();
const useCompactDate = width < 400;

Expand Down Expand Up @@ -569,6 +570,7 @@ export default function AgendaScreen() {
if (!api) {
return;
}
const requestId = ++requestIdRef.current;

try {
const dateString = formatDateForApi(date);
Expand All @@ -589,6 +591,7 @@ export default function AgendaScreen() {
api.getTodoStates().catch(() => null),
api.getAllHabitStatuses(14, 14).catch(() => null),
]);
if (requestId !== requestIdRef.current) return;
// Convert multi-day response to single-day format
const entries = multiDayData.days[dateString] || [];
const agendaData: SingleDayAgendaResponse = {
Expand All @@ -615,6 +618,7 @@ export default function AgendaScreen() {
}
setError(null);
} catch (err) {
if (requestId !== requestIdRef.current) return;
console.error("Failed to load agenda:", err);
setError("Failed to load agenda");
}
Expand All @@ -627,6 +631,7 @@ export default function AgendaScreen() {
if (!api) {
return;
}
const requestId = ++requestIdRef.current;

try {
const startDateString = formatDateForApi(startDate);
Expand All @@ -647,6 +652,7 @@ export default function AgendaScreen() {
api.getTodoStates().catch(() => null),
api.getAllHabitStatuses(14, 14).catch(() => null),
]);
if (requestId !== requestIdRef.current) return;
setMultiDayData(multiDayAgendaData);
if (statesData) {
setTodoStates(statesData);
Expand All @@ -666,6 +672,7 @@ export default function AgendaScreen() {
}
setError(null);
} catch (err) {
if (requestId !== requestIdRef.current) return;
console.error("Failed to load multi-day agenda:", err);
setError("Failed to load multi-day agenda");
}
Expand Down Expand Up @@ -699,18 +706,17 @@ export default function AgendaScreen() {
]);

// Refetch when mutations happen elsewhere
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}
if (viewMode === "multiday") {
fetchMultiDayAgenda(selectedDate, multiDayRangeLength, showCompleted);
} else {
fetchAgenda(selectedDate, showCompleted);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mutationVersion]);
useMutationVersionEffect(
mutationVersion,
() => {
if (viewMode === "multiday") {
fetchMultiDayAgenda(selectedDate, multiDayRangeLength, showCompleted);
} else {
fetchAgenda(selectedDate, showCompleted);
}
},
{ skipInitial: true },
);

const goToPrevious = useCallback(() => {
const newDate = new Date(selectedDate);
Expand Down
21 changes: 12 additions & 9 deletions app/(tabs)/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getTodoKey, TodoItem } from "@/components/TodoItem";
import { useApi } from "@/context/ApiContext";
import { useFilters } from "@/context/FilterContext";
import { useMutation } from "@/context/MutationContext";
import { useMutationVersionEffect } from "@/hooks/useMutationVersionEffect";
import { TodoEditingProvider } from "@/hooks/useTodoEditing";
import { Todo, TodoStatesResponse } from "@/services/api";
import { filterTodos } from "@/utils/filterTodos";
Expand Down Expand Up @@ -36,7 +37,7 @@ export default function SearchScreen() {
const theme = useTheme();
const { mutationVersion } = useMutation();
const { filters } = useFilters();
const isInitialMount = useRef(true);
const requestIdRef = useRef(0);

const handleTodoUpdated = useCallback(
(todo: Todo, updates: Partial<Todo>) => {
Expand All @@ -50,19 +51,22 @@ export default function SearchScreen() {

const fetchTodos = useCallback(async () => {
if (!api) return;
const requestId = ++requestIdRef.current;

try {
const [todosResponse, statesResponse] = await Promise.all([
api.getAllTodos(),
api.getTodoStates().catch(() => null),
]);
if (requestId !== requestIdRef.current) return;
setTodos(todosResponse.todos);
setFilteredTodos(todosResponse.todos);
if (statesResponse) {
setTodoStates(statesResponse);
}
setError(null);
} catch (err) {
if (requestId !== requestIdRef.current) return;
setError("Failed to load todos");
console.error(err);
}
Expand All @@ -73,14 +77,13 @@ export default function SearchScreen() {
}, [fetchTodos]);

// Refetch when mutations happen elsewhere
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}
fetchTodos();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mutationVersion]);
useMutationVersionEffect(
mutationVersion,
() => {
fetchTodos();
},
{ skipInitial: true },
);

useEffect(() => {
// First apply context filters
Expand Down
36 changes: 22 additions & 14 deletions app/(tabs)/views.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getTodoKey, TodoItem } from "@/components/TodoItem";
import { useApi } from "@/context/ApiContext";
import { useFilters } from "@/context/FilterContext";
import { useMutation } from "@/context/MutationContext";
import { useMutationVersionEffect } from "@/hooks/useMutationVersionEffect";
import { TodoEditingProvider } from "@/hooks/useTodoEditing";
import {
CustomView,
Expand Down Expand Up @@ -41,7 +42,7 @@ export default function ViewsScreen() {
const theme = useTheme();
const { mutationVersion } = useMutation();
const { filters } = useFilters();
const isInitialMount = useRef(true);
const requestIdRef = useRef(0);

// Apply filters to view entries
const filteredEntries = selectedView
Expand All @@ -67,18 +68,21 @@ export default function ViewsScreen() {

const fetchViews = useCallback(async () => {
if (!api) return;
const requestId = ++requestIdRef.current;

try {
const [viewsData, statesData] = await Promise.all([
api.getCustomViews(),
api.getTodoStates().catch(() => null),
]);
if (requestId !== requestIdRef.current) return;
setViews(viewsData.views);
if (statesData) {
setTodoStates(statesData);
}
setError(null);
} catch (err) {
if (requestId !== requestIdRef.current) return;
setError("Failed to load views");
console.error(err);
}
Expand All @@ -87,17 +91,22 @@ export default function ViewsScreen() {
const fetchViewEntries = useCallback(
async (key: string) => {
if (!api) return;
const requestId = ++requestIdRef.current;

setLoading(true);
try {
const viewData = await api.getCustomView(key);
if (requestId !== requestIdRef.current) return;
setSelectedView(viewData);
setError(null);
} catch (err) {
if (requestId !== requestIdRef.current) return;
setError("Failed to load view entries");
console.error(err);
} finally {
setLoading(false);
if (requestId === requestIdRef.current) {
setLoading(false);
}
}
},
[api],
Expand All @@ -108,18 +117,17 @@ export default function ViewsScreen() {
}, [fetchViews]);

// Refetch when mutations happen elsewhere
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}
if (selectedView) {
fetchViewEntries(selectedView.key);
} else {
fetchViews();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mutationVersion]);
useMutationVersionEffect(
mutationVersion,
() => {
if (selectedView) {
fetchViewEntries(selectedView.key);
} else {
fetchViews();
}
},
{ skipInitial: true },
);

const onRefresh = useCallback(async () => {
setRefreshing(true);
Expand Down
12 changes: 10 additions & 2 deletions context/TemplatesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import React, {
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";

Expand Down Expand Up @@ -57,18 +58,21 @@ export function TemplatesProvider({ children }: { children: ReactNode }) {
const [error, setError] = useState<string | null>(null);
const { isAuthenticated } = useAuth();
const api = useApi();
const requestIdRef = useRef(0);

const reloadTemplates = useCallback(async () => {
if (!api) {
return;
}

const requestId = ++requestIdRef.current;
setIsLoading(true);
setError(null);

try {
// Use the unified /metadata endpoint for all app metadata
const metadata = await api.getMetadata();
if (requestId !== requestIdRef.current) return;

// Log any errors from the backend
if (metadata.errors && metadata.errors.length > 0) {
Expand Down Expand Up @@ -102,6 +106,7 @@ export function TemplatesProvider({ children }: { children: ReactNode }) {
api.getCategoryTypes(),
api.getHabitConfig(),
]);
if (requestId !== requestIdRef.current) return;

const [
templatesResult,
Expand Down Expand Up @@ -153,8 +158,10 @@ export function TemplatesProvider({ children }: { children: ReactNode }) {
// exposedFunctions not available in fallback mode (no individual endpoint)
setExposedFunctions(null);
} finally {
setIsLoading(false);
setHasLoadedOnce(true);
if (requestId === requestIdRef.current) {
setIsLoading(false);
setHasLoadedOnce(true);
}
}
}, [api]);

Expand All @@ -171,6 +178,7 @@ export function TemplatesProvider({ children }: { children: ReactNode }) {
setHabitConfig(null);
setExposedFunctions(null);
setHasLoadedOnce(false);
setIsLoading(false);
}
}, [isAuthenticated, reloadTemplates]);

Expand Down
26 changes: 26 additions & 0 deletions hooks/useMutationVersionEffect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useEffect, useRef } from "react";

/**
* Run an effect when mutationVersion changes, without pulling the callback into
* the effect dependency array (avoids "exhaustive-deps" disables).
*/
export function useMutationVersionEffect(
mutationVersion: number,
effect: () => void,
options: { skipInitial?: boolean } = {},
) {
const effectRef = useRef(effect);
const isInitial = useRef(true);

useEffect(() => {
effectRef.current = effect;
}, [effect]);

useEffect(() => {
if (options.skipInitial !== false && isInitial.current) {
isInitial.current = false;
return;
}
effectRef.current();
}, [mutationVersion, options.skipInitial]);
}
Loading