Hey there! Let's build a weather dashboard using React, TypeScript, and the OpenWeather API. This tutorial will teach you how to handle API requests, manage loading states, and create a clean UI for weather data.
What We'll Build
Prerequisites
- Basic React knowledge
- Node.js installed
- OpenWeather API key (I'll show you how to get one)
Getting Started
1. Set Up the Project
npm create vite@latest weather-dashboard -- --template react-ts
cd weather-dashboard
npm install
Install dependencies:
npm install axios
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
2. Get Your API Key
- Go to OpenWeather
- Sign up for a free account
- Get your API key from your dashboard
3. Set Up Environment Variables
Create .env
file in root:
VITE_API_KEY=your_api_key_here
VITE_API_URL=https://api.openweathermap.org/data/2.5
4. Create Types
Create src/types/index.ts
:
export interface WeatherData {
main: {
temp: number;
feels_like: number;
humidity: number;
pressure: number;
};
weather: Array<{
main: string;
description: string;
icon: string;
}>;
wind: {
speed: number;
};
name: string;
}
export interface WeatherError {
message: string;
}
5. Create API Service
Create src/services/weatherAPI.ts
:
import axios from 'axios';
import { WeatherData } from '../types';
const API_KEY = import.meta.env.VITE_API_KEY;
const API_URL = import.meta.env.VITE_API_URL;
export const getWeather = async (city: string): Promise<WeatherData> => {
try {
const response = await axios.get(
`${API_URL}/weather?q=${city}&appid=${API_KEY}&units=metric`
);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(error.response?.data.message || 'Failed to fetch weather data');
}
throw error;
}
};
6. Create Components
Create src/components/SearchBar.tsx
:
import { useState } from 'react';
interface SearchBarProps {
onSearch: (city: string) => void;
isLoading: boolean;
}
export function SearchBar({ onSearch, isLoading }: SearchBarProps) {
const [city, setCity] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (city.trim()) {
onSearch(city);
}
};
return (
<form onSubmit={handleSubmit} className="flex gap-2">
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="Enter city name..."
className="flex-1 p-2 rounded-lg border focus:outline-none focus:border-blue-500"
/>
<button
type="submit"
disabled={isLoading}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600
disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{isLoading ? 'Searching...' : 'Search'}
</button>
</form>
);
}
Create src/components/WeatherCard.tsx
:
import { WeatherData } from '../types';
interface WeatherCardProps {
data: WeatherData;
}
export function WeatherCard({ data }: WeatherCardProps) {
return (
<div className="bg-white rounded-lg shadow-lg p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold">{data.name}</h2>
<img
src={`http://openweathermap.org/img/wn/${data.weather[0].icon}@2x.png`}
alt={data.weather[0].description}
className="w-16 h-16"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-4xl font-bold">{Math.round(data.main.temp)}°C</p>
<p className="text-gray-500">{data.weather[0].main}</p>
</div>
<div className="space-y-2">
<p className="text-gray-600">
Feels like: {Math.round(data.main.feels_like)}°C
</p>
<p className="text-gray-600">
Humidity: {data.main.humidity}%
</p>
<p className="text-gray-600">
Wind: {data.wind.speed} m/s
</p>
</div>
</div>
</div>
);
}
7. Update App Component
Update src/App.tsx
:
import { useState } from 'react';
import { SearchBar } from './components/SearchBar';
import { WeatherCard } from './components/WeatherCard';
import { getWeather } from './services/weatherAPI';
import { WeatherData, WeatherError } from './types';
function App() {
const [weather, setWeather] = useState<WeatherData | null>(null);
const [error, setError] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const handleSearch = async (city: string) => {
try {
setIsLoading(true);
setError('');
const data = await getWeather(city);
setWeather(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
setWeather(null);
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-100 py-8 px-4">
<div className="max-w-md mx-auto space-y-4">
<h1 className="text-3xl font-bold text-center mb-8">
Weather Dashboard
</h1>
<SearchBar onSearch={handleSearch} isLoading={isLoading} />
{error && (
<div className="p-4 bg-red-100 text-red-700 rounded-lg">
{error}
</div>
)}
{weather && <WeatherCard data={weather} />}
</div>
</div>
);
}
export default App;
8. Update Tailwind Config
Update tailwind.config.js
:
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
Add Tailwind to your src/index.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
9. Additional Features (Optional)
- Add loading skeleton:
create this file
src/component/LoadingSkeleton.tsx
export function LoadingSkeleton() {
return (
<div className="animate-pulse bg-white rounded-lg shadow-lg p-6">
<div className="h-8 bg-gray-200 rounded w-1/2 mb-4"></div>
<div className="space-y-3">
<div className="h-6 bg-gray-200 rounded"></div>
<div className="h-6 bg-gray-200 rounded w-3/4"></div>
</div>
</div>
);
}
update src/App.tsx
{isLoading && <LoadingSkeleton />}
{!isLoading && weather && <WeatherCard data={weather} />}
{!isLoading && error && (
<div className="p-4 bg-red-100 text-red-700 rounded-lg">
{error}
</div>
)}
Best Practices and Tips
- Loading States
// Show different states based on loading
{isLoading && <LoadingSkeleton />}
{!isLoading && weather && <WeatherCard data={weather} />}
{!isLoading && error && <ErrorMessage message={error} />}
- Unit Testing Example (using Vitest)
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { WeatherCard } from './WeatherCard';
describe('WeatherCard', () => {
it('displays weather information correctly', () => {
const mockData = {
name: 'London',
main: {
temp: 20,
feels_like: 22,
humidity: 80,
pressure: 1013
},
weather: [{
main: 'Clear',
description: 'clear sky',
icon: '01d'
}],
wind: {
speed: 5
}
};
render(<WeatherCard data={mockData} />);
expect(screen.getByText('London')).toBeInTheDocument();
expect(screen.getByText('20°C')).toBeInTheDocument();
});
});
Next Steps
You can enhance this project by:
- Adding 5-day forecast
- Saving favorite cities
- Adding geolocation support
- Adding weather maps
- Adding more weather details
- Adding unit conversion (Celsius/Fahrenheit)
Common Issues and Solutions
- CORS Issues
// Use proxy in vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.openweathermap.org',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
});
- API Key Security
// Always use environment variables
// Never commit .env files
// Consider using a backend proxy
That's it! You now have a functional weather dashboard that fetches real data from an API. This project teaches you about:
- API integration
- Error handling
- Loading states
- TypeScript with React
- Environment variables
- Component organization
Check the repository to get the full source
Happy coding! 🚀
Top comments (1)
Very Useful Post thanks