Hone logo
Hone
Problems

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:

  1. Access the current theme: Components should be able to retrieve the currently active theme object.
  2. 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:

  • ThemeProvider Component:
    • Accepts children as props.
    • Internally manages the current theme state.
    • Provides the current theme and a function to toggle the theme via React Context.
  • useTheme Custom Hook:
    • Should be used within components that need access to the theme.
    • Returns an object containing the currentTheme and a toggleTheme function.
    • 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).
  • 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 useTheme is called in a component that is not a descendant of ThemeProvider?

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 ThemeProvider should accept an object mapping theme names (strings) to theme objects.
  • The ThemeProvider should also accept an initialThemeName string to set the starting theme.
  • The useTheme hook should return a { currentTheme: ThemeType, toggleTheme: () => void } object.
  • Theme objects can be any shape, but for this challenge, assume they contain backgroundColor, textColor, and borderColor properties.
  • 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 toggleTheme function, consider how you'll switch between the available themes.
  • The error handling for useTheme is crucial for usability and debugging.
  • You might want to define a Theme interface or type to ensure consistency in your theme objects.
Loading editor...
typescript