Simplify route hooks with shared query logic

Refactor AddEventModal state management
This commit is contained in:
2026-05-18 22:13:53 +02:00
parent 9c1f6ebf7e
commit 25676bf1e7
5 changed files with 81 additions and 100 deletions
+3 -7
View File
@@ -33,12 +33,8 @@ const AddEventModal: React.FC<AddEventModalProps> = ({ 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<Event | undefined>(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<AddEventModalProps> = ({ isOpen, onClose, editEven
setEventTime("");
}
setSuccess(false);
}
}, [editEvent, isOpen]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
+6 -46
View File
@@ -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<BikeRoute | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(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<BikeRoute>(
(a, b, c, d) => api.getBikeRoute(a, b, c, d),
fromLat, fromLng, toLat, toLng,
);
return { bikeRoute, loading, error };
}
+63
View File
@@ -0,0 +1,63 @@
import { useState, useEffect, useRef } from "react";
type RouteFetcher<T> = (fromLat: number, fromLng: number, toLat: number, toLng: number) => Promise<T>;
/**
* 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<T>(
fetcher: RouteFetcher<T>,
fromLat: number | undefined,
fromLng: number | undefined,
toLat: number | undefined,
toLng: number | undefined,
clearOnMissing = false,
) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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 };
}
+7 -47
View File
@@ -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<WalkRoute | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(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<WalkRoute>(
(a, b, c, d) => api.getWalkRoute(a, b, c, d),
fromLat, fromLng, toLat, toLng,
true,
);
return { walkRoute, loading, error };
}