Day 7: ReactJS - When "Context" Makes Sense

Imagine you have a big family dinner. Everyone is sitting at the table, and there are lots of dishes like pasta, salad, and desserts. Now, instead of each person getting up to fetch their own food, the host (your mom, dad, or anyone) says, “Hey, I’m going to pass the food platter around so you can just take what you need without leaving your seat.”

You must be wondering why I have put this example here? In fact it is for the topic which we are going to explore in this blog post: Context.

In React, the Context is like that host passing around important information (or data) to different parts of the app, so they don’t have to "get up" (ask for the information individually). This data could be things like:

  • What theme the app is using (dark mode or light mode).

  • Whether the user is logged in.

  • The current language (English, French, etc.).

We will implement something from the examples above…

We have seen basic examples based on Context in Day 6 Blog, yet here we are going to cover this topic in a more theory fashion.


The Problem to solve

In React, components usually pass data to each other through props (short for properties). So if you have a parent component, it can pass data to its children like this:

<Parent>
  <Child data={someData} />
</Parent>

But sometimes, your app gets big, with lots of components nested deep inside other components. If you want to share the same piece of data between multiple components that are far apart, passing props from parent to child, grandchild, great-grandchild (and so on) becomes messy. This is called "prop drilling", and it can make your code hard to manage.

We can solve the problem of Prop Drilling using Context

Enter Context!

Context lets you create a "central storage" where you can put your important data, and any component in your app can just grab that data, without needing to pass props down through multiple layers of components.

Here’s the technical breakdown:

Context Provider: This is like the "host" at the dinner table. It holds the data you want to share, and passes it down to any component that needs it.

Context Consumer: These are the components at the "dinner table" that want to use the data. They "consume" the data provided by the Context Provider.

useContext Hook: This is a hook in React that allows you to access the context (or "grab the food") directly inside your component, without needing to pass it down manually.


Let us understand the concept with a simple example. Let us say we will implement the example to check what theme the app is using (light or dark).

All you need to start is Context using createContext(). This is like making a container where you’ll store the information.

import React {createContext, useState} from 'react';

// Creating a Context for Theme
const ThemeContext = createContext(null);

Here, we create a ThemeContext that will hold our theme data (like "light" or "dark" mode).

Next we will require a “Provider” that will hold the actual theme value and make it available to all the components that need it. It "provides" the theme to any component inside it.

function ThemeProvider({ children }) { 
    const [theme, setTheme] = useState('light'); // Managing theme state 

    return ( 
        <ThemeContext.Provider value={{ theme, setTheme }}> 
            {children} 
        </ThemeContext.Provider> 
    ); 
}

ThemeProvider is a special component that wraps around all other components. It holds the theme value using useState and allows you to change the theme using setTheme. {children} is the content that will go inside the provider (other components).

Any component inside the ThemeProvider can now consume the theme using the useContext hook.

import { useContext } from 'react'; 
import { ThemeContext } from './ThemeContext'; // Import the context 

function ThemedButton() { 
    const { theme, setTheme } = useContext(ThemeContext); // Access the theme data 
    return ( 
        <button 
            onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')} 
            style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }} 
        > 
            Toggle Theme 
        </button> 
    ); 
}

Here, ThemedButton is a component that accesses the current theme value. It changes its background color based on whether the theme is light or dark. The button can toggle the theme when clicked (light mode becomes dark mode and vice versa).

Finally, you wrap your entire app (or parts of it) inside the ThemeProvider. This makes sure that any component inside this provider can access the theme context.

function App() { 
    return ( 
        <ThemeProvider> 
            <ThemedButton /> 
            {/* Other components can go here and also access the theme */} 
        </ThemeProvider> 
    ); 
} 
export default App;

Now, whenever you click the ThemedButton, it can change the theme from light to dark because it’s connected to the global ThemeContext!

So here is how the full code looks like:

import React, { createContext, useContext, useState } from 'react';

// Create the context
const ThemeContext = createContext(null);

function App() {
  const [theme, setTheme] = useState('light');  // State to manage the theme

  return (
    <ThemeContext.Provider value={theme}>
      <Header />
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </ThemeContext.Provider>
  );
}

function Header() {
  const theme = useContext(ThemeContext);  // Get the theme from context

  return (
    <header className={`header-${theme}`}>
      <h1>Current Theme: {theme}</h1>
    </header>
  );
}

export default App

Technical Breakdown:

Context Creation: The createContext function creates a central store where we can keep global data (in this case, the theme).

Provider: The ThemeContext.Provider acts like a wrapper around our components, supplying them with the theme data. It's like a giant umbrella under which all the components can share data.

useContext Hook: Instead of passing the theme as props from one component to another, we just grab the theme data directly using useContext(ThemeContext). This way, any component inside the ThemeProvider can access the theme without worrying about prop drilling.


Okay. Time to tighten the screws. Let’s dive deeper into managing multiple global states using multiple contexts in React. This will help you manage various types of global data, such as user authentication and theme settings, in one app. In above example we have seen how a single context works. Now we will look into another examples where we will handle multiple contexts.

While a single context is great for passing down one piece of data (like a theme), as an app grows, you might need to manage multiple pieces of global data (like user authentication, theme, language preferences, etc.). In such cases, it’s better to use multiple contexts to keep things organized and efficient.

We will upgrade our app a bit. We’ll build a simple app where:

  • Theme context: Manages the app's theme (light or dark).

  • User context: Manages the current user’s login status (logged in or logged out).

import React, { createContext, useState } from 'react';

// Create Theme and User contexts
const ThemeContext = createContext(null);   // For Light-Dark Theme
const UserContext = createContext(null);    // For checking user Logged in or not

To proceed, we need to set up the providers for both contexts. These providers will pass down the global data (theme and user) to any component that needs it.

function App() {
  // Set up states for theme and user
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: 'Guest', isLoggedIn: false });

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <Header />
        <MainContent />
        <Footer />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

export default App;

There will be 3 components whch we will be creating. Header, MainContent and Footer. By nesting the UserContext.Provider inside the ThemeContext.Provider, all child components can access both the theme and user states.

Now, let’s consume these contexts inside different components. We’ll use the useContext hook to access both the theme and user data. As discussed, we have split our app in 3 parts namely Header, MainContent and Footer. We will have a button to toggle theme on Header.

import React, { useContext } from 'react';
import { ThemeContext } from './App';

function Header() {
  // Consuming the theme context
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <header className={`header-${theme}`}>
      <h1>Welcome to the App</h1>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </header>
  );
}

export default Header;

We have use useContext(ThemeContext) to access the current theme and setTheme function. If we perform all steps correctly, the button following it should help us toggling between the themes. By default, we have set it to Light theme.

Now let us focus on the MainContent part.

import React, { useContext } from 'react';
import { UserContext } from './App';

function MainContent() {
  // Consume the user context
  const { user, setUser } = useContext(UserContext);

  const handleLogin = () => {
    setUser({ name: 'John Doe', isLoggedIn: true });
  };

  const handleLogout = () => {
    setUser({ name: 'Guest', isLoggedIn: false });
  };

  return (
    <main>
      {user.isLoggedIn ? (
        <div>
          <h2>Welcome, {user.name}!</h2>
          <button onClick={handleLogout}>Log Out</button>
        </div>
      ) : (
        <div>
          <h2>Please log in</h2>
          <button onClick={handleLogin}>Log In</button>
        </div>
      )}
    </main>
  );
}

export default MainContent;

Here, useContext(UserContext) is used to get the user object and the setUser function. Based on whether the user is logged in, the component either shows a welcome message and logout button or a login button.

Finally, the Footer Component.

import React, { useContext } from 'react';
import { ThemeContext } from './App';

function Footer() {
    const { theme } = useContext(ThemeContext);
    return (
      <footer className={`footer-${theme}`}>
        <p>App footer in {theme} mode</p>
      </footer>
    );
  }

export default Footer;

So lets have a look at the final version of our code and explainations to be followed:

import React, { createContext, useContext, useState } from 'react';

// Create contexts
const ThemeContext = createContext(null);
const UserContext = createContext(null);

function App() {
  // State for theme and user
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: 'Guest', isLoggedIn: false });

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <Header />
        <MainContent />
        <Footer />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

function Header() {
  const { theme, setTheme } = useContext(ThemeContext);
  return (
    <header className={`header-${theme}`}>
      <h1>Welcome to the App</h1>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </header>
  );
}

function MainContent() {
  const { user, setUser } = useContext(UserContext);

  const handleLogin = () => {
    setUser({ name: 'John Doe', isLoggedIn: true });
  };

  const handleLogout = () => {
    setUser({ name: 'Guest', isLoggedIn: false });
  };

  return (
    <main>
      {user.isLoggedIn ? (
        <div>
          <h2>Welcome, {user.name}!</h2>
          <button onClick={handleLogout}>Log Out</button>
        </div>
      ) : (
        <div>
          <h2>Please log in</h2>
          <button onClick={handleLogin}>Log In</button>
        </div>
      )}
    </main>
  );
}

function Footer() {
  const { theme } = useContext(ThemeContext);
  return (
    <footer className={`footer-${theme}`}>
      <p>App footer in {theme} mode</p>
    </footer>
  );
}

export default App;

The main thing is the App component. It has 2 contexts, one for theme and one for user login / logout. The app state manages both the theme and the user’s login status. We have wrapped the entire app in both context providers (ThemeContext.Provider and UserContext.Provider) to share the global data across the components.

Header component displays the current theme and provides a button to toggle the theme between light and dark. MainComponent displays different content based on whether the user is logged in or not. As well as provides us button to Log In or Log Out based on the UserContext. Footer Component adjusts the footer style based on the theme.

CSS Developer Warning : No CSS found in output. Kindly apply yours.

This here is by default view. If we hit the Toggle theme button, then…

And if we click the Log In button, then…

Clicking on Logout will print “Please Log In“ again as it was. Implementing these functionality in React Apps will certainly take it to the next level.


A Gentle Introduction to Redux

Imagine you have a school. There are classrooms, and each classroom has its own whiteboard where the teacher writes information for students to see. Each classroom (like a React component) has its own separate information (or state). Now, let’s say you want all classrooms to share some common information (e.g., announcements) without having to rewrite it in each room. This is where a central board (like Redux’s store) comes in, so everyone can look at the same shared information.

Redux is a state management tool that helps React apps (or any JavaScript apps) manage and share data across different parts of the app. It’s like having a central warehouse where all the data is stored, and any part of your app can take or update data from that warehouse instead of keeping it in their own local storage.

In React, you typically use useState to handle state within components. But what if many components in your app need to use the same data? You’d have to pass that state manually from one component to another, which can become confusing and messy. Redux solves this problem by keeping the state in a centralized location.

Lets Break everything down. We will need to undersatnd what Store, Actions and Reducers are. These are the basic concepts which lay a basic foundations on what exactly Redux is.


The Store:

As the name suggests, we can imagine the store as the big box of centralized data that holds all the state (or data) for your app. This state can be anything, like a list of users, the current theme (light/dark mode), or whether the user is logged in. The store is where all the important data lives.

The basic syntax of creating a store is as follows:

import { createStore } from 'redux';

// Creating a Redux store to hold the state
const store = createStore(reducer);

The Action:

An action is like a letter or request that tells the store what to do. It’s simply a JavaScript object that describes what you want to happen in the store. For example, if you want to add a user, you would send an action like:

const addUserAction = {
  type: 'ADD_USER',        // The type of action (what you're doing)
  payload: { name: 'John Doe', age: 30 }  // The data you're sending
};

Actions are plain objects that have a type property (which describes what the action does) and often a payload (which is the data you want to change).

The Reducer:

A reducer is a special function that tells the store how to update its state based on the action. You can think of a reducer as the brain that decides how the state changes when an action is sent.

For example, when the action "ADD_USER" comes in, the reducer decides how to update the list of users by adding the new user to it.

function userReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_USER':
      return [...state, action.payload];  // Add the new user to the state
    default:
      return state;  // If no matching action, return the current state
  }
}

The state starts as an empty array (because we start with no users). When the action ADD_USER is dispatched, the reducer adds the new user to the state.


Practical Demonstration

// A simplified version of Redux in action
import { createStore } from 'redux';

// Reducer function: Defines how the state should change
function userReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_USER':
      return [...state, action.payload];  // Adds a new user to the list
    default:
      return state;  // If no action matches, return current state
  }
}

// STEP 1 - Create the store using the reducer
const store = createStore(userReducer);

// STEP 2 - Dispatching an action to add a user
store.dispatch({ type: 'ADD_USER', payload: { name: 'Jane Doe', age: 28 } });

// STEP 3 - Check the updated state
console.log(store.getState());  // [{ name: 'Jane Doe', age: 28 }]

STEP 1 : We first create the store that holds all of the state. The store uses the reducer to know how to handle changes in state.

STEP 2 : To update the state, we send an action to the store. This is like sending a request or instruction to update the store with new information. The action goes to the reducer, and the reducer decides how to update the state.

STEP 3 : After dispatching the action, the state gets updated. To see the current state in the store, we use store.getState().

Bottom Line : As your app grows, managing state with useState inside components can get messy when multiple components need to access the same data. Instead of passing data around through props (known as "prop drilling"), Redux allows any component to access the central state in a clean and organized way.


Context vs Redux

Purpose :

Context is great for sharing simple, static data across components, like a user’s login status or a theme setting.

Redux is ideal for complex, dynamic state management where multiple components might need to update or access different parts of the state.

State Management :

Context doesn’t provide a way to manage complex updates to the state. You’re responsible for handling state changes manually with functions like useState or useReducer.

Redux provides a structured way to handle state updates through actions and reducers, ensuring that state updates are predictable and centralized.

Scalability :

Context works well for small applications with limited state.

Redux is better for larger applications where state management becomes difficult to maintain across many components.

Suppose, we are designing an app. Now, let’s assume you’re building a shopping cart feature in your app. You need to manage 3 things:

  • The items in the cart.

  • The total price.

  • Whether the user is logged in.

We will have a look on how we will handle this situation with context as well as redux.

Using Context:

You could create one context for each of these (one for the cart, one for the user, etc.). However, as the state grows (e.g., adding more features like discount codes or order history), managing everything with Context could get messy because you’d need to set up multiple contexts and pass down more and more data.

Using Redux:

Redux would centralize all this state in a single store. You can have reducers to manage the cart, total price, and user information separately, but they all live in one place, making it easier to manage and debug.

So how to decide when to choose Redux over Context and vice versa?

CHOOSING CONTEXT over REDUX:

  • If your app is small or medium-sized.

  • If you only need to share simple, less-frequently changing data.

  • When you don’t need complex state logic (like deeply nested data or a lot of updates).

CHOOSING REDUX over CONTEXT:

  • If your app is large and has a lot of complex state that changes frequently.

  • If you need more structure, predictability, and easier debugging.

  • When you have multiple pieces of state that interact or rely on each other.


CLOSING STATEMENT

Use Context as it is simple and built into React. It is best for lightweight, simple state sharing like themes or user data.

Redux is more structured and powerful. It is best for managing complex, large-scale state that requires frequent updates and interaction between components.


So that’s it as part of today’s learning quota. We went from Context to Redux; looking at the practical usage and comparisons. Make sure you replicate the above examples using different scenarios during practice and we should be good to go more deeper in Redux.

Signing Off!! Ciao