π οΈ Weekend Project: Frontend Frameworks & State
This is the capstone for the module: a hands-on weekend build where you turn everything you learned about components, state, and data flow into a real, working Weather Dashboard. You'll work through it in clear milestones, tick off a checklist as you go, and measure your result against a "what good looks like" bar.
π― Learning Objectives
By the end of this project, you will be able to:
- Scaffold a single-page app in a modern framework (React, Vue, or Svelte) and structure it into clear components
- Design a state model β decide what is local component state, what is shared app state, and what is server data
- Fetch, cache, and render live data from a public API with proper loading and error states
- Persist user choices (favorite locations, units) so they survive a page reload
- Self-assess your build against a concrete quality checklist
Estimated Time: 6β10 hours (a focused weekend) β’ Difficulty: Intermediate
Hands-on: This whole lesson is the hands-on. You ship a running app by Sunday night.
In This Lesson
What You're Building
You'll build a Weather Dashboard: a small single-page app that lets a user search for a city, see current conditions and a short forecast, and pin a few favorite locations that are remembered between visits. It's deliberately modest in scope but rich in the skills that matter β component composition, state management, asynchronous data, and persistence.
Pick one framework you want to practice. The concepts are identical across all three; only the syntax differs.
π Choose your framework
React β hooks (useState, useEffect), plus a Context or a small store for shared state. Scaffold with npm create vite@latest -- --template react.
Vue 3 β the Composition API (ref, computed, watch) and a Pinia store. Scaffold with npm create vue@latest.
Svelte β reactive $state/stores and dead-simple bindings. Scaffold with npm create svelte@latest.
The finished feature set is small and testable:
- Search a city by name and show current temperature, conditions, humidity, and wind.
- Show a compact multi-day forecast.
- Save and remove favorite cities; favorites survive a reload.
- Toggle Β°C / Β°F, and remember that choice too.
- Handle the boring-but-critical states: loading, empty, and error.
π‘ Get a free API key first
Sign up for a free key at OpenWeatherMap (or Open-Meteo, which needs no key at all). Keep the key out of your committed code β put it in a .env file and reference it via your bundler's env system (e.g. import.meta.env.VITE_WEATHER_KEY in Vite).
How to Approach It (Polya's Method)
Rather than diving straight into code, borrow a four-step problem-solving loop from mathematician George PΓ³lya. It keeps a weekend build from turning into a weekend of thrashing.
the problem] --> B[2. Devise
a plan] B --> C[3. Execute
the plan] C --> D[4. Review
& extend] D -->|new insight| A
| Step | For this project, that means⦠|
|---|---|
| Understand | List the exact user actions and the data each screen needs. Write them down before opening the editor. |
| Plan | Sketch the component tree and the state model (next section). Decide the milestones and their order. |
| Execute | Build one vertical slice at a time β search working end-to-end before you touch favorites. |
| Review | Run through the checklist, fix the gaps, then pick one stretch goal. |
β οΈ Resist the urge to build everything at once. A vertical slice β search box β API call β rendered card β that works end-to-end teaches you more in an hour than a half-finished UI for every feature does in a day.
Design the State First
State management is the real subject of this module, so plan it deliberately. The single most useful question you can ask about any piece of data is: who owns it, and who needs to see it? That answer tells you where it lives.
β οΈ The classic beginner mistake
Dumping everything into a global store "just in case." Global state you don't need makes components harder to reuse and re-renders harder to reason about. Start local; lift up only when sharing forces you to.
Here's a reasonable shape for the shared store β notice it holds user choices and the active selection, but the fetched weather is cached separately, keyed by city:
// The shared, persisted slice of state
const appState = {
favorites: ['London', 'Tokyo'], // saved cities
unit: 'metric', // 'metric' (Β°C) or 'imperial' (Β°F)
activeCity: 'London', // which city the dashboard shows
};
// Server data is cached separately, keyed by "city:unit"
// so switching units or revisiting a city can reuse a recent result.
const weatherCache = new Map(); // 'London:metric' -> { data, fetchedAt }
The Milestones
Work top to bottom. Each milestone leaves you with something that runs β commit at the end of every one so you always have a working fallback.
Milestone 0 β Scaffold & component tree
Create the project, delete the boilerplate, and stub out empty components so the tree is visible. A workable breakdown:
App
βββ SearchBar (local state: input text)
βββ UnitToggle (reads/writes shared: unit)
βββ FavoritesList (reads shared: favorites; click sets activeCity)
β βββ FavoriteItem
βββ WeatherPanel (reads shared: activeCity, unit; fetches server data)
βββ CurrentCard
βββ Forecast
βββ ForecastDay
Milestone 1 β Search one city, end-to-end
Wire the search box to the API and render a current-conditions card. Don't worry about favorites or units yet β just prove the data pipeline works for a single hard-coded unit.
Milestone 2 β Loading & error states
Every fetch has three outcomes, not one. Show a spinner or skeleton while loading, a friendly message on failure (bad city name, network down), and the data on success. This is where amateur apps and solid apps diverge.
Milestone 3 β Shared store: favorites & unit
Introduce your store. Move favorites, unit, and activeCity into it. Now clicking a favorite updates the panel, and the unit toggle re-renders everything consistently.
Milestone 4 β Persist to localStorage
Read the store's initial value from localStorage on startup, and write back whenever favorites or unit change. Reload the page β your choices should still be there.
Milestone 5 β Forecast & polish
Add the multi-day forecast, tidy the layout so it's responsive, add icons, and make sure keyboard users can operate the search and favorites.
Worked Example: The Data Layer
The trickiest, most reusable piece is the fetch-and-cache function. Building it once, cleanly, makes every framework's component code trivial. Here it is framework-agnostic β plain modern JavaScript you can import anywhere.
// src/lib/weather.js
const BASE = 'https://api.openweathermap.org/data/2.5';
const KEY = import.meta.env.VITE_WEATHER_KEY;
const cache = new Map(); // 'city:unit' -> { data, fetchedAt }
const MAX_AGE = 10 * 60 * 1000; // 10 minutes
export async function getWeather(city, unit = 'metric') {
const key = `${city.toLowerCase()}:${unit}`;
const cached = cache.get(key);
if (cached && Date.now() - cached.fetchedAt < MAX_AGE) {
return cached.data; // fresh enough β skip the network
}
const url = `${BASE}/weather?q=${encodeURIComponent(city)}`
+ `&units=${unit}&appid=${KEY}`;
const res = await fetch(url);
if (res.status === 404) throw new Error(`City "${city}" not found.`);
if (!res.ok) throw new Error(`Weather service error (${res.status}).`);
const data = await res.json();
cache.set(key, { data, fetchedAt: Date.now() });
return data;
}
Now a React component that consumes it stays small β all the messy caching lives elsewhere:
// src/components/WeatherPanel.jsx
import { useState, useEffect } from 'react';
import { getWeather } from '../lib/weather';
export default function WeatherPanel({ city, unit }) {
const [state, setState] = useState({ status: 'idle', data: null, error: null });
useEffect(() => {
if (!city) return;
let cancelled = false;
setState({ status: 'loading', data: null, error: null });
getWeather(city, unit)
.then((data) => { if (!cancelled) setState({ status: 'success', data, error: null }); })
.catch((err) => { if (!cancelled) setState({ status: 'error', data: null, error: err.message }); });
return () => { cancelled = true; }; // ignore stale responses
}, [city, unit]);
if (state.status === 'loading') return <p role="status">Loading {city}β¦</p>;
if (state.status === 'error') return <p role="alert">{state.error}</p>;
if (!state.data) return <p>Search for a city to begin.</p>;
const { name, main, weather } = state.data;
const symbol = unit === 'metric' ? 'Β°C' : 'Β°F';
return (
<article className="current-card">
<h3>{name}</h3>
<p className="temp">{Math.round(main.temp)}{symbol}</p>
<p>{weather[0].description} Β· humidity {main.humidity}%</p>
</article>
);
}
π‘ Why the cancelled flag matters
If a user searches "Paris" then quickly "Tokyo", two requests are in flight. Without the cleanup flag, a slow Paris response could arrive last and overwrite Tokyo β a classic race condition. The cleanup function ignores any response from an effect that's already been superseded.
And the persistence helper for Milestone 4 β small, and reused for favorites and unit alike:
// src/lib/storage.js
export function load(key, fallback) {
try {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : fallback;
} catch {
return fallback; // corrupted or unavailable storage β degrade gracefully
}
}
export function save(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
/* storage full or blocked β ignore, app still works in-memory */
}
}
Build Checklist
Tick these off as you go. If every box is checked, you've hit the core bar for the project.
β Core requirements
- Project scaffolds and runs with a single
npm run dev. - Searching a valid city shows current temperature, conditions, humidity, and wind.
- An invalid city shows a clear error message β the app does not crash or hang.
- A loading indicator appears while a request is in flight.
- Favorites can be added and removed; clicking one loads its weather.
- The Β°C / Β°F toggle updates all displayed temperatures consistently.
- Favorites and unit choice survive a full page reload.
- The API key lives in
.env, not in committed source. - Layout is usable on a phone-width screen.
- Search and favorites are operable with the keyboard alone.
β οΈ Common gaps to double-check
- The empty first-load state (no city selected yet) β does it say something helpful?
- Rapid searches β does the last one always win? (See the
cancelledflag.) - Adding a city that's already a favorite β is it de-duplicated?
- Is temperature rounded, or does it show
17.34Β°C?
What Good Looks Like
Passing the checklist means it works. These qualities are what separate a "works on my machine" demo from something you'd be glad to show an interviewer.
| Dimension | Just okay | What good looks like |
|---|---|---|
| State | Everything in one giant global object. | Local state stays local; only genuinely shared values live in the store; server data is cached separately. |
| Async | Only the happy path is handled. | Loading, empty, and error states are all deliberate; races can't corrupt the UI. |
| Components | One 400-line component. | Small, single-purpose components with clear props; data fetching separated from presentation. |
| Resilience | Crashes on bad input or storage errors. | Degrades gracefully β corrupted storage, offline, and bad city names are all survivable. |
| Accessibility | Mouse-only, no status messaging. | Keyboard operable, role="status"/role="alert" for async feedback, sufficient contrast. |
| Repo | No README, secrets committed. | A README explaining choices, .env.example committed instead of the real key, tidy commit history. |
π‘ Self-review ritual
Before you call it done, open the app in a fresh browser profile, disable your network in DevTools, feed it a nonsense city name, and reload it twice. If it stays calm and informative through all of that, it's good.
Stretch Goals
Finished early, or want to push the state-management muscle harder? Pick one β depth beats breadth.
- Geolocation: on first load, ask permission and default the dashboard to the user's current location.
- Derived state: add a "feels like vs. actual" badge computed from the data rather than stored β practice
computed/useMemo. - Optimistic UI: when adding a favorite, show it immediately and roll back only if something fails.
- Offline-first: register a service worker so the last-viewed cities render even with no network (a callback to the previous lesson).
- Testing: write a couple of unit tests for
getWeather's cache and error handling. - Deploy: ship it to Netlify or Vercel and put the live URL in your README.
ποΈ Reflection exercise
Objective: Cement the state-design thinking, not just the code.
After you finish, write a short paragraph in your README answering: which values did you keep local, which did you lift into the store, and why? Name one value you were tempted to make global but didn't need to.
π‘ Hint
The search input text is the classic "tempted to globalize but shouldn't." Only WeatherPanel needs the result of a search (the active city), not the raw keystrokes.
β Sample answer
"Search text and the favorites-dropdown-open flag stayed local to their components β nothing else cares about them. favorites, unit, and activeCity went into the store because the panel, the toggle, and the favorites list all read or write them. Fetched weather isn't in the store at all; it's cached by city so it can be refetched independently of user choices. I was tempted to put the search text in the store to 'sync' it, but the only thing other components need is the chosen city, which I set on submit."
Summary & Quiz
π Key Takeaways
- Attack a build in vertical slices and milestones β always keep something that runs.
- Design state before UI: local for transient UI, a shared store for genuinely shared values, and a separate cache for server data.
- Async isn't done until loading, empty, and error states are handled and races can't corrupt the view.
- Persistence via
localStorageshould degrade gracefully when storage is unavailable. - "Done" is the checklist; "good" is small components, resilience, accessibility, and a readable repo.
π― Quick Quiz
Question 1: The user's search-box text should normally live where?
Question 2: Why does the fetch effect use a cancelled cleanup flag?
Question 3: Which practice best matches the "what good looks like" bar for handling storage?
π Further Reading
- React β Managing State
- Pinia β Vue's official store
- Open-Meteo β free weather API (no key)
- web.dev β PWA checklist (for the offline stretch goal)
π What's Next?
You've now shipped a real stateful frontend. Next module we turn to the server side of security: Authentication Models and Workflows β how apps prove who a user is and keep them signed in safely.
π Ship it!
Commit your final milestone, write that README, and give yourself credit β you built and reasoned about a complete stateful app in a weekend.