Hone logo
Hone
Problems

React State Management Library Challenge

This challenge asks you to build a foundational state management library for React applications using TypeScript. You will create a system that allows components to subscribe to global state changes, update that state, and trigger re-renders when the state they depend on changes. This is a core concept for building complex and scalable React applications without relying on external libraries.

Problem Description

Your task is to implement a simple, yet effective, state management solution in TypeScript for React. This solution should provide a way to:

  1. Define and initialize global state: A single source of truth for application data.
  2. Access state within components: Components should be able to read specific parts of the global state.
  3. Update state: Provide mechanisms to modify the global state immutably.
  4. Subscribe to state changes: Components should re-render automatically when the relevant parts of the global state change.

Key Requirements:

  • createStore(initialState): A function that takes an initial state object and returns a store object.
    • The store object should have a getState() method to retrieve the current state.
    • The store object should have a setState(updater) method to update the state. The updater can be either a new state object or a function that receives the current state and returns the new state. This ensures immutability.
    • The store object should have a subscribe(callback) method that registers a callback function to be executed whenever the state changes. It should return an unsubscribe function to remove the callback.
  • useStore(store, selector) hook: A React hook that allows components to subscribe to state changes.
    • It takes the store object (created by createStore) and an optional selector function.
    • The selector function receives the current state and should return the specific piece of state the component is interested in. If no selector is provided, the entire state is returned.
    • The hook should return the selected state.
    • When the selected state changes, the component using the hook should re-render.
    • The hook must handle multiple components subscribing to the same store and different parts of the state.
    • Ensure that only components whose selected state has actually changed are re-rendered.

Expected Behavior:

When the global state is updated using setState, all subscribed components should be notified. Components whose selected state (as determined by the selector function) has changed should re-render. Components whose selected state has not changed should not re-render.

Edge Cases:

  • No selector provided to useStore: The hook should correctly return the entire state.
  • Multiple components subscribing to the same part of the state.
  • Multiple components subscribing to different parts of the state.
  • State updates that result in no actual change to the selected state for a component.
  • Unsubscribing from the store.

Examples

Example 1: Basic State Access and Update

// Assume createStore and useStore are implemented correctly

// Initial state
const initialState = { count: 0, message: "Hello" };

// Create the store
const myStore = createStore(initialState);

// Component A (subscribes to the entire state)
function ComponentA() {
  const state = useStore(myStore); // No selector, gets entire state
  return <div>Count: {state.count}, Message: {state.message}</div>;
}

// Component B (subscribes to a specific part of the state)
function ComponentB() {
  const count = useStore(myStore, (state) => state.count); // Selects 'count'
  return <div>Current Count: {count}</div>;
}

// --- Usage ---
// Initial render:
// ComponentA shows: Count: 0, Message: Hello
// ComponentB shows: Current Count: 0

// After calling myStore.setState({ count: 1 });
// ComponentA shows: Count: 1, Message: Hello (re-renders)
// ComponentB shows: Current Count: 1 (re-renders)

// After calling myStore.setState({ message: "World" });
// ComponentA shows: Count: 1, Message: World (re-renders)
// ComponentB shows: Current Count: 1 (does NOT re-render as 'count' hasn't changed)

Example 2: State Update with Updater Function

// Assume createStore and useStore are implemented correctly

const initialState = { counter: { value: 10 } };
const counterStore = createStore(initialState);

function CounterDisplay() {
  const value = useStore(counterStore, (state) => state.counter.value);
  return <div>Counter: {value}</div>;
}

function CounterButton() {
  const increment = () => {
    counterStore.setState(prevState => ({
      counter: {
        ...prevState.counter, // Important for immutability
        value: prevState.counter.value + 1
      }
    }));
  };

  return <button onClick={increment}>Increment</button>;
}

// --- Usage ---
// Initial render:
// CounterDisplay shows: Counter: 10

// Clicking the button:
// 1. Increment: CounterDisplay shows: Counter: 11
// 2. Increment: CounterDisplay shows: Counter: 12

Example 3: Unsubscribing

// Assume createStore and useStore are implemented correctly

const initialState = { data: "initial" };
const dataStore = createStore(initialState);

function DataDisplay() {
  const data = useStore(dataStore, (state) => state.data);
  return <div>Data: {data}</div>;
}

function App() {
  const [showDisplay, setShowDisplay] = React.useState(true);

  return (
    <div>
      {showDisplay && <DataDisplay />}
      <button onClick={() => setShowDisplay(false)}>Hide Display</button>
      <button onClick={() => dataStore.setState({ data: "updated" })}>Update Data</button>
    </div>
  );
}

// --- Usage ---
// Initial render: DataDisplay shows: Data: initial
// Clicking "Update Data": DataDisplay shows: Data: updated

// Clicking "Hide Display": DataDisplay component unmounts.
// The subscription associated with DataDisplay should be automatically cleaned up.
// Clicking "Update Data" again should not cause errors or try to update an unmounted component's state.

Constraints

  • TypeScript Version: Use TypeScript 4.0 or later.
  • React Version: Assume React 17 or later.
  • No External State Management Libraries: Do not use libraries like Redux, Zustand, Jotai, Recoil, etc. Your solution must be built from scratch.
  • Immutability: State updates must be immutable. Directly mutating the state object is not allowed.
  • Performance: The useStore hook should only trigger re-renders when the selected state actually changes. Avoid unnecessary re-renders.
  • Dependencies: Your solution should not introduce any dependencies beyond React itself and its core utilities.

Notes

  • Consider how you will manage the list of subscribers and how to efficiently notify only those whose relevant state has changed.
  • The selector function is crucial for performance optimization. Think about how to compare the previous selected value with the new one to determine if a re-render is necessary.
  • Remember to handle component unmounting and unsubscribe listeners to prevent memory leaks.
  • This is a simplified state management library. Real-world libraries often have more advanced features (middleware, dev tools integration, etc.), but focus on the core functionality described here.
Loading editor...
typescript