mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-03 13:44:09 +08:00
6.7 KiB
6.7 KiB
Weather Hook — Implementation Guide
Recommended API: Open-Meteo
| Property | Value |
|---|---|
| Endpoint | https://api.open-meteo.com/v1/forecast |
| Cost | Free for non-commercial use; no signup |
| API key | None required |
| Rate limit | 10,000 requests/day |
| Data | Current conditions, hourly, daily, historical |
| Weather codes | WMO standard codes |
Why Open-Meteo over alternatives
| Provider | API key | Free tier | Notes |
|---|---|---|---|
| Open-Meteo | No | 10K/day | Best for prototypes and low-traffic; no auth required |
| OpenWeatherMap | Yes | 1K calls/day | Popular but requires signup and API key |
| WeatherAPI.com | Yes | 1M calls/month | Good free tier but requires key management |
| Visual Crossing | Yes | 1K calls/day | Historical data strength |
Open-Meteo is the default choice because it requires zero configuration.
WMO weather codes
Open-Meteo returns standard WMO weather codes. Map them to human-readable labels:
const WEATHER_LABELS: Record<number, string> = {
0: "Clear",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
45: "Foggy",
48: "Depositing rime fog",
51: "Light drizzle",
53: "Drizzle",
55: "Dense drizzle",
61: "Slight rain",
63: "Rain",
65: "Heavy rain",
71: "Slight snow",
73: "Snow",
75: "Heavy snow",
77: "Snow grains",
80: "Slight rain showers",
81: "Rain showers",
82: "Heavy rain showers",
85: "Slight snow showers",
86: "Heavy snow showers",
95: "Thunderstorm",
96: "Thunderstorm + hail",
99: "Thunderstorm + heavy hail",
};
function weatherLabel(code: number): string {
return WEATHER_LABELS[code] ?? "Unknown";
}
Temperature conversion
Open-Meteo returns Celsius by default. Convert to Fahrenheit when needed:
function celsiusToFahrenheit(c: number): number {
return Math.round((c * 9) / 5 + 32);
}
Alternatively, request Fahrenheit directly via &temperature_unit=fahrenheit in the query string.
TypeScript interfaces
export interface WeatherCurrent {
description: string;
tempF: number;
humidity: number;
windSpeedKmh: number;
windSpeedMph: number;
}
export interface WeatherHour {
time: string;
tempF: number;
}
export interface WeatherData {
current: WeatherCurrent;
hourly: WeatherHour[];
timezone: string;
}
The useWeather hook
Create at hooks/useWeather.ts:
import { useState, useEffect } from "react";
const DEFAULT_LAT = 37.7749;
const DEFAULT_LNG = -122.4194;
async function fetchWeather(lat: number, lng: number): Promise<WeatherData> {
const url = new URL("https://api.open-meteo.com/v1/forecast");
url.searchParams.set("latitude", String(lat));
url.searchParams.set("longitude", String(lng));
url.searchParams.set("current", "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m");
url.searchParams.set("hourly", "temperature_2m");
url.searchParams.set("timezone", "auto");
url.searchParams.set("forecast_days", "1");
const res = await fetch(url.toString());
if (!res.ok) throw new Error(`Weather API error: ${res.status}`);
const data = await res.json();
const cur = data.current ?? {};
const tempC = cur.temperature_2m ?? 0;
const humidity = cur.relative_humidity_2m ?? 0;
const windKmh = cur.wind_speed_10m ?? 0;
const windMph = Math.round(windKmh * 0.621371 * 10) / 10;
const code = cur.weather_code ?? 0;
const hourly: WeatherHour[] = [];
const times: string[] = data.hourly?.time ?? [];
const temps: (number | null)[] = data.hourly?.temperature_2m ?? [];
const now = new Date();
for (let i = 0; i < times.length; i++) {
const t = times[i];
const temp = temps[i];
if (t && temp != null) {
const d = new Date(t);
if (d >= now) {
hourly.push({
time: d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true }),
tempF: celsiusToFahrenheit(temp),
});
}
}
if (hourly.length >= 6) break;
}
return {
current: {
description: weatherLabel(code),
tempF: celsiusToFahrenheit(tempC),
humidity,
windSpeedKmh: windKmh,
windSpeedMph: windMph,
},
hourly,
timezone: data.timezone ?? "auto",
};
}
export function useWeather(lat?: number | null, lng?: number | null) {
const latitude = lat ?? DEFAULT_LAT;
const longitude = lng ?? DEFAULT_LNG;
const [data, setData] = useState<WeatherData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
fetchWeather(latitude, longitude)
.then((d) => {
if (!cancelled) setData(d);
})
.catch((e) => {
if (!cancelled) setError(e instanceof Error ? e.message : "Failed to load weather");
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [latitude, longitude]);
return { data, loading, error };
}
Hook design patterns
| Pattern | Why |
|---|---|
Cancellation flag (cancelled) |
Prevents state updates on unmounted components |
| Default coordinates | Widget works immediately without user location |
Dependency array [latitude, longitude] |
Re-fetches only when location changes |
Separate loading and error states |
Enables distinct UI for each state |
Open-Meteo API parameters reference
Current weather variables
| Variable | Description |
|---|---|
temperature_2m |
Air temperature at 2m height (°C) |
relative_humidity_2m |
Relative humidity (%) |
weather_code |
WMO weather code |
wind_speed_10m |
Wind speed at 10m height (km/h) |
apparent_temperature |
Feels-like temperature (°C) |
precipitation |
Precipitation sum (mm) |
Hourly variables
| Variable | Description |
|---|---|
temperature_2m |
Temperature each hour |
precipitation_probability |
Chance of rain (%) |
weather_code |
Condition each hour |
Daily variables
| Variable | Description |
|---|---|
temperature_2m_max |
Daily high |
temperature_2m_min |
Daily low |
weather_code |
Dominant condition |
sunrise |
Sunrise time (ISO) |
sunset |
Sunset time (ISO) |
Useful query parameters
| Param | Example | Purpose |
|---|---|---|
timezone |
auto or America/Los_Angeles |
Localizes times |
forecast_days |
1–16 |
Number of days |
temperature_unit |
fahrenheit |
Direct °F responses |
wind_speed_unit |
mph |
Direct mph responses |
CSP considerations
If CSP is enforced, add Open-Meteo to connect-src:
connect-src 'self' https://api.open-meteo.com;