Ahmed Abdelaziz

Weather Now — Real-Time Weather Web AppDesign Docs

Architecture

How a frontend-only app coordinates live APIs without ever feeling slow — layers, state split, and rendering strategy.

Last updated August 2026

Overview

Weather Now is intentionally a frontend application — no backend, no database. All the engineering lives in one question: how do you coordinate several external APIs, live data, and user preferences in the browser without the app ever feeling slow or jumpy?

Simple rule

Dependencies point inward: pages compose components, components read state, and services are the only code that talks to external APIs.


The Layers

LayerWhat it does
Server shellapp/page.tsx renders nav, heading, and search as static HTML — the first paint costs nothing.
Client islandsOnly Search and ForecastContainer (and their children) hydrate. They own fetching and interactivity.
StateTanStack Query caches the weather payload by coordinates; Zustand holds location, unit preferences (persisted), and panel visibility.
ServicesOne module per external API — Open-Meteo forecast SDK, geocoding search, a BigDataCloud server action, browser geolocation.
UtilsUnit converters and WMO weather-code → icon/description mapping.

Request flow

The weather fetch and the reverse-geocoding call run in parallel, and the result is cached by coordinate pair — revisiting a recent location never refetches.


State in Two Halves

Two kinds of state with different jobs:

  • Server state (TanStack Query) — the weather payload. Cached, deduplicated, retried, keyed by ["weatherData", lat, lng].
  • Client state (Zustand) — active location, unit preferences (persisted to localStorage), units-panel visibility. Selector-based subscriptions mean a preference change re-renders only the widgets that read it.

The unit-switching trick

Data is stored raw in Celsius. Every widget converts at render time using shared converters. Switching units re-renders subscribers instantly — zero network requests.


Rendering Without Jank

The forecast grid is always mounted. While data is pending, each panel shows a shimmer skeleton shaped exactly like the final widget; when data arrives it fills in place.

  • No loading-box swap → no layout shift.
  • No single giant render when the payload resolves → each section fills independently.
  • Hourly rows group once per payload (useMemo), not on every render.
  • Skeleton shimmer respects prefers-reduced-motion.

Accessibility as Architecture

  • Search is a full ARIA 1.2 combobox: role="combobox", aria-expanded, aria-activedescendant, listbox/option semantics, arrow keys + Home + End + Enter + Escape.
  • Units panel: focus moves in on open, Escape returns focus to the trigger; options are real buttons with aria-pressed.
  • Skip link, consistent focus-visible outlines, 44 px minimum targets, meaningful icon alt text (weather descriptions double as alt).

Hard Problems Solved

ProblemRoot causeFix
Stale search results flickered for fast typistsOlder responses resolving after newer ones500 ms debounce + per-request AbortController; superseded requests cancelled
Layout shift + one blocking render (~40 widgets, ~30 images) when data landedLoading box swapped for the whole gridAlways-mounted skeleton containers sized to final layout; parallel fetches; memoized hourly grouping
Open-Meteo variables unreadable by nameSDK exposes values positionally; daily min/max encode as the same typeDocumented index contract in the service layer; WMO codes mapped centrally
Unit switching felt slow or refetchedPer-unit fetchesRaw Celsius storage + render-time conversion from persisted preferences
First visit stalled on the geolocation promptSatellite fix can take seconds; some devices have no GPSCoordinates cached in localStorage, used instantly on revisit; fresh fix overwrites cache silently

Result

A single-page app that paints instantly, never jumps, never lies about search results, and works for keyboard, screen reader, and touch users alike.