Hone logo
Hone
Problems

Build a React Selector Library in TypeScript

This challenge asks you to create a reusable React hook that simplifies the process of selecting and transforming data from a global state. Many applications require complex logic to derive specific pieces of information from a central store. A well-designed selector hook can improve performance by memoizing results and make your components cleaner and more readable.

Problem Description

You need to build a custom React hook, useSelector, that mimics the behavior of popular state management libraries' selectors. This hook will take a selector function as an argument and return the selected value from a conceptual global state.

Key Requirements:

  1. useSelector Hook: Create a TypeScript hook named useSelector<TState, TSelected>.
    • TState: The type of your global state.
    • TSelected: The type of the value returned by the selector function.
  2. Selector Function: The hook should accept a selector function. This function will receive the current global state and return a derived value.
    • Signature: (state: TState) => TSelected
  3. State Updates: The hook should subscribe to changes in the global state. When the state changes, it should re-run the selector function.
  4. Return Value: The hook should return the result of the selector function.
  5. Memoization: Crucially, the hook must memoize the result of the selector function. If the state changes but the result of the selector function does not change, the component using the hook should not re-render. This is a core performance optimization.
  6. Global State Simulation: For this challenge, you will simulate a global state and a mechanism to update it. You do not need to implement a full Redux-like store. A simple useState within a context provider can serve as your global state manager.

Expected Behavior:

  • When a component mounts, useSelector should execute the selector function with the current state and return the initial selected value.
  • When the global state is updated, useSelector should re-evaluate the selector function.
  • If the new result of the selector function is different from the previously returned value, the component using the hook should re-render.
  • If the new result of the selector function is the same as the previously returned value (e.g., using === comparison), the component should not re-render, even though the global state has changed.

Edge Cases to Consider:

  • Initial Render: Ensure the selector is called correctly on the first render.
  • No State Change: Verify that components don't re-render if the state changes but the selector output remains the same.
  • Complex Selectors: The memoization should work even for selectors that compute values based on multiple parts of the state.
  • Function/Object Equality: The memoization should handle primitive types, objects, and arrays correctly. The common approach is reference equality for non-primitives.

Examples

Example 1: Simple Value Selection

// Assume a global state and a provider that manages it
interface AppState {
  user: { name: string; id: number } | null;
  theme: 'light' | 'dark';
}

const initialState: AppState = {
  user: { name: 'Alice', id: 1 },
  theme: 'light',
};

// Provider component (simplified for context)
// const AppStateProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
//   const [state, setState] = useState(initialState);
//   return (
//     <AppContext.Provider value={{ state, setState }}>
//       {children}
//     </AppContext.Provider>
//   );
// };

// In a component:
function UserNameDisplay() {
  // Selector to get the user's name
  const userName = useSelector((state: AppState) => state.user?.name);

  console.log('UserNameDisplay rendered'); // This should only log when userName changes

  return <div>User: {userName || 'Guest'}</div>;
}

// --- Scenario ---
// Initial State: { user: { name: 'Alice', id: 1 }, theme: 'light' }
//
// Initial Render of UserNameDisplay:
//   useSelector((state) => state.user?.name) is called with state.
//   Selector returns 'Alice'.
//   UserNameDisplay renders for the first time.
//   Console logs: "UserNameDisplay rendered"
//
// State Update:
//   globalState = { user: { name: 'Bob', id: 2 }, theme: 'light' }
//
// Re-evaluation:
//   useSelector is triggered.
//   Selector is called with new state.
//   Selector returns 'Bob'.
//   'Bob' !== 'Alice', so UserNameDisplay re-renders.
//   Console logs: "UserNameDisplay rendered"
//
// State Update:
//   globalState = { user: { name: 'Bob', id: 3 }, theme: 'dark' }
//
// Re-evaluation:
//   useSelector is triggered.
//   Selector is called with new state.
//   Selector returns 'Bob'.
//   'Bob' === 'Bob', so UserNameDisplay does NOT re-render.
//   Console logs: NO new log

Example 2: Derived Value (Object)

// Using the same AppState interface as above

function UserInfo() {
  // Selector to get a derived object
  const userInfo = useSelector((state: AppState) => ({
    name: state.user?.name,
    isLoggedIn: !!state.user,
  }));

  console.log('UserInfo rendered', userInfo); // Should only log when userInfo object changes reference

  return (
    <div>
      <p>Name: {userInfo.name || 'Guest'}</p>
      <p>Status: {userInfo.isLoggedIn ? 'Logged In' : 'Logged Out'}</p>
    </div>
  );
}

// --- Scenario ---
// Initial State: { user: { name: 'Alice', id: 1 }, theme: 'light' }
//
// Initial Render of UserInfo:
//   useSelector((state) => ({ name: state.user?.name, isLoggedIn: !!state.user }))
//   Selector returns { name: 'Alice', isLoggedIn: true }.
//   UserInfo renders.
//   Console logs: "UserInfo rendered { name: 'Alice', isLoggedIn: true }"
//
// State Update:
//   globalState = { user: { name: 'Alice', id: 1 }, theme: 'dark' } (only theme changed)
//
// Re-evaluation:
//   useSelector is triggered.
//   Selector is called with new state.
//   Selector returns { name: 'Alice', isLoggedIn: true }.
//   The returned object reference is the SAME as before (or a deep comparison would yield the same result).
//   UserInfo does NOT re-render.
//   Console logs: NO new log
//
// State Update:
//   globalState = { user: null, theme: 'dark' }
//
// Re-evaluation:
//   useSelector is triggered.
//   Selector is called with new state.
//   Selector returns { name: undefined, isLoggedIn: false }.
//   The returned object reference is DIFFERENT.
//   UserInfo re-renders.
//   Console logs: "UserInfo rendered { name: undefined, isLoggedIn: false }"

Constraints

  • TypeScript: The solution must be written in TypeScript.
  • Hook-based: The core functionality must be encapsulated in a custom React hook.
  • Memoization Strategy: A common and acceptable memoization strategy is to store the last selected value and compare it with the new value using a strict equality check (===). This works well for primitives and object references.
  • State Management Simulation: You need to simulate a state update mechanism. Using React's useState within a Context.Provider is the recommended approach for this simulation.
  • No External Libraries: Do not use any external state management libraries like Redux, Zustand, Jotai, etc., for implementing the useSelector hook itself. You can use them as inspiration.

Notes

  • Think about how to manage subscriptions to state changes. When the state changes, how does your hook know to re-evaluate?
  • The memoization is key to preventing unnecessary re-renders. Consider the equality check carefully.
  • You'll need to provide a basic AppContext and useAppContext hook to access the simulated state and the setState function from your provider.
  • Focus on making the useSelector hook robust and efficient. The provided examples should serve as your primary testing ground.
Loading editor...
typescript