afv-library/samples/webapp-template-app-react-sample-b2e-experimental/.a4drules/skills/building-weather-widget/implementation/weather-hook.md

6.7 KiB
Raw Blame History

Weather Hook — Implementation Guide

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 116 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;