From 25676bf1e7941dbc543564c018d949f291cd1d6e Mon Sep 17 00:00:00 2001 From: fegger Date: Mon, 18 May 2026 22:13:53 +0200 Subject: [PATCH] Simplify route hooks with shared query logic Refactor AddEventModal state management --- .gitignore | 2 + apps/web/src/app/add-event/AddEventModal.tsx | 10 +--- apps/web/src/hooks/useBikeRoute.ts | 52 ++-------------- apps/web/src/hooks/useRouteQuery.ts | 63 ++++++++++++++++++++ apps/web/src/hooks/useWalkRoute.ts | 54 +++-------------- 5 files changed, 81 insertions(+), 100 deletions(-) create mode 100644 apps/web/src/hooks/useRouteQuery.ts diff --git a/.gitignore b/.gitignore index 3254be8..ae9b64a 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ runs/ # next.js build output (apps) apps/web/.next/ + +apps/mobile/log.txt diff --git a/apps/web/src/app/add-event/AddEventModal.tsx b/apps/web/src/app/add-event/AddEventModal.tsx index a331135..3d5ae47 100644 --- a/apps/web/src/app/add-event/AddEventModal.tsx +++ b/apps/web/src/app/add-event/AddEventModal.tsx @@ -33,12 +33,8 @@ const AddEventModal: React.FC = ({ isOpen, onClose, editEven const { addEvent, updateEvent } = useEventsStore(); - // Track previous props to reset form fields when they change (avoids setState in useEffect) - const [prevEditEvent, setPrevEditEvent] = useState(editEvent); - const [prevIsOpen, setPrevIsOpen] = useState(isOpen); - if (editEvent !== prevEditEvent || isOpen !== prevIsOpen) { - setPrevEditEvent(editEvent); - setPrevIsOpen(isOpen); + useEffect(() => { + if (!isOpen) return; if (editEvent) { setTitle(editEvent.title); setDestination(editEvent.destination); @@ -51,7 +47,7 @@ const AddEventModal: React.FC = ({ isOpen, onClose, editEven setEventTime(""); } setSuccess(false); - } + }, [editEvent, isOpen]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); diff --git a/apps/web/src/hooks/useBikeRoute.ts b/apps/web/src/hooks/useBikeRoute.ts index 49316c0..0906732 100644 --- a/apps/web/src/hooks/useBikeRoute.ts +++ b/apps/web/src/hooks/useBikeRoute.ts @@ -1,56 +1,16 @@ -import { useState, useEffect } from "react"; import type { BikeRoute } from "@timetoleave/core"; -import { ApiClient } from "@timetoleave/api-client"; +import { api } from "@/lib/api"; +import { useRouteQuery } from "./useRouteQuery"; -const client = new ApiClient(); - -/** - * Bike route hook. Fetches a bicycle route from origin to destination - * via OSRM. Skips the request if any coordinate is undefined. - */ export function useBikeRoute( fromLat: number | undefined, fromLng: number | undefined, toLat: number | undefined, toLng: number | undefined, ) { - const [bikeRoute, setBikeRoute] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - let isMounted = true; - - const fetchRoute = async () => { - if (fromLat == null || fromLng == null || toLat == null || toLng == null) { - return; - } - - setLoading(true); - setError(null); - - try { - const data = await client.getBikeRoute(fromLat, fromLng, toLat, toLng); - - if (isMounted) { - setBikeRoute(data); - setLoading(false); - } - } catch (err: unknown) { - if (isMounted) { - const message = err instanceof Error ? err.message : "Failed to fetch bike route"; - setError(message); - setLoading(false); - } - } - }; - - fetchRoute(); - - return () => { - isMounted = false; - }; - }, [fromLat, fromLng, toLat, toLng]); - + const { data: bikeRoute, loading, error } = useRouteQuery( + (a, b, c, d) => api.getBikeRoute(a, b, c, d), + fromLat, fromLng, toLat, toLng, + ); return { bikeRoute, loading, error }; } diff --git a/apps/web/src/hooks/useRouteQuery.ts b/apps/web/src/hooks/useRouteQuery.ts new file mode 100644 index 0000000..42da935 --- /dev/null +++ b/apps/web/src/hooks/useRouteQuery.ts @@ -0,0 +1,63 @@ +import { useState, useEffect, useRef } from "react"; + +type RouteFetcher = (fromLat: number, fromLng: number, toLat: number, toLng: number) => Promise; + +/** + * Shared fetch logic for OSRM-backed route hooks (bike, walk). + * Uses a ref for the fetcher so swapping it never triggers a re-fetch — + * only coordinate changes do. + * + * @param clearOnMissing - When true, resets data/error to null when coords are absent. + */ +export function useRouteQuery( + fetcher: RouteFetcher, + fromLat: number | undefined, + fromLng: number | undefined, + toLat: number | undefined, + toLng: number | undefined, + clearOnMissing = false, +) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetcherRef = useRef(fetcher); + fetcherRef.current = fetcher; + + useEffect(() => { + let isMounted = true; + + if (fromLat == null || fromLng == null || toLat == null || toLng == null) { + if (clearOnMissing) { + setData(null); + setLoading(false); + setError(null); + } + return; + } + + setLoading(true); + setError(null); + + fetcherRef.current(fromLat, fromLng, toLat, toLng) + .then((result) => { + if (isMounted) { + setData(result); + setLoading(false); + } + }) + .catch((err: unknown) => { + if (isMounted) { + setError(err instanceof Error ? err.message : "Failed to fetch route"); + setLoading(false); + } + }); + + return () => { + isMounted = false; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fromLat, fromLng, toLat, toLng, clearOnMissing]); + + return { data, loading, error }; +} diff --git a/apps/web/src/hooks/useWalkRoute.ts b/apps/web/src/hooks/useWalkRoute.ts index aaae7e9..17dcd99 100644 --- a/apps/web/src/hooks/useWalkRoute.ts +++ b/apps/web/src/hooks/useWalkRoute.ts @@ -1,57 +1,17 @@ -import { useState, useEffect } from "react"; import type { WalkRoute } from "@timetoleave/core"; -import { api as client } from "@/lib/api"; +import { api } from "@/lib/api"; +import { useRouteQuery } from "./useRouteQuery"; -/** - * Walk route hook. Fetches a walking route between two coordinates - * via OSRM (proxied through the server). Clears state when coordinates are null. - */ export function useWalkRoute( fromLat: number | undefined, fromLng: number | undefined, toLat: number | undefined, toLng: number | undefined, ) { - const [walkRoute, setWalkRoute] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - let isMounted = true; - - const fetchRoute = async () => { - if (fromLat == null || fromLng == null || toLat == null || toLng == null) { - setWalkRoute(null); - setLoading(false); - setError(null); - return; - } - - setLoading(true); - setError(null); - - try { - const data = await client.getWalkRoute(fromLat, fromLng, toLat, toLng); - - if (isMounted) { - setWalkRoute(data); - setLoading(false); - } - } catch (err: unknown) { - if (isMounted) { - const message = err instanceof Error ? err.message : "Failed to fetch walk route"; - setError(message); - setLoading(false); - } - } - }; - - fetchRoute(); - - return () => { - isMounted = false; - }; - }, [fromLat, fromLng, toLat, toLng]); - + const { data: walkRoute, loading, error } = useRouteQuery( + (a, b, c, d) => api.getWalkRoute(a, b, c, d), + fromLat, fromLng, toLat, toLng, + true, + ); return { walkRoute, loading, error }; }