Hone logo
Hone
Problems

Implementing a Custom useState Hook

Modern UI frameworks heavily rely on reactive state management. The useState hook is a fundamental primitive for introducing local, mutable state into functional components. This challenge involves recreating the core mechanics of such a hook, focusing on how state is maintained and updates trigger effects.

Problem Description

Your task is to implement a createUseState factory function that produces a useState hook. This custom useState hook will manage a single piece of state. When this state is updated, it should trigger a provided "render" or "effect" callback, simulating how a component would react to state changes.

Key requirements for your implementation:

  1. createUseState Function:

    • This factory function will take a single argument: renderCallback.
    • renderCallback is a function that takes no arguments and will be invoked whenever the state managed by the produced useState hook changes.
    • It should return the useState function.
  2. useState Function (returned by createUseState):

    • This function will take an initialState argument.
    • On the first call within its context, it should initialize the state with initialState. On subsequent calls (simulating re-renders), it should ignore initialState and return the current, established state.
    • It should return a tuple or array containing two elements:
      • The current state value.
      • A setState function.
  3. setState Function:

    • This function will take one argument, newState.
    • newState can be either:
      • A direct value (e.g., setState(5)).
      • A function that takes the previous state as an argument and returns the new state (e.g., setState(prev => prev + 1)).
    • It must update the internal state based on newState.
    • Crucially, it should invoke the renderCallback (provided to createUseState) only if the calculated newState is strictly different (!==) from the previous state. If the state remains the same, renderCallback should not be called.

Expected Behavior:

  • The useState function should maintain its state across multiple invocations within the same "component execution" context (simulating re-renders).
  • The renderCallback should accurately reflect state changes, being invoked only when an actual update occurs.

Edge Cases to Consider:

  • initialState could be null, undefined, 0, empty string, etc.
  • setState can receive a function as an update.
  • setState can be called with a value that is identical to the current state.

Examples

Example 1: Basic State Update

// Define a mock render function to observe calls
function mockRender(message) {
  LOG("Render called: " + message);
}

// Create our custom useState hook factory
const customUseState = createUseState(mockRender);

// Simulate a component function that uses the hook
function MyComponent() {
  const [count, setCount] = customUseState(0);

  LOG("Current count in MyComponent: " + count);

  // Simulate an interaction that updates state if count is less than 2
  // This condition prevents infinite loops in this example setup
  if (count < 2) {
    setCount(count + 1);
  }
}

// Initial call to simulate component mounting
MyComponent();
// Subsequent calls to simulate re-renders after state updates
MyComponent();
MyComponent();

Output:

Current count in MyComponent: 0
Render called: State updated.
Current count in MyComponent: 1
Render called: State updated.
Current count in MyComponent: 2

Explanation:

  1. MyComponent() is called. customUseState(0) initializes count to 0. LOG shows 0. setCount(1) is then called.
  2. setCount(1) updates the internal state. Since 1 is different from 0, mockRender is called.
  3. MyComponent() is implicitly "re-rendered" (called again). customUseState now returns 1. LOG shows 1. setCount(2) is then called.
  4. setCount(2) updates state. Since 2 is different from 1, mockRender is called.
  5. MyComponent() is implicitly "re-rendered" again. customUseState now returns 2. LOG shows 2. The if condition count < 2 is false, so setCount is not called again.

Example 2: Functional Updates and No-Op Updates

let renderTriggerCount = 0;
function mockRenderCallback() {
  renderTriggerCount++;
  LOG("Mock render callback invoked. Total: " + renderTriggerCount);
}

const customUseState = createUseState(mockRenderCallback);

let currentRenderedValue = null;
let stateUpdaterFunction = null;

// Simulate a component execution
function ComponentRenderer() {
  const [value, setValue] = customUseState("start");
  currentRenderedValue = value; // Store for external observation
  stateUpdaterFunction = setValue; // Store for external manipulation
  LOG("ComponentRenderer - Current value: " + value);
}

// Initial component mount
ComponentRenderer();

// Update using a function
stateUpdaterFunction(prev => prev + "ING");
LOG("After functional update. External observed value: " + currentRenderedValue);
// Simulate re-render
ComponentRenderer();

// Update with the same value (should NOT trigger render)
stateUpdaterFunction("starting");
LOG("After no-op update. External observed value: " + currentRenderedValue);
// Simulate re-render
ComponentRenderer();

// Update with a different direct value
stateUpdaterFunction("finished");
LOG("After direct update. External observed value: " + currentRenderedValue);
// Simulate re-render
ComponentRenderer();

Output:

ComponentRenderer - Current value: start
Mock render callback invoked. Total: 1
After functional update. External observed value: start
ComponentRenderer - Current value: startING
After no-op update. External observed value: startING
ComponentRenderer - Current value: startING
Mock render callback invoked. Total: 2
After direct update. External observed value: startING
ComponentRenderer - Current value: finished

Explanation:

  1. ComponentRenderer() mounts. useState("start") initializes state to "start".
  2. stateUpdaterFunction (which is setValue) is called with prev => prev + "ING". State becomes "startING". mockRenderCallback is triggered.
  3. ComponentRenderer() simulates a re-render. useState("start") returns "startING" (initial value "start" is ignored as state already exists).
  4. stateUpdaterFunction("starting") is called. Since current state ("startING") is not "starting", state becomes "starting". mockRenderCallback is triggered.
  5. ComponentRenderer() simulates a re-render. useState("start") now returns "starting".
  6. stateUpdaterFunction("finished") is called. Since current state ("starting") is different from "finished", state becomes "finished". mockRenderCallback is triggered.
  7. ComponentRenderer() simulates a re-render. useState("start") now returns "finished".

Constraints

  • The initialState can be any primitive value (number, string, boolean, null, undefined) or a simple object/array literal.
  • The renderCallback is guaranteed to be a function that takes no arguments and has no return value.
  • The setState function must handle both direct values and functional updates.
  • The useState hook should maintain its state persistently across multiple invocations of the "component" it's used in.
  • The setState function should only trigger the renderCallback if the new state value is strictly different (!==) from the previous state value.

Notes

  • This challenge simplifies a real framework's useState by focusing on a single state variable that is managed by the createUseState factory. Real hooks manage multiple states within a single component instance by relying on call order, which is beyond the scope of this exercise.
  • Consider how to maintain the state outside the useState function itself, but within the scope of the createUseState call, to ensure persistence across "component renders." This closure pattern is fundamental to how such hooks work.
  • The renderCallback simulates the "re-render" or "side-effect" that happens when state changes in a reactive system. Understanding its role is crucial for grasping reactivity.
Loading editor...
plaintext