React Context with Horoscopes

Understanding the Problem

In React applications, data is typically passed from parent components to child components through props. This works well for small component trees, but it becomes cumbersome in larger applications where data needs to be accessed by many components at different levels. This issue is known as "prop drilling" or "prop threading".

React Context provides a solution to this problem by allowing data to be shared across the component tree without manually passing props down through each level. In our current application structure:

App
├── Navbar (displays all horoscope signs)
├── SideCard (displays the title)
└── SeparatorOne
    └── SeparatorTwo
        └── Detail (needs to display horoscope details)
        

We need to pass the current horoscope sign from the top level to the Detail component. Without Context, we would need to pass the data through SeparatorOne and SeparatorTwo components, even though they don't need this data.

Our goal is to:

  1. Create a HoroscopeContext to hold our horoscope data
  2. Set up a Provider to make this data available throughout the app
  3. Consume the context in the Detail component to display the appropriate data

Devising a Plan

To solve this problem, we'll follow these steps:

  1. Create a context directory in the src folder
  2. Create a HoroscopeContext.jsx file to define our context
  3. Modify the main.jsx file to wrap our App with the HoroscopeContext.Provider
  4. Update the Detail component to consume the context
  5. Test our implementation

Carrying Out the Plan

Step 1: Create the Context Directory and File

First, create a new directory called context in your src folder:

mkdir src/context

Step 2: Create the HoroscopeContext.jsx File

Create a new file called HoroscopeContext.jsx in the context directory with the following content:

// src/context/HoroscopeContext.jsx
import { createContext } from 'react';

export const HoroscopeContext = createContext();

This code imports the createContext function from React and uses it to create a new context called HoroscopeContext. The createContext function creates a Context object. When React renders a component that subscribes to this Context object, it will read the current context value from the closest matching Provider above it in the tree.

Step 3: Set Up the Context Provider in main.jsx

Now, we need to modify the main.jsx file to wrap our App component with the HoroscopeContext.Provider:

// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { HoroscopeContext } from './context/HoroscopeContext';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <HoroscopeContext.Provider value={{ sign: "Leo" }}>
      <App />
    </HoroscopeContext.Provider>
  </React.StrictMode>
);

In this code, we're importing our HoroscopeContext and using the Provider component that comes with it. The Provider component accepts a value prop, which is the data we want to make available to all components in the tree. In this case, we're providing an object with a sign property set to "Leo".

Step 4: Consume the Context in the Detail Component

Finally, we need to update the Detail component to consume the context:

// src/components/Detail.jsx
import { useContext } from 'react';
import { HoroscopeContext } from '../context/HoroscopeContext';

const Detail = () => {
  const horoscopesObj = useContext(HoroscopeContext);
  
  return (
    <div className='details'>
      <img
        src='https://upload.wikimedia.org/wikipedia/commons/e/e1/FullMoon2010.jpg'
        alt=''
      />
      <h2>{horoscopesObj.sign}</h2>
      <h4>Element: </h4>
      <h4>Traits: </h4>
    </div>
  );
};

export default Detail;

Here, we're importing the useContext hook from React and our HoroscopeContext. Inside the component, we call useContext(HoroscopeContext) to access the current context value. This gives us access to the sign property we provided in the main.jsx file. We then use this value in our JSX to display the current sign.

Testing Our Solution

Now that we've implemented our solution, we can test it to make sure it works as expected:

  1. Start your application with npm run dev
  2. Open your browser's React DevTools (if you have the React DevTools browser extension installed)
  3. In the Component tree, you should see HoroscopeContext.Provider at the top level
  4. If you click on it, you should see the value prop with our sign property
  5. Navigate to the Detail component in the tree
  6. You should see a hooks section showing the context being consumed
  7. On the page itself, you should see "Leo" displayed in the Detail component

If everything is working correctly, congratulations! You've successfully implemented React Context in your application.

Looking Back

Let's review what we've learned and accomplished:

What is React Context?

React Context is a feature that allows you to share data between components without having to explicitly pass props through every level of the component tree. It's particularly useful for data that can be considered "global" for a tree of React components, such as:

When to Use Context

Context is designed to share data that can be considered "global" for a tree of React components. Use context when:

Context Components

React Context consists of three main parts:

  1. Context Object: Created with createContext()
  2. Provider: Makes the context value available to all child components
  3. Consumer: Reads the context value (in modern React, usually via the useContext hook)

Real-World Analogies

Think of React Context like a broadcasting system:

Another analogy is a family inheritance:

Advanced Usage

While we've implemented a basic version of Context, there are more advanced patterns you might want to explore:

Dynamic Context with useState

Instead of hardcoding "Leo" as our sign, we could make it dynamic:

// Updated main.jsx
import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { HoroscopeContext } from './context/HoroscopeContext';
import './index.css';

function Root() {
  const [sign, setSign] = useState("Leo");
  
  return (
    <HoroscopeContext.Provider value={{ sign, setSign }}>
      <App />
    </HoroscopeContext.Provider>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Root />
  </React.StrictMode>
);

Now, any component can not only read the current sign but also update it using setSign.

Context Provider as a Component

For more complex scenarios, you might want to create a dedicated provider component:

// src/context/HoroscopeContext.jsx
import { createContext, useState } from 'react';

export const HoroscopeContext = createContext();

export const HoroscopeProvider = ({ children }) => {
  const [sign, setSign] = useState("Leo");
  
  return (
    <HoroscopeContext.Provider value={{ sign, setSign }}>
      {children}
    </HoroscopeContext.Provider>
  );
};

Then in main.jsx:

import { HoroscopeProvider } from './context/HoroscopeContext';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <HoroscopeProvider>
      <App />
    </HoroscopeProvider>
  </React.StrictMode>
);

Custom Hook for Context

To simplify usage and avoid importing both useContext and your context in every component, you can create a custom hook:

// src/context/HoroscopeContext.jsx
export const useHoroscope = () => {
  const context = useContext(HoroscopeContext);
  if (context === undefined) {
    throw new Error('useHoroscope must be used within a HoroscopeProvider');
  }
  return context;
};

Then in your components:

import { useHoroscope } from '../context/HoroscopeContext';

const Detail = () => {
  const { sign } = useHoroscope();
  // Rest of component...
};

Exercise: Complete the Horoscope App

Now that you understand the basics of React Context, try extending the application to:

  1. Make the sign dynamic by adding setSign to the context value
  2. Update the Navbar component to change the selected sign when a user clicks on a sign
  3. Use the horoscopes data to display detailed information about the selected sign in the Detail component

Hint: You'll need to:

// In Navbar.jsx
import { useContext } from 'react';
import { HoroscopeContext } from '../context/HoroscopeContext';
import horoscopeObj from '../data/horoscopes';

const Navbar = () => {
  const { sign, setSign } = useContext(HoroscopeContext);
  const horoscopes = Object.keys(horoscopeObj);
  
  return (
    <nav>
      {horoscopes.map(signName => (
        <span 
          key={signName}
          onClick={() => setSign(signName)}
          className={sign === signName ? 'active' : ''}
        >
          {signName}
        </span>
      ))}
    </nav>
  );
};
// In Detail.jsx
import { useContext } from 'react';
import { HoroscopeContext } from '../context/HoroscopeContext';
import horoscopeObj from '../data/horoscopes';

const Detail = () => {
  const { sign } = useContext(HoroscopeContext);
  const currentSign = horoscopeObj[sign];
  
  return (
    <div className='details'>
      <img src={currentSign.backgroundImg} alt={sign} />
      <h2>{sign}</h2>
      <h4>Element: {currentSign.element}</h4>
      <h4>Traits: {currentSign.traits}</h4>
    </div>
  );
};