Understanding the Problem
In this phase, we'll refactor the Weather component from a class component to a function component. This introduces a significant new challenge: handling lifecycle methods with hooks. The Weather component:
- Uses
componentDidMountto get the user's location when the component first appears - Makes an API call to fetch weather data based on the user's coordinates
- Stores the weather data in component state
- Renders different content based on the fetched data
This is a common pattern in React applications—fetching data when a component mounts and then displaying that data. In function components, we'll use the useEffect hook to replicate this behavior.
Setting Up the Weather API Key
Before we start refactoring, we need to make sure we have access to a weather API key. The application uses the OpenWeatherMap API, which requires an API key for authentication.
- Create a
.envfile in the root directory of your project - Add the following line to the file:
VITE_WEATHER_API=b65b43cc09af164f099fe5a807d56972 - Make sure the
.envfile is included in your.gitignoreto avoid exposing your API key - If you encounter a fetch limit, you can try one of the alternative keys listed in the README
In Vite (the build tool this project uses), environment variables are accessed using import.meta.env instead of process.env that you might be familiar with from Create React App.
George Polya's Problem-Solving Method
Step 1: Understand the Problem
Let's analyze the original Weather class component to understand its functionality:
// Weather.jsx
import React from 'react';
import { toQueryString } from '../utils';
class Weather extends React.Component {
constructor(props) {
super(props);
this.state = {
weather: null
};
}
componentDidMount() {
navigator.geolocation?.getCurrentPosition(
this.pollWeather,
(err) => console.log(err),
{ timeout: 10000 }
);
}
pollWeather = async (location) => {
let url = 'http://api.openweathermap.org/data/2.5/weather?';
/* Remember that it's unsafe to expose your API key. (Note that pushing
files that include your key to Github will expose your key!) In
production, you would definitely save your key in an environment variable,
so do that here. Since this project runs in your local environment
(localhost), save your key as an environment variable in a .env file in
the root directory of your app. You can then access the key here as
"process.env.". Make sure to .gitignore your .env file!
Also remember to restart your server (i.e., re-run "npm run dev") whenever
you change your .env file. */
const apiKey = '???';
const params = {
lat: location.coords.latitude,
lon: location.coords.longitude,
appid: apiKey
};
url += toQueryString(params);
const res = await fetch(url);
if (res.ok) {
const weather = await res.json();
this.setState({ weather });
}
else {
alert ("Check Weather API key!")
}
}
render() {
const weather = this.state.weather;
let content = <div className='loading'>loading weather...</div>;
if (weather) {
const temp = (weather.main.temp - 273.15) * 1.8 + 32;
content = (
<div>
<p>{weather.name}</p>
<p>{temp.toFixed(1)} degrees</p>
</div>
);
}
else {
content = (
<div>
Weather is currently unavailable. (Are Location Services enabled?)
</div>
)
}
return (
<section className="weather-section">
<h1>Weather</h1>
<div className='weather'>
{content}
</div>
</section>
);
}
}
export default Weather;
Analyzing this code, we can see that:
- The component has one state variable:
weather(initialized tonull) - The
componentDidMountlifecycle method is used to get the user's location when the component mounts - The
pollWeathermethod is used as a callback when the location is obtained, and it makes a fetch request to the weather API - The render method displays different content based on whether weather data has been successfully fetched
- The API key is meant to be accessed from an environment variable, but is currently set to
'???'
Step 2: Devise a Plan
- Convert the class declaration to a function declaration
- Replace
this.state.weatherwith theuseStatehook - Replace
componentDidMountwith theuseEffecthook - Move the
pollWeatherfunction inside theuseEffectcallback - Update the API key to use
import.meta.env.VITE_WEATHER_API - Update references to
this.stateand remove therendermethod
Step 3: Carry Out the Plan
1. Component Declaration and State
We'll start by converting the class declaration to a function and setting up the state with useState:
// From
class Weather extends React.Component {
constructor(props) {
super(props);
this.state = {
weather: null
};
}
// ...
}
// To
function Weather() {
const [weather, setWeather] = useState(null);
// ...
}
Don't forget to import the necessary hooks:
import React, { useState, useEffect } from 'react';
2. Converting componentDidMount with useEffect
The key new concept here is using useEffect to replace the componentDidMount lifecycle method. The useEffect hook takes two arguments:
- A function that contains the code to run (the "effect")
- An array of dependencies that controls when the effect runs
When you pass an empty array [] as the second argument, the effect will only run once after the component mounts, which is equivalent to componentDidMount:
// From
componentDidMount() {
navigator.geolocation?.getCurrentPosition(
this.pollWeather,
(err) => console.log(err),
{ timeout: 10000 }
);
}
// To
useEffect(() => {
navigator.geolocation?.getCurrentPosition(
// pollWeather will be defined inside this callback
// ...
(err) => console.log(err),
{ timeout: 10000 }
);
}, []); // Empty array means "run once after mount"
3. Moving pollWeather Inside useEffect
In function components, it's common to define helper functions inside the effects that use them. This keeps the code organized and avoids issues with stale closures:
useEffect(() => {
// Define pollWeather inside the effect
const pollWeather = async (location) => {
let url = 'http://api.openweathermap.org/data/2.5/weather?';
// Use environment variable for API key
const apiKey = import.meta.env.VITE_WEATHER_API;
const params = {
lat: location.coords.latitude,
lon: location.coords.longitude,
appid: apiKey
};
url += toQueryString(params);
try {
const res = await fetch(url);
if (res.ok) {
const weatherData = await res.json();
setWeather(weatherData);
} else {
alert("Check Weather API key!");
}
} catch (error) {
console.error("Failed to fetch weather:", error);
}
};
navigator.geolocation?.getCurrentPosition(
pollWeather,
(err) => console.log(err),
{ timeout: 10000 }
);
}, []);
Notice several improvements in our implementation:
- We're using
import.meta.env.VITE_WEATHER_APIto access the API key from our environment variables - We've added a
try/catchblock for better error handling - We're using
setWeatherinstead ofthis.setStateto update the state
4. Updating the Render Logic
Now we'll update the render logic to use the weather state variable directly:
// Content rendering logic based on weather state
let content = <div className='loading'>loading weather...</div>;
if (weather) {
const temp = (weather.main.temp - 273.15) * 1.8 + 32;
content = (
<div>
<p>{weather.name}</p>
<p>{temp.toFixed(1)} degrees</p>
</div>
);
} else {
content = (
<div>
Weather is currently unavailable. (Are Location Services enabled?)
</div>
);
}
// Return JSX directly from the function component
return (
<section className="weather-section">
<h1>Weather</h1>
<div className='weather'>
{content}
</div>
</section>
);
5. Putting It All Together
Here's the complete refactored Weather component:
// Weather.jsx (Refactored)
import React, { useState, useEffect } from 'react';
import { toQueryString } from '../utils';
function Weather() {
// Replace this.state with useState
const [weather, setWeather] = useState(null);
// Replace componentDidMount with useEffect
useEffect(() => {
// Define pollWeather inside the effect
const pollWeather = async (location) => {
let url = 'http://api.openweathermap.org/data/2.5/weather?';
// Use environment variable for API key
const apiKey = import.meta.env.VITE_WEATHER_API;
const params = {
lat: location.coords.latitude,
lon: location.coords.longitude,
appid: apiKey
};
url += toQueryString(params);
try {
const res = await fetch(url);
if (res.ok) {
const weatherData = await res.json();
setWeather(weatherData);
} else {
alert("Check Weather API key!");
}
} catch (error) {
console.error("Failed to fetch weather:", error);
}
};
navigator.geolocation?.getCurrentPosition(
pollWeather,
(err) => console.log(err),
{ timeout: 10000 }
);
}, []); // Empty dependency array means run once on mount
// Determine content based on weather state
let content = <div className='loading'>loading weather...</div>;
if (weather) {
const temp = (weather.main.temp - 273.15) * 1.8 + 32;
content = (
<div>
<p>{weather.name}</p>
<p>{temp.toFixed(1)} degrees</p>
</div>
);
} else {
content = (
<div>
Weather is currently unavailable. (Are Location Services enabled?)
</div>
);
}
// Return what was previously in the render method
return (
<section className="weather-section">
<h1>Weather</h1>
<div className='weather'>
{content}
</div>
</section>
);
}
export default Weather;
Step 4: Look Back and Review
Let's verify that our refactored component maintains the same functionality as the original:
- The component still initializes the
weatherstate tonull - The location is still requested when the component mounts
- The weather data is still fetched and stored in state
- The component still renders different content based on whether weather data is available
- The API key is properly accessed from environment variables
Let's run the tests to confirm that everything works as expected:
npm test
The tests for the Weather component should pass, confirming that our refactoring was successful.
Understanding useEffect
What is useEffect?
The useEffect hook is one of the most powerful and important hooks in React. It lets you perform side effects in function components. Side effects can include:
- Data fetching
- Subscriptions to external data sources
- Manual DOM manipulations
- Logging
- Setting up timers
- And other operations that need to happen after rendering
useEffect takes two arguments:
- A function that contains the code to run (the "effect")
- An optional array of dependencies that determines when the effect should re-run
useEffect and Lifecycle Methods
The useEffect hook can be used to replicate the behavior of various lifecycle methods from class components:
| Lifecycle Method | useEffect Equivalent |
|---|---|
componentDidMount |
useEffect(() => { ... }, []) |
componentDidUpdate |
useEffect(() => { ... }, [dependencies]) |
componentWillUnmount |
useEffect(() => { return () => { ... }; }, [...]) |
In our Weather component, we used useEffect with an empty dependency array to replicate componentDidMount.
Dependency Array
The second argument to useEffect is an array of dependencies. This array controls when the effect runs:
- Empty array ([]): Effect runs once after the initial render (like
componentDidMount) - Array with dependencies ([a, b, c]): Effect runs after the initial render and whenever any dependency changes (like
componentDidUpdatewith specific conditions) - No array: Effect runs after every render
In our Weather component, we used an empty dependency array because we only want to fetch the weather data once when the component mounts, not on every re-render or state change.
Cleanup Function
Effects can return a cleanup function that runs before the component unmounts or before the effect runs again:
useEffect(() => {
// Effect code here...
// Return a cleanup function
return () => {
// Cleanup code here...
};
}, [dependencies]);
This is useful for cleaning up subscriptions, timers, or event listeners to prevent memory leaks. We didn't need a cleanup function in our Weather component, but we'll see this pattern in later examples.
Common Patterns for Data Fetching with useEffect
Basic Pattern
The pattern we used in our Weather component is a common approach for data fetching:
function MyComponent() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, []); // Empty array means run once after mount
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!data) return <div>No data available</div>;
return (
<div>
{/* Render data here */}
</div>
);
}
This pattern includes loading and error states for a better user experience.
Fetching Data with Parameters
If you need to fetch data based on props or state, you should include those values in the dependency array:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) return; // Skip the effect if query is empty
const fetchResults = async () => {
const response = await fetch(`https://api.example.com/search?q=${query}`);
const data = await response.json();
setResults(data);
};
fetchResults();
}, [query]); // Re-run the effect when query changes
return (
<div>
{/* Render results here */}
</div>
);
}
Cancelling Requests
For long-running requests, it's important to handle cancellation to avoid setting state on unmounted components or race conditions:
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
if (isMounted) {
setData(data); // Only update state if component is still mounted
}
};
fetchData();
// Cleanup function
return () => {
isMounted = false; // Set flag to false when component unmounts
};
}, []);
This pattern prevents the "Can't perform a React state update on an unmounted component" warning.
Real-World Applications
When to Use This Pattern
The pattern we've implemented—fetching data when a component mounts—is extremely common in web applications and can be found in many scenarios:
- Dashboards: Loading user-specific data when a dashboard component mounts
- Profile pages: Fetching user details when their profile page loads
- Product pages: Loading product information when viewing a product
- Settings interfaces: Retrieving user preferences when a settings component is displayed
- Geolocation-based features: As in our example, getting the user's location to provide localized content
Best Practices
- Loading states: Always show loading indicators while data is being fetched
- Error handling: Implement proper error handling and user-friendly error messages
- Dependency array: Carefully consider what values should trigger a re-fetch
- Cleanup: Properly clean up subscriptions and pending requests
- API abstraction: Consider moving API calls to separate service functions for better organization
- Custom hooks: For reusable data fetching logic, create custom hooks like
useFetchoruseWeather
Extensions
The basic pattern we've implemented can be extended in many ways:
- Caching: Storing fetched data to avoid unnecessary requests
- Polling: Periodically re-fetching data to keep it fresh
- Pagination: Loading data in chunks as the user scrolls or requests more
- Real-time updates: Using WebSockets or Server-Sent Events for live data
- Optimistic updates: Updating the UI immediately before waiting for API confirmation