Simplify route hooks with shared query logic
Refactor AddEventModal state management
This commit is contained in:
@@ -49,3 +49,5 @@ runs/
|
|||||||
|
|
||||||
# next.js build output (apps)
|
# next.js build output (apps)
|
||||||
apps/web/.next/
|
apps/web/.next/
|
||||||
|
|
||||||
|
apps/mobile/log.txt
|
||||||
|
|||||||
@@ -33,12 +33,8 @@ const AddEventModal: React.FC<AddEventModalProps> = ({ isOpen, onClose, editEven
|
|||||||
|
|
||||||
const { addEvent, updateEvent } = useEventsStore();
|
const { addEvent, updateEvent } = useEventsStore();
|
||||||
|
|
||||||
// Track previous props to reset form fields when they change (avoids setState in useEffect)
|
useEffect(() => {
|
||||||
const [prevEditEvent, setPrevEditEvent] = useState<Event | undefined>(editEvent);
|
if (!isOpen) return;
|
||||||
const [prevIsOpen, setPrevIsOpen] = useState(isOpen);
|
|
||||||
if (editEvent !== prevEditEvent || isOpen !== prevIsOpen) {
|
|
||||||
setPrevEditEvent(editEvent);
|
|
||||||
setPrevIsOpen(isOpen);
|
|
||||||
if (editEvent) {
|
if (editEvent) {
|
||||||
setTitle(editEvent.title);
|
setTitle(editEvent.title);
|
||||||
setDestination(editEvent.destination);
|
setDestination(editEvent.destination);
|
||||||
@@ -51,7 +47,7 @@ const AddEventModal: React.FC<AddEventModalProps> = ({ isOpen, onClose, editEven
|
|||||||
setEventTime("");
|
setEventTime("");
|
||||||
}
|
}
|
||||||
setSuccess(false);
|
setSuccess(false);
|
||||||
}
|
}, [editEvent, isOpen]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -1,56 +1,16 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import type { BikeRoute } from "@timetoleave/core";
|
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(
|
export function useBikeRoute(
|
||||||
fromLat: number | undefined,
|
fromLat: number | undefined,
|
||||||
fromLng: number | undefined,
|
fromLng: number | undefined,
|
||||||
toLat: number | undefined,
|
toLat: number | undefined,
|
||||||
toLng: number | undefined,
|
toLng: number | undefined,
|
||||||
) {
|
) {
|
||||||
const [bikeRoute, setBikeRoute] = useState<BikeRoute | null>(null);
|
const { data: bikeRoute, loading, error } = useRouteQuery<BikeRoute>(
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
(a, b, c, d) => api.getBikeRoute(a, b, c, d),
|
||||||
const [error, setError] = useState<string | null>(null);
|
fromLat, fromLng, toLat, toLng,
|
||||||
|
);
|
||||||
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]);
|
|
||||||
|
|
||||||
return { bikeRoute, loading, error };
|
return { bikeRoute, loading, error };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -1,57 +1,17 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import type { WalkRoute } from "@timetoleave/core";
|
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(
|
export function useWalkRoute(
|
||||||
fromLat: number | undefined,
|
fromLat: number | undefined,
|
||||||
fromLng: number | undefined,
|
fromLng: number | undefined,
|
||||||
toLat: number | undefined,
|
toLat: number | undefined,
|
||||||
toLng: number | undefined,
|
toLng: number | undefined,
|
||||||
) {
|
) {
|
||||||
const [walkRoute, setWalkRoute] = useState<WalkRoute | null>(null);
|
const { data: walkRoute, loading, error } = useRouteQuery<WalkRoute>(
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
(a, b, c, d) => api.getWalkRoute(a, b, c, d),
|
||||||
const [error, setError] = useState<string | null>(null);
|
fromLat, fromLng, toLat, toLng,
|
||||||
|
true,
|
||||||
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]);
|
|
||||||
|
|
||||||
return { walkRoute, loading, error };
|
return { walkRoute, loading, error };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user