Building a React App with AI: It’s All About the Mindset
Creating a successful React web application with AI is surprisingly simple, but there is a catch: You cannot expect to sit down with a half-formed idea, tell an AI "I want a weather site," and get decent results.
AI tools multiply your leverage, not your clarity. To get a production-ready app, you need a clear, structured mindset and a willingness to research the details.
Below is a step-by-step process for creating a weather data visualization app with AI and the key takeaways from each stage.
Stop Being Vague: Know Exactly What You Want
"I want a weather site" is far too vague and leaves dozens of critical technical questions unanswered:
- Where is the data coming from?
- Which metrics do you want to show? (Temperature? Air pressure? Soil moisture? The list is endless.)
- What domain name will you use, and do you need an SSL certificate?
- How should the data actually look on screen?
Before typing a single prompt, you need to do a little homework.
A few minutes of research revealed Open-Meteo.com, a free API that provides historical global weather archives dating back to 1940, solving the data source problem. Next came identifying the hosting environment: a small Virtual Private Server (VPS) with an IP address already mapped to the target domain.
First Attempt: The "Good Enough" Prompt
To kick off the project, the AI was provided with a basic overview of the server specifications, project goals, and desired feature set.
The Initial Prompt
"I have a freshly created 1 vCPU, 2 GB RAM, 50 GB storage VPS. The IP address is 123.156.789.0. The domain pointed at the server is myomnigraph.com.
I want to build a React-based web app that uses the Open-Meteo dataset.
I want to be able to go to the site and select a location/city. The app should have buttons to choose which data I want to display: temp, humidity, air pressure, wind speed. I want to be able to set date ranges, choosing any range between the beginning of the dataset (1940) until the present.
The output should be in a pleasing dark theme, logically organized and labeled. There should be an option to download the displayed data subset as a CSV file. Tell me what I need to do."
How It Went
Believe it or not, this basic prompt actually worked to get things off the ground.
Did it spit out errors? Absolutely! But instead of panicking, terminal and browser error messages were simply fed back into the AI, allowing it to diagnose and fix its own mistakes. The key to getting AI-generated code working is patience.
However, as the application grew, it became clear that vague instructions often resulted in architectural decisions that required significant refactoring later.
The "Ideal" Goal Expression: What Should Have Been Prompted
Once you understand the stack components (build tool, styling framework, deployment web server, and libraries), you can provide a much tighter prompt.
If starting the project from scratch, a prompt like the following would provide the AI with the context needed.
Act as a senior full-stack engineer and system administrator. I want to build, deploy, and host a production-ready, dark-themed React data visualization web application on a fresh VPS instance, using the free Open-Meteo dataset.
Server Specs: 1 vCPU, 2 GB RAM, 50 GB NVMe storage (Debian 12 / Linux).
IP & Domain: IP 123.456.789.0 mapped to myomnigraph.com and www.myomnigraph.com.
Web Server & Security: Nginx reverse proxy serving static production assets, with automated HTTPS/SSL via Let's Encrypt (Certbot).
Build System & Framework: Vite + React (TypeScript SPA architecture to eliminate Node.js runtime memory overhead on a 2 GB server).
Styling: Tailwind CSS configured for a clean, modern dark theme.
Visualizations & Tools: Recharts for responsive time-series plotting, Lucide React for UI icons, and PapaParse for client-side data parsing/export.
Location Search: City autocomplete/search powered by the Open-Meteo Geocoding API.
Historical & Forecast Data Fetching: Seamless date-range selector (supporting dates from 1940 to the present) querying Open-Meteo's Archive API for past dates and Forecast API for current/future dates.
Comprehensive Metric Catalog: Categorized parameter browsing across Atmospheric (temp, humidity, pressure, cloud cover, dew point), Precipitation (rain, snowfall), Wind & Solar (speed, gusts, direction, radiation), and Soil/Agri (soil temp, moisture) metrics.
Interactive Multi-Line Chart: Real-time toggles for parameters, rendering synchronized color-coded time-series lines with customized dark-mode tooltips.
Data Export: A one-click button to export the currently filtered weather dataset directly to a CSV file.
1. Complete Linux/Debian system provisioning commands (swap file creation, Node.js, Nginx setup, Certbot SSL configuration).
2. Complete, error-free source code files (`vite.config.ts`, `src/index.css`, `src/App.tsx`) resolving all TypeScript strictness rules.
3. Step-by-step build (`npm run build`) and deployment instructions.
Hang On There, Cowboy!
You might be looking at that second prompt and thinking:
"I don't even know what half of those technical terms mean!"
That's completely normal.
Building with AI is not about knowing how to write every line of code by hand anymore. It's about learning enough of the vocabulary to ask the right questions, set up the right guardrails, and guide the tool to build what you actually envisioned.
Appendix: The Nitty and the Gritty
This is the end-to-end game plan to get myomnigraph.com live with your Open-Meteo React application.
Because Open-Meteo provides both a Forecast API (for current/recent dates) and an Archive API (for historical data dating back to 1940), the web application will intelligently select the right endpoint depending on the date range you select.
Everything below is optimized to run smoothly within your 1 vCPU / 2 GB RAM server footprint without bogging down system resources.
Phase 1: Server Provisioning & Nginx Setup
Log in to your server via SSH (ssh [email protected]).
1. Enable a Swap File & Update
On a 2 GB RAM instance, creating a small swap file prevents Out-Of-Memory (OOM) errors during software installs or builds.
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
2. Install Nginx and Node.js
# Update repositories
sudo apt update && sudo apt upgrade -y
# Install Nginx
sudo apt install nginx -y
sudo systemctl enable --now nginx
# Install Node.js LTS (v20+)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install nodejs -y
3. Configure Nginx for myomnigraph.com
Create a site configuration file:
sudo nano /etc/nginx/sites-available/myomnigraph.com
Paste the following configuration:
server {
listen 80;
listen [::]:80;
server_name myomnigraph.com www.myomnigraph.com;
root /var/www/myomnigraph.com/dist;
index index.html;
# Handles client-side routing in React
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets aggressively
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
}
Enable the site and restart Nginx:
sudo ln -s /etc/nginx/sites-available/myomnigraph.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
Phase 2: Building the React Application
You can build this app directly on your server or locally and upload the resulting production files. Building it in a directory on the server (/var/www/myomnigraph.com) is very straightforward.
1. Initialize the Vite + React Project
cd /var/www
sudo mkdir myomnigraph.com
sudo chown -R $USER:$USER /var/www/myomnigraph.com
cd /var/www/myomnigraph.com
# Create React + TypeScript project with Vite
npm create vite@latest . -- --template react-ts
# Install core visualization and icon libraries
npm install recharts lucide-react papaparse @types/papaparse
2. Application Source Code
Replace the contents of src/App.tsx with the following complete implementation. It includes:
- Dark theme styling (pure CSS / Tailwind-like layout)
- Location search using Open-Meteo's Geocoding API
- Dataset toggles (Temperature, Relative Humidity, Surface Pressure, Wind Speed)
- Date Range Picker (supports historical archive back to 1940 up to the present)
- CSV Export button (converts the filtered dataset directly to a downloadable file using PapaParse)
Click to view source code
import { useState, useEffect } from 'react';
import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid } from 'recharts';
import { Download, Search, CloudRain, Thermometer, Wind, Gauge, Droplets } from 'lucide-react';
import Papa from 'papaparse';
import { useState, useEffect } from 'react';
import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid } from 'recharts';
import { Download, Search, CloudRain, Thermometer, Wind, Gauge, Droplets } from 'lucide-react';
import Papa from 'papaparse';
interface City {
name: string;
latitude: number;
longitude: number;
country: string;
admin1?: string;
}
interface MetricConfig {
key: string;
label: string;
unit: string;
color: string;
icon: JSX.Element;
}
const METRICS: Record<string, MetricConfig> = {
temperature_2m: { key: 'temperature_2m', label: 'Temperature', unit: '°C', color: '#f97316', icon: <Thermometer className="w-4 h-4" /> },
relative_humidity_2m: { key: 'relative_humidity_2m', label: 'Humidity', unit: '%', color: '#3b82f6', icon: <Droplets className="w-4 h-4" /> },
surface_pressure: { key: 'surface_pressure', label: 'Air Pressure', unit: 'hPa', color: '#a855f7', icon: <Gauge className="w-4 h-4" /> },
wind_speed_10m: { key: 'wind_speed_10m', label: 'Wind Speed', unit: 'km/h', color: '#10b981', icon: <Wind className="w-4 h-4" /> },
};
export default function App() {
const [query, setQuery] = useState('Houston');
cont [searchResults, setSearchResults] = useState<City[]>([]);
const [selectedCity, setSelectedCity] = useState<City>({ name: 'Houston', latitude: 29.7604, longitude: -95.3698, country: 'United States' });
const [selectedMetrics, setSelectedMetrics] = useState<string[]>(['temperature_2m', 'wind_speed_10m']);
// Date selection state
const [startDate, setStartDate] = useState('2024-01-01');
const [endDate, setEndDate] = useState(new Date().toISOString().split('T')[0]);
const [weatherData, setWeatherData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// Search Cities via Open-Meteo Geocoding
const handleSearch = async () => {
if (!query) return;
const res = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=5&language=en&format=json`);
const data = await res.json();
if (data.results) setSearchResults(data.results);
};
// Fetch Weather Data (Forecast or Historical Archive)
useEffect(() => {
const fetchWeather = async () => {
setading(true);
const metricsParam = Object.keys(METRICS).join(',');
// Choose Archive endpoint for older dates, Forecast for modern dates
const isHistorical = new Date(startDate) < new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const baseUrl = isHistorical
? 'https://archive-api.open-meteo.com/v1/archive'
: 'https://api.open-meteo.com/v1/forecast';
const url = `${baseUrl}?latitude=${selectedCity.latitude}&longitude=${selectedCity.longitude}&start_date=${startDate}&end_date=${endDate}&hourly=${metricsParam}`;
try {
const res = await fetch(url);
const data = await res.json();
if (data.hourly && data.hourly.time) {
const formatted = data.hourly.time.map((timeStr: string, idx: number) => {
const entry: any = { time: timeStr.replace('T', ' ') };
Object.keys(METRICS).forEach((m) => {
entry[m] = data.hourly[m][idx];
});
return entry;
});
setWeatherData(formatted);
}
} catch (err) {
console.error("Failed to fetch weather data:", err);
} finally {
setLoading(false);
}
};
fetchWeather();
}, [selectedCity, startDate, endDate]);
const toggleMetric = (key: string) => {
setSelectedMetrics(prev =>
prev.includes(key) ? prev.filter(m => m !== key) : [...prev, key]
);
};
// Export current active dataset view to CSV
const downloadCSV = () => {
if (!weatherData.length) return;
const csv = Papa.unparse(weatherData);
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.setAttribute('download', `${selectedCity.name}_weather_${startDate}_to_${endDate}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<div className="min-h-screen bg-slate-950 text-slate-100 p-6 font-sans">
<header className="max-w-7xl mx-auto mb-8 border-b border-slate-800 pb-4 flex justify-between items-center">
<div className="flex items-center gap-3">
<CloudRain className="w-8 h-8 text-blue-500" />
<h1 className="text-2xl font-bold tracking-wide">OmniGraph Weather Analytics</h1>
</div>
<button
onClick={downloadCSV}
className="flex items-center gap-2 bg-slate-800 hover:bg-slate-700 text-slate-200 px-4 py-2 rounded-lg border border-slate-700 transition"
>
<Download className="w-4 h-4" /> Export CSV
</button>
</header>
<main className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar Controls */}
<div className="bg-slate-900 border border-slate-800 p-5 rounded-xl flex flex-col gap-6">
{/* Location Search */}
<div>
<label className="text-xs font-semibold uppercase tracking-wider text-slate-400 mb-2 block">Location</label>
<div className="flex gap-2">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search city..."
className="bg-slate-950 border border-slate-800 rounded-md px-3 py-2 text-sm w-full focus:outline-none focus:border-blue-500 text-slate-100"
/>
<button onClick={handleSearch} className="bg-blue-600 hover:bg-blue-500 p-2 rounded-md text-white">
<Search className="w-4 h-4" />
</button>
</div>
{searchResults.length > 0 && (
<ul className="mt-2 bg-slate-950 border border-slate-800 rounded-md overflow-hidden">
{searchResults.map((city, idx) => (
<li
key={idx}
onClick={() => { setSelectedCity(city); setSearchResults([]); }}
className="px-3 py-2 text-sm hover:bg-slate-800 cursor-pointer text-slate-300"
>
{city.name}, {city.admin1 ? `${city.admin1}, ` : ''}{city.country}
</li>
))}
</ul>
)}
</div>
{/* Date Range Controls */}
<div>
<label className="text-xs font-semibold uppercase tracking-wider text-slate-400 mb-2 block">Date Range (1940 – Present)</label>
<div className="flex flex-col gap-2">
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="bg-slate-950 border border-slate-800 rounded-md px-3 py-1.5 text-sm text-slate-200"
/>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="bg-slate-950 border border-slate-800 rounded-md px-3 py-1.5 text-sm text-slate-200"
/>
</div>
</div>
{/* Metric Selector Buttons */}
<div>
<label className="text-xs font-semibold uppercase tracking-wider text-slate-400 mb-2 block">Data Metrics</label>
<div className="flex flex-col gap-2">
{Object.values(METRICS).map((m) => {
const active = selectedMetrics.includes(m.key);
return (
<button
key={m.key}
onClick={() => toggleMetric(m.key)}
className={`flex items-center justify-between px-3 py-2 rounded-md text-sm border transition ${
active
? 'bg-slate-800 border-slate-600 text-white'
: 'bg-slate-950 border-slate-800 text-slate-500 hover:text-slate-300'
}`}
>
<span className="flex items-center gap-2">{m.icon} {m.label}</span>
<span className="text-xs font-mono" style={{ color: active ? m.color : 'inherit' }}>{m.unit}</span>
</button>
);
})}
</div>
</div>
</div>
{/* Chart Visualization Panel */}
<div className="lg:col-span-3 bg-slate-900 border border-slate-800 p-6 rounded-xl flex flex-col">
<div className="flex justify-between items-center mb-6">
<div>
<h2 className="text-xl font-semibold text-slate-100">{selectedCity.name}</h2>
<p className="text-xs text-slate-400">
{selectedCity.latitude.toFixed(2)}°N, {selectedCity.longitude.toFixed(2)}°E | {startDate} to {endDate}
</p>
</div>
{loading && <span className="text-xs text-blue-400 animate-pulse">Loading dataset...</span>}
</div>
<div className="h-[450px] w-full">
{weatherData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={weatherData}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis dataKey="time" stroke="#64748b" tick={{ fontSize: 11 }} />
<YAxis stroke="#64748b" tick={{ fontSize: 11 }} />
<Tooltip
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', color: '#f8fafc' }}
itemStyle={{ fontSize: '12px' }}
/>
{selectedMetrics.map((metricKey) => (
<Line
key={metricKey}
type="monotone"
dataKey={metricKey}
name={METRICS[metricKey].label}
stroke={METRICS[metricKey].color}
dot={false}
strokeWidth={2}
/>
))}
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-slate-600">
No data available for selected criteria.
</div>
)}
</div>
</div>
</main>
</div>
);
}
3. Add Tailwind CSS for Dark Theme Styling
To get the styled dark components instantly, install Tailwind:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Edit tailwind.config.js:
JavaScript
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: { extend: {} },
plugins: [],
}
Add Tailwind directives to the top of src/index.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
Phase 3: Build & Deploy
Compile the React app into static bundle files:
npm run build
This generates optimized static files inside /var/www/[myomnigraph.com/dist](https://myomnigraph.com/dist). Since Nginx is already configured to serve this directory, your app will immediately render when hitting myomnigraph.com (or [http://123.456.789.0](http://123.456.789.0)).
Phase 4: Free SSL Certificate (HTTPS)
Secure the site with Let's Encrypt using Certbot:
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d myomnigraph.com -d www.myomnigraph.com
Certbot will automatically verify the DNS pointing to 123.456.789.0, issue the certificate, and update the Nginx configuration to enforce encrypted HTTPS traffic.
Summary
Building great web applications with AI isn't just about generating code. It's about clearly communicating what you want. When you provide detailed guidance about your environment, architecture, and desired features, AI can better understand your goals and produce production-ready, TypeScript-based solutions faster, more accurately, and with far fewer rounds of revisions.
If you need further assistance, Bluehost Chat Support is available 24 hours a day, 7days a week while Bluehost Phone Support is available 7 days a week from 7 am-12 midnight EST.
- Chat Support - While on our website, you should see a CHAT bubble in the bottom right-hand corner of the page. Click anywhere on the bubble to begin a chat session.
- Phone Support -
- US: 888-401-4678
- International: +1 801-765-9400
You may also refer to our Knowledge Base articles to help answer common questions and guide you through various setup, configuration, and troubleshooting steps.