Hone logo
Hone
Problems

Custom React useContextSelector Hook

This challenge asks you to create a custom React hook, useContextSelector, that mirrors the functionality of react-use-contextselector. This hook allows components to subscribe to specific parts of a React Context, optimizing re-renders by only updating when the selected slice of the context changes.

Problem Description

Your task is to implement a useContextSelector hook in TypeScript. This hook should accept a React Context and a selector function as arguments. The selector function will be used to extract a specific piece of data from the context's value. The useContextSelector hook should then return this selected value.

Key Requirements:

  1. Context Subscription: The hook must correctly subscribe to changes in the provided React Context.
  2. Selector Function: It must accept a selector function that takes the context's value and returns a specific part of it.
  3. Value Return: The hook should return the value produced by the selector function.
  4. Optimized Re-renders: The component using useContextSelector should only re-render if the value returned by the selector function actually changes. If other parts of the context value change but are not selected, the component should not re-render.
  5. Type Safety: The implementation must be in TypeScript, ensuring type safety for context values and selector functions.

Expected Behavior:

  • When the context value changes, useContextSelector should re-evaluate the selector function.
  • If the new selected value is different from the previous selected value, the component using the hook will re-render.
  • If the new selected value is the same as the previous selected value, the component will not re-render.

Edge Cases:

  • Consider the initial render and how the first value is determined.
  • Handle scenarios where the context value is undefined or null (though typically context providers ensure a value).
  • The selector function might return primitive types, objects, or arrays. The comparison for re-rendering should be robust.

Examples

Example 1: Simple Selector

Consider a UserContext that holds an object with name and age.

// Assume UserContext is defined elsewhere and provided
// const UserContext = React.createContext<{ name: string; age: number } | undefined>(undefined);

// Component using useContextSelector
function UserNameDisplay() {
  const userName = useContextSelector(UserContext, (user) => user?.name);
  console.log("UserNameDisplay rendered");
  return <div>User Name: {userName}</div>;
}

// If the UserContext value changes from
// { name: "Alice", age: 30 } to { name: "Bob", age: 30 }
// UserNameDisplay will re-render because user?.name changed.

// If the UserContext value changes from
// { name: "Alice", age: 30 } to { name: "Alice", age: 31 }
// UserNameDisplay will NOT re-render because user?.name remained "Alice".

Example 2: Selector Returning an Object

Consider a SettingsContext with a theme and fontSize.

// Assume SettingsContext is defined elsewhere and provided
// const SettingsContext = React.createContext<{ theme: string; fontSize: number } | undefined>(undefined);

// Component using useContextSelector
function ThemeDisplay() {
  const themeSettings = useContextSelector(SettingsContext, (settings) => ({
    theme: settings?.theme,
    fontSize: settings?.fontSize,
  }));
  console.log("ThemeDisplay rendered");
  return <div>Theme: {themeSettings.theme}, Font Size: {themeSettings.fontSize}</div>;
}

// If SettingsContext value changes from { theme: "dark", fontSize: 16 } to { theme: "light", fontSize: 16 }
// ThemeDisplay will re-render because the returned object { theme: "light", fontSize: 16 } is different.

// If SettingsContext value changes from { theme: "dark", fontSize: 16 } to { theme: "dark", fontSize: 18 }
// ThemeDisplay will re-render because the returned object { theme: "dark", fontSize: 18 } is different.

Example 3: Memoizing Selector Output

While the hook should handle re-renders based on selected value changes, the user of the hook can further optimize by memoizing the output of their selector if needed, though useContextSelector itself handles the primary optimization.

import React, { useContext, useState, useEffect, useRef } from 'react';

// Assuming useContextSelector is implemented correctly and available
// const useContextSelector = ...

interface ComplexContextValue {
  user: { name: string; id: number };
  settings: { notificationsEnabled: boolean; language: string };
}

const ComplexContext = React.createContext<ComplexContextValue | undefined>(undefined);

function UserNameAndLangDisplay() {
  // Simple selector without memoization (may re-render more than necessary if language also changes frequently)
  // const userName = useContextSelector(ComplexContext, (ctx) => ctx?.user.name);
  // const userLang = useContextSelector(ComplexContext, (ctx) => ctx?.settings.language);

  // Using a single selector to extract both, which is better practice if they are often used together.
  // The hook will only re-render if EITHER name OR language changes.
  const nameAndLang = useContextSelector(ComplexContext, (ctx) => ({
    name: ctx?.user.name,
    language: ctx?.settings.language,
  }));

  console.log("UserNameAndLangDisplay rendered");
  return (
    <div>
      User Name: {nameAndLang.name}, Language: {nameAndLang.language}
    </div>
  );
}

Constraints

  • The hook must be implemented using standard React hooks (useContext, useState, useRef, useEffect).
  • The implementation should avoid relying on external libraries for the core hook logic.
  • The comparison between the previous and current selected values should be a shallow comparison for primitive types and object references. For more complex deep comparisons, the user of the hook would need to memoize the selector's output.
  • Performance: The hook should be efficient and not introduce unnecessary overhead beyond what's required for context listening and comparison.

Notes

  • Think about how to store the previous selected value to compare against the new one.
  • Consider the dependency array for useEffect to ensure the hook re-subscribes correctly if the context or selector changes (though typically the context reference is stable).
  • The selector function itself should ideally be stable (e.g., defined outside the component or memoized with useCallback by the component using useContextSelector) to prevent unnecessary re-renders triggered by the selector reference changing. However, your hook should still function correctly even if the selector reference changes.
  • The comparison logic for determining if a re-render is needed is crucial for fulfilling the optimization requirement.
Loading editor...
typescript