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:
-
createMyContextfunction:- This function should accept an optional
defaultValue. - It should return an object with two properties:
ProviderandConsumer.
- This function should accept an optional
-
Providercomponent:- This component will be used to wrap a part of your application tree.
- It accepts a
valueprop, which is the data that will be made available to its descendants. - It should render its
children.
-
Consumercomponent:- This component will be used to access the value provided by the nearest
Providerancestor. - It accepts a render prop function as its single child. This function will receive the context
valueas an argument and should return a React element. - If no
Provideris found in the ancestry, theConsumershould use thedefaultValueprovided tocreateMyContext.
- This component will be used to access the value provided by the nearest
-
State Management: The
Providershould be able to update itsvalueprop, and allConsumers 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'svalueprop changes, all consuming components should automatically update. - If a
Consumeris rendered outside of anyProvider, it should receive thedefaultValue.
Edge cases:
- Multiple nested
Providers: Consumers should always receive the value from the nearest ancestorProvider. Providerwith novalueprop: Should default toundefinedif nodefaultValuewas 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
Providercomponent should be a functional component or a class component that manages its internal state for thevalueprop. - The
Consumercomponent 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, orContext.Consumer/Context.Provider. You are building these from scratch.
Notes
- Consider how to subscribe
Consumercomponents toProviderupdates. You might need to store a list of subscribers within theProvider. - Think about how to trigger re-renders in the
Consumerwhen theProvider's value changes. - The
Providerwill likely need to use internal state (useStateorsetState) to manage itsvalueprop updates. - When a
Consumerrenders, it needs to find the closestProviderancestor. You might need to simulate this traversal or use a mechanism to pass down information. A common approach is to have theProvidercreate a "context node" in the component tree thatConsumers can look for.