Weather App
A RadiantElement host that fetches live weather data from Open-Meteo, shares it through context, and lets a child summary card subscribe to a single slice.
Weather
The host fetches each city from Open-Meteo on connect and on click. The summary rerenders from the visible report stored in context.
Pick a city to load the current forecast.
The example covers:
createResource(this, {...})for abortable, source-driven async fetches@provideContexton the host for shared state ownership@contextSelectorwithselecton the summary card for read-only slice subscriptions@contextSelectorwithoutselecton the host for full-context access inrender()- stale-while-revalidate rendering via
staleTimeandpendingDelay
createResource(...) is available from @ecopages/radiant. Prefer importing it from the same entry as RadiantElement / @state so your bundler keeps a single @ecopages/signals instance. The focused subpath @ecopages/radiant/signals/host-resource remains available for tree-shaking, but it must resolve to the same signals copy as the rest of Radiant.
Architecture
The host owns the active city id as @state, the provider, and the resource lifecycle. Because source reads ctx.host.activeCityId, city changes automatically abort older requests and start a new fetch — no manual refetch() needed. Successful responses update the shared context via this.provider.setContext(...).
The summary card only cares about the visible report. It subscribes to that slice with @contextSelector({ select: ... }) and reads this.visibleReport directly in render(). It does not need @consumeContext because it never calls setContext(...).
Utility code (city list, fetch helper, types) lives in weather-app.utils.ts and weather-app.types.ts. The example below focuses on the Radiant-specific part.
Summary Card
import {
RadiantElement,
contextSelector,
customElement,
} from '@ecopages/radiant';
import type { WeatherReport } from './weather-app.types';
// weatherContext created via createContext<WeatherContext>(Symbol('weather-context'))
@customElement('radiant-weather-summary')
export class RadiantWeatherSummary extends RadiantElement {
@contextSelector({
context: weatherContext,
select: ({ visibleReport }) => visibleReport,
})
visibleReport: WeatherReport | undefined;
override render() {
if (!this.visibleReport) return null;
return (
<article data={{ city: this.visibleReport.city }}>
<p>{this.visibleReport.city}</p>
<p>{this.visibleReport.condition}</p>
<p>
{this.visibleReport.temperature}
<span>°C</span>
</p>
<p>{this.visibleReport.summary}</p>
<dl>
<div>
<dt>Humidity</dt>
<dd>{this.visibleReport.humidity}%</dd>
</div>
<div>
<dt>Wind</dt>
<dd>{this.visibleReport.windKph} km/h</dd>
</div>
</dl>
</article>
);
}
}@contextSelector on the field keeps the value in sync and schedules requestUpdate() automatically when the selected slice changes on a render-owning host.
Host
import {
type ContextProvider,
RadiantElement,
contextSelector,
createContext,
createResource,
customElement,
provideContext,
state,
} from '@ecopages/radiant';
import type { WeatherCity, WeatherContext, WeatherReport } from './weather-app.types';
import {
DEFAULT_CITY_ID,
WEATHER_CITIES,
fetchWeatherReport,
getWeatherCity,
upsertWeatherReport,
} from './weather-app.utils';
const weatherContext = createContext<WeatherContext>(Symbol('weather-context'));
@customElement('radiant-weather-app')
export class RadiantWeatherAppElement extends RadiantElement {
@state activeCityId = DEFAULT_CITY_ID;
@provideContext<typeof weatherContext>({
context: weatherContext,
initialValue: {
activeCityId: DEFAULT_CITY_ID,
reports: [],
visibleReport: undefined,
},
})
provider!: ContextProvider<typeof weatherContext>;
private weatherQuery = createResource(this, {
pendingDelay: 500,
staleTime: 1 * 60 * 1000,
source: (ctx) => ctx.host.activeCityId,
fetcher: (cityId, ctx) => fetchWeatherReport(getWeatherCity(cityId), ctx.signal),
onSuccess: (report, ctx) => {
ctx.host.provider.setContext({
activeCityId: report.cityId,
reports: upsertWeatherReport(ctx.host.provider.getContext().reports, report),
visibleReport: report,
});
},
});
@contextSelector({ context: weatherContext })
weatherState: WeatherContext = {
activeCityId: DEFAULT_CITY_ID,
reports: [],
visibleReport: undefined,
};
private readonly handleCityClick = (event: Event) => {
const button = event.currentTarget as HTMLButtonElement | null;
const cityId = button?.dataset.cityId;
if (!cityId) return;
this.activeCityId = cityId;
this.provider.setContext({ activeCityId: cityId });
};
override render() {
const status = this.weatherQuery.status.get();
const error = this.weatherQuery.error.get();
const { activeCityId, reports, visibleReport } = this.weatherState;
const activeCity = getWeatherCity(activeCityId);
const activeReport = reports.find((report) => report.cityId === activeCityId);
const isPending = status === 'pending';
return (
<section>
<div>
{WEATHER_CITIES.map((city) => (
<button
type="button"
data={{ cityId: city.id }}
on:click={this.handleCityClick}
aria={{ pressed: city.id === activeCityId }}
>
{city.label}
</button>
))}
</div>
<div>
{visibleReport ? (
<radiant-weather-summary />
) : (
<div>Waiting for {activeCity.label} to resolve...</div>
)}
</div>
</section>
);
}
}The host uses @contextSelector without select so it receives the full WeatherContext. This is necessary here because render() reads activeCityId, reports, and visibleReport together, and some of that logic depends on instance state (weatherQuery.status.get()).
Key Patterns
- Read-only consumers use
@contextSelectoralone. The summary card does not need@consumeContextbecause it never writes to context. - The host uses both
@provideContextand@contextSelector. It writes viaprovider.setContext(...)and reads via theweatherStatefield. createResourcemanages the async lifecycle. Source changes abort previous requests,staleTimecaches responses, andpendingDelayavoids flash-of-loading for fast responses. No manual cleanup is needed.@contextSelectoron a field schedulesrequestUpdate()automatically on render-owning hosts. No empty method body and no manualthis.update()call.
See Also
- Radiant Counter for the smallest host-render example
- Radiant Todo App for a provider-plus-consumer architecture
- Resources for the
createResourceAPI reference - @provideContext, @contextSelector, and @onContextUpdate for the decorator APIs