Hone logo
Hone
Problems

Building a Custom React Context System

This challenge will guide you through the process of building a fundamental piece of React's state management: a custom Context system. Understanding how Context works from scratch will deepen your understanding of React's core principles and empower you to manage global application state more effectively.

Problem Description

Your task is to implement a simplified version of React's Context API. This system should allow components to "provide" a value at a higher level in the component tree and allow descendant components to "consume" that value without explicit prop drilling.

Key requirements:

  1. createMyContext function:

    • This function should accept an optional defaultValue.
    • It should return an object with two properties: Provider and Consumer.
  2. Provider component:

    • This component will be used to wrap a part of your application tree.
    • It accepts a value prop, which is the data that will be made available to its descendants.
    • It should render its children.
  3. Consumer component:

    • This component will be used to access the value provided by the nearest Provider ancestor.
    • It accepts a render prop function as its single child. This function will receive the context value as an argument and should return a React element.
    • If no Provider is found in the ancestry, the Consumer should use the defaultValue provided to createMyContext.
  4. State Management: The Provider should be able to update its value prop, and all Consumers subscribed to it should re-render with the new value.

Expected behavior:

  • Components can access context data without passing props down through intermediate components.
  • When the Provider's value prop changes, all consuming components should automatically update.
  • If a Consumer is rendered outside of any Provider, it should receive the defaultValue.

Edge cases:

  • Multiple nested Providers: Consumers should always receive the value from the nearest ancestor Provider.
  • Provider with no value prop: Should default to undefined if no defaultValue was provided during context creation.

Examples

Example 1: Basic Usage

import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';

// Assume createMyContext is implemented as described above

const ThemeContext = createMyContext('light'); // Default value

function App() {
  const [theme, setTheme] = useState('dark');

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

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  return (
    <ThemeContext.Consumer>
      {themeValue => (
        <button style={{ background: themeValue === 'dark' ? '#333' : '#eee', color: themeValue === 'dark' ? '#eee' : '#333' }}>
          I am a {themeValue} button
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

// Render App (simplified for example)
// const root = ReactDOM.createRoot(document.getElementById('root')!);
// root.render(<App />);

Input: (Initial render of App)

Output: The ThemedButton will render with a dark background and light text, displaying "I am a dark button".

Explanation: The App component provides the theme state ('dark') via ThemeContext.Provider. The ThemedButton consumes this value using ThemeContext.Consumer and renders accordingly.

Example 2: Updating Context Value

Continuing from Example 1, if the user clicks the "Toggle Theme" button:

Input: User clicks "Toggle Theme" button. setTheme is called with 'light'.

Output: The ThemedButton will re-render with a light background and dark text, displaying "I am a light button".

Explanation: The state in App updates, causing ThemeContext.Provider to re-render with a new value. This change propagates to the ThemedButton's Consumer, triggering a re-render with the updated context value.

Example 3: No Provider

// Assume createMyContext is implemented
const UserContext = createMyContext({ name: 'Guest' });

function Greeting() {
  return (
    <UserContext.Consumer>
      {user => (
        <p>Hello, {user.name}!</p>
      )}
    </UserContext.Consumer>
  );
}

// Render Greeting without any UserContext.Provider ancestor
// const root = ReactDOM.createRoot(document.getElementById('root')!);
// root.render(<Greeting />);

Input: Render Greeting component directly.

Output: The component will render <p>Hello, Guest!</p>.

Explanation: Since there is no UserContext.Provider ancestor, the UserContext.Consumer falls back to using the defaultValue ({ name: 'Guest' }) provided when createMyContext was called.

Constraints

  • Your implementation should use TypeScript.
  • The Provider component should be a functional component or a class component that manages its internal state for the value prop.
  • The Consumer component should accept a function as its child.
  • The system should handle re-renders efficiently when the context value changes.
  • Avoid using React's built-in useContext, createContext, or Context.Consumer/Context.Provider. You are building these from scratch.

Notes

  • Consider how to subscribe Consumer components to Provider updates. You might need to store a list of subscribers within the Provider.
  • Think about how to trigger re-renders in the Consumer when the Provider's value changes.
  • The Provider will likely need to use internal state (useState or setState) to manage its value prop updates.
  • When a Consumer renders, it needs to find the closest Provider ancestor. You might need to simulate this traversal or use a mechanism to pass down information. A common approach is to have the Provider create a "context node" in the component tree that Consumers can look for.
Loading editor...
typescript