Greenhouse Project

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:

This project will help us understand how to use React Context to share state between components without prop drilling.

Devising a Plan

  1. Set up the project and understand the codebase structure
  2. Connect the ThemeContext to the Greenhouse component to display day/night images
  3. Implement the LightSwitch component to toggle between day and night themes
  4. Create a ClimateContext to manage temperature and humidity
  5. Connect the Thermometer component to the ClimateContext
  6. Connect the Hygrometer component to the ClimateContext
  7. Connect the ClimateStats component to display temperature and humidity
  8. 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

  1. createContext(): Creates a Context object.
  2. Context.Provider: A React component that allows consuming components to subscribe to context changes.
  3. 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:

Real-World Applications

The patterns we've used in this project are commonly used in real-world applications for:

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.