Building a Theming System with a Custom React Context Hook
This challenge focuses on creating a robust and reusable theming system for a React application using custom context and hooks. You will implement a mechanism to dynamically switch between different themes (e.g., light and dark) and provide this theme information to any component in your application through a custom hook.
Problem Description
Your task is to build a custom React Context hook that manages application themes. This hook should allow components to:
- Access the current theme: Components should be able to retrieve the currently active theme object.
- Toggle the theme: Components should be able to trigger a change from the current theme to an alternative one (e.g., from light to dark, or dark to light).
You need to create a ThemeProvider component that wraps your application, a useTheme custom hook for components to consume the theme, and define at least two distinct theme objects.
Key Requirements:
ThemeProviderComponent:- Accepts
childrenas props. - Internally manages the current theme state.
- Provides the current theme and a function to toggle the theme via React Context.
- Accepts
useThemeCustom Hook:- Should be used within components that need access to the theme.
- Returns an object containing the
currentThemeand atoggleThemefunction. - Should throw an error if used outside of a
ThemeProvider.
- Theme Objects:
- Define at least two distinct theme objects (e.g.,
lightTheme,darkTheme). Each theme object should contain properties that can be used for styling (e.g.,backgroundColor,textColor,borderColor).
- Define at least two distinct theme objects (e.g.,
- Initial Theme: The system should start with a default theme (e.g.,
lightTheme).
Expected Behavior:
When the toggleTheme function is called, the currentTheme accessible via the useTheme hook should update to the other defined theme, and all consuming components should re-render with the new theme's styling properties.
Edge Cases:
- What happens if
useThemeis called in a component that is not a descendant ofThemeProvider?
Examples
Let's imagine a simple DisplayTheme component that uses the useTheme hook.
Example 1: Initial Render
// Assume Light Theme is the initial theme
const lightTheme = {
backgroundColor: '#ffffff',
textColor: '#000000',
borderColor: '#cccccc',
};
const darkTheme = {
backgroundColor: '#1a1a1a',
textColor: '#ffffff',
borderColor: '#444444',
};
// Inside App.tsx:
// <ThemeProvider themes={{ light: lightTheme, dark: darkTheme }} initialThemeName="light">
// <DisplayTheme />
// </Content>
// Inside DisplayTheme.tsx:
function DisplayTheme() {
const { currentTheme } = useTheme(); // Assume useTheme is correctly implemented
return (
<div style={{ backgroundColor: currentTheme.backgroundColor, color: currentTheme.textColor }}>
This is some text.
</div>
);
}
Input: ThemeProvider initialized with lightTheme as the default. DisplayTheme component renders.
Output:
The div will have styles backgroundColor: '#ffffff' and color: '#000000'.
Explanation: The useTheme hook correctly retrieves the lightTheme object from the context, which is then applied to the div's styles.
Example 2: After Toggling Theme
// Assume the same theme objects as above.
// Inside App.tsx:
// <ThemeProvider themes={{ light: lightTheme, dark: darkTheme }} initialThemeName="light">
// <DisplayTheme />
// <button onClick={() => toggleTheme()}>Toggle</button> {/* Assuming toggleTheme is accessible */}
// </Content>
// Inside DisplayTheme.tsx:
function DisplayTheme() {
const { currentTheme } = useTheme();
return (
<div style={{ backgroundColor: currentTheme.backgroundColor, color: currentTheme.textColor }}>
This is some text.
</div>
);
}
Input: The user clicks the "Toggle" button.
Output:
The div will now have styles backgroundColor: '#1a1a1a' and color: '#ffffff'.
Explanation: Calling toggleTheme updates the state within ThemeProvider. This causes a re-render, and useTheme now retrieves the darkTheme object, updating the component's appearance.
Example 3: Using useTheme outside of ThemeProvider
// Outside of any ThemeProvider
function StandaloneComponent() {
const { currentTheme } = useTheme(); // This will throw an error
// ...
}
Input: StandaloneComponent attempts to render useTheme() without being wrapped in ThemeProvider.
Output: An error message: "useTheme must be used within a ThemeProvider".
Explanation: The useTheme hook's implementation includes a check to ensure it's being used within the correct context. If not, it throws an informative error.
Constraints
- The
ThemeProvidershould accept an object mapping theme names (strings) to theme objects. - The
ThemeProvidershould also accept aninitialThemeNamestring to set the starting theme. - The
useThemehook should return a{ currentTheme: ThemeType, toggleTheme: () => void }object. - Theme objects can be any shape, but for this challenge, assume they contain
backgroundColor,textColor, andborderColorproperties. - The solution should be implemented in TypeScript.
Notes
- Consider how you will manage the mapping between theme names and actual theme objects.
- Think about how React's Context API works and how to create a provider and a consumer hook.
- When implementing the
toggleThemefunction, consider how you'll switch between the available themes. - The error handling for
useThemeis crucial for usability and debugging. - You might want to define a
Themeinterface or type to ensure consistency in your theme objects.