Understanding the Problem
We need to build a React application that simulates a greenhouse environment. The application will use React Context to manage and control various aspects of the greenhouse:
- Create a theme context to switch between day and night modes
- Implement a light switch component to toggle the theme
- Create a climate context to manage temperature and humidity
- Build thermometer and hygrometer components that connect to the climate context
- Display climate statistics
This project will help us understand how to use React Context to share state between components without prop drilling.
Devising a Plan
- Set up the project and understand the codebase structure
- Connect the ThemeContext to the Greenhouse component to display day/night images
- Implement the LightSwitch component to toggle between day and night themes
- Create a ClimateContext to manage temperature and humidity
- Connect the Thermometer component to the ClimateContext
- Connect the Hygrometer component to the ClimateContext
- Connect the ClimateStats component to display temperature and humidity
- Implement bonus features if desired (gradual temperature/humidity changes)
Executing the Plan
Phase 1: Connect ThemeContext to Greenhouse Component
First, let's look at the ThemeContext.jsx file to understand how it's structured:
// src/context/ThemeContext.jsx
import { createContext, useContext, useState } from 'react';
// Create the context
export const ThemeContext = createContext();
// Create the provider component
export const ThemeProvider = ({ children }) => {
const [themeName, setThemeName] = useState('day');
return (
<ThemeContext.Provider value={{ themeName, setThemeName }}>
{children}
</ThemeContext.Provider>
);
};
// Custom hook to use the theme context
export const useTheme = () => useContext(ThemeContext);
Now, we need to modify the Greenhouse.jsx component to use this context:
// src/components/Greenhouse/Greenhouse.jsx
import { useTheme } from '../../context/ThemeContext';
import dayImage from './images/greenhouse-day.jpg';
import nightImage from './images/greenhouse-night.jpg';
import './Greenhouse.css';
import LightSwitch from './LightSwitch';
import ClimateStats from './ClimateStats';
function Greenhouse() {
// Use the useTheme hook to access themeName
const { themeName } = useTheme();
return (
<section>
<img
className='greenhouse-img'
src={themeName === 'day' ? dayImage : nightImage}
alt='greenhouse'
/>
<LightSwitch />
<ClimateStats />
</section>
);
}
export default Greenhouse;
This change makes the Greenhouse component use the appropriate image based on the current theme (day or night).
Phase 2: Implement LightSwitch Component
Now, let's implement the LightSwitch component to toggle between day and night themes:
// src/components/Greenhouse/LightSwitch.jsx
import './LightSwitch.css';
import { useTheme } from '../../context/ThemeContext';
function LightSwitch() {
// Use the useTheme hook to access themeName and setThemeName
const { themeName, setThemeName } = useTheme();
return (
<div className={`light-switch ${themeName}`}>
<div className="on" onClick={() => setThemeName('day')}>
DAY
</div>
<div className="off" onClick={() => setThemeName('night')}>
NIGHT
</div>
</div>
);
}
export default LightSwitch;
When the "DAY" div is clicked, the theme changes to "day", and when the "NIGHT" div is clicked, the theme changes to "night". The class of the light-switch div also updates to reflect the current theme.
Phase 3: Create ClimateContext
Next, we need to create a new context for managing climate settings. Let's create a new file:
// src/context/ClimateContext.jsx
import { createContext, useContext, useState } from 'react';
// Create the context
export const ClimateContext = createContext();
// Create the provider component
export const ClimateProvider = ({ children }) => {
const [temperature, setTemperature] = useState(50);
return (
<ClimateContext.Provider value={{ temperature, setTemperature }}>
{children}
</ClimateContext.Provider>
);
};
// Custom hook to use the climate context
export const useClimate = () => useContext(ClimateContext);
Then, we need to wrap our application with this new provider in main.jsx:
// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import { ThemeProvider } from './context/ThemeContext';
import { ClimateProvider } from './context/ClimateContext';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<ThemeProvider>
<ClimateProvider>
<App />
</ClimateProvider>
</ThemeProvider>
</React.StrictMode>
);
Phase 4: Connect Thermometer Component
Now, let's connect the Thermometer component to our ClimateContext:
// src/components/Thermometer/Thermometer.jsx
import ReactSlider from 'react-slider';
import './Thermometer.css';
import { useClimate } from '../../context/ClimateContext';
function Thermometer() {
// Use the useClimate hook to access temperature and setTemperature
const { temperature, setTemperature } = useClimate();
return (
<section>
<h2>Thermometer</h2>
<div className="actual-temp">Actual Temperature: {temperature}°F</div>
<ReactSlider
value={temperature}
onAfterChange={(val) => setTemperature(val)}
className="thermometer-slider"
thumbClassName="thermometer-thumb"
trackClassName="thermometer-track"
ariaLabel={"Thermometer"}
orientation="vertical"
min={0}
max={120}
renderThumb={(props, state) => <div {...props}>{state.valueNow}</div>}
renderTrack={(props, state) => (
<div {...props} index={state.index}></div>
)}
invert
pearling
minDistance={1}
/>
</section>
);
}
export default Thermometer;
Phase 5: Update ClimateContext for Humidity and Connect Hygrometer
First, let's update our ClimateContext to include humidity:
// src/context/ClimateContext.jsx
import { createContext, useContext, useState } from 'react';
export const ClimateContext = createContext();
export const ClimateProvider = ({ children }) => {
const [temperature, setTemperature] = useState(50);
const [humidity, setHumidity] = useState(40);
return (
<ClimateContext.Provider value={{
temperature,
setTemperature,
humidity,
setHumidity
}}>
{children}
</ClimateContext.Provider>
);
};
export const useClimate = () => useContext(ClimateContext);
Now, let's connect the Hygrometer component:
// src/components/Hygrometer/Hygrometer.jsx
import ReactSlider from 'react-slider';
import './Hygrometer.css';
import { useClimate } from '../../context/ClimateContext';
function Hygrometer() {
// Use the useClimate hook to access humidity and setHumidity
const { humidity, setHumidity } = useClimate();
return (
<section>
<h2>Hygrometer</h2>
<div className="actual-humid">Actual Humidity: {humidity}%</div>
<ReactSlider
value={humidity}
onAfterChange={(val) => setHumidity(val)}
className="hygrometer-slider"
thumbClassName="hygrometer-thumb"
trackClassName="hygrometer-track"
ariaLabel={"Hygrometer"}
orientation="vertical"
min={0}
max={100}
renderThumb={(props, state) => <div {...props}>{state.valueNow}</div>}
renderTrack={(props, state) => (
<div {...props} index={state.index}></div>
)}
invert
pearling
minDistance={1}
/>
</section>
);
}
export default Hygrometer;
Phase 6: Connect ClimateStats Component
Finally, let's connect the ClimateStats component to display the current temperature and humidity:
// src/components/Greenhouse/ClimateStats.jsx
import './ClimateStats.css';
import { useClimate } from '../../context/ClimateContext';
function ClimateStats() {
// Use the useClimate hook to access temperature and humidity
const { temperature, humidity } = useClimate();
return (
<div className="climate-stats">
<div className="temperature">
Temperature {temperature}°F
</div>
<div className="humidity">
Humidity {humidity}%
</div>
</div>
);
}
export default ClimateStats;
Bonus 1: Implement Gradual Temperature Change
For the first bonus, we'll modify the Thermometer component to gradually change the actual temperature:
// src/components/Thermometer/Thermometer.jsx
import { useEffect, useState } from 'react';
import ReactSlider from 'react-slider';
import './Thermometer.css';
import { useClimate } from '../../context/ClimateContext';
function Thermometer() {
const { temperature, setTemperature } = useClimate();
const [desiredTemp, setDesiredTemp] = useState(temperature);
// Use useEffect to gradually adjust the temperature
useEffect(() => {
// If the actual temp already equals the desired temp, do nothing
if (temperature === desiredTemp) return;
// Set up a timer to change the temperature by 1 degree per second
const timer = setTimeout(() => {
// Increase or decrease based on whether actual temp is less than or greater than desired
if (temperature < desiredTemp) {
setTemperature(temperature + 1);
} else {
setTemperature(temperature - 1);
}
}, 1000); // 1000ms = 1 second
// Clean up the timer when the component unmounts or dependencies change
return () => clearTimeout(timer);
}, [temperature, desiredTemp, setTemperature]);
return (
<section>
<h2>Thermometer</h2>
<div className="actual-temp">Actual Temperature: {temperature}°F</div>
<ReactSlider
value={desiredTemp}
onAfterChange={(val) => setDesiredTemp(val)}
className="thermometer-slider"
thumbClassName="thermometer-thumb"
trackClassName="thermometer-track"
ariaLabel={"Thermometer"}
orientation="vertical"
min={0}
max={120}
renderThumb={(props, state) => <div {...props}>{state.valueNow}</div>}
renderTrack={(props, state) => (
<div {...props} index={state.index}></div>
)}
invert
pearling
minDistance={1}
/>
</section>
);
}
export default Thermometer;
Bonus 2: Implement Gradual Humidity Change
Similarly, we can modify the Hygrometer component to gradually change the humidity:
// src/components/Hygrometer/Hygrometer.jsx
import { useEffect, useState } from 'react';
import ReactSlider from 'react-slider';
import './Hygrometer.css';
import { useClimate } from '../../context/ClimateContext';
function Hygrometer() {
const { humidity, setHumidity } = useClimate();
const [desiredHumidity, setDesiredHumidity] = useState(humidity);
// Use useEffect to gradually adjust the humidity
useEffect(() => {
// If the actual humidity already equals the desired humidity, do nothing
if (humidity === desiredHumidity) return;
// Set up a timer to change the humidity by 2% per second
const timer = setTimeout(() => {
if (humidity < desiredHumidity) {
// Ensure we don't go past the desired humidity
const change = Math.min(2, desiredHumidity - humidity);
setHumidity(humidity + change);
} else {
// Ensure we don't go below the desired humidity
const change = Math.min(2, humidity - desiredHumidity);
setHumidity(humidity - change);
}
}, 1000); // 1000ms = 1 second
// Clean up the timer when the component unmounts or dependencies change
return () => clearTimeout(timer);
}, [humidity, desiredHumidity, setHumidity]);
return (
<section>
<h2>Hygrometer</h2>
<div className="actual-humid">Actual Humidity: {humidity}%</div>
<ReactSlider
value={desiredHumidity}
onAfterChange={(val) => setDesiredHumidity(val)}
className="hygrometer-slider"
thumbClassName="hygrometer-thumb"
trackClassName="hygrometer-track"
ariaLabel={"Hygrometer"}
orientation="vertical"
min={0}
max={100}
renderThumb={(props, state) => <div {...props}>{state.valueNow}</div>}
renderTrack={(props, state) => (
<div {...props} index={state.index}></div>
)}
invert
pearling
minDistance={1}
/>
</section>
);
}
export default Hygrometer;
Looking Back: Understanding Context in React
Throughout this project, we've used React Context to manage the state of our greenhouse application. Let's explore the key concepts we've learned:
What is React Context?
React Context provides a way to share values (like state) between components without having to explicitly pass props through every level of the component tree. This is particularly useful for values that are considered "global" for a subtree of components, such as the current theme, user authentication, or in our case, greenhouse climate settings.
The Context API Components
- createContext(): Creates a Context object.
- Context.Provider: A React component that allows consuming components to subscribe to context changes.
- useContext(): A hook that subscribes to context changes.
Custom Context Hooks
In this project, we created custom hooks (useTheme and useClimate) that encapsulate the call to useContext. This approach has several benefits:
- It simplifies the import statements in components that use the context.
- It provides a descriptive name that clearly indicates what data is being accessed.
- It centralizes the context usage, making it easier to modify later if needed.
Real-World Applications
The patterns we've used in this project are commonly used in real-world applications for:
- Theme switching (light/dark modes)
- User authentication and authorization
- Language/localization preferences
- Shopping carts in e-commerce applications
- Application settings and preferences
By understanding and applying these patterns, you now have a powerful tool for managing state across components in a React application without resorting to prop drilling or more complex state management libraries.