Hone logo
Hone
Problems

Recreate React's useState Hook in TypeScript

The useState hook is a fundamental building block in React for managing component state. Understanding how it works under the hood is crucial for any React developer. This challenge asks you to implement your own useState hook in TypeScript, replicating its core functionality.

Problem Description

Your task is to create a custom useState hook in TypeScript that behaves similarly to React's built-in useState. This hook should allow a component to declare a state variable and provide a function to update that state.

Key Requirements:

  1. Initialization: The hook should accept an initial value for the state. If no initial value is provided, the state should be undefined.
  2. State Value: The hook should return an array containing two elements:
    • The current state value.
    • A function to update the state.
  3. State Update: The update function should accept either a new value or a function that receives the previous state and returns the new state.
  4. Re-renders: Crucially, when the state is updated, the component that uses the hook should re-render. This implies a mechanism to trigger re-renders.

Expected Behavior:

When useState is called within a functional component:

  • The first time, it returns the initial value and a setter function.
  • Subsequent calls (after state updates) should return the latest state value and the same setter function.
  • Calling the setter function should update the state and cause the component to re-render.

Edge Cases to Consider:

  • Handling the case where useState is called without an initial value.
  • Ensuring that multiple useState calls within the same component are managed independently.

Examples

Example 1:

Imagine a simple Counter component using your custom useState.

import { render, screen, fireEvent } from '@testing-library/react'; // Assume testing library for rendering

// Assume your custom useState hook is imported as 'customUseState'
import { customUseState } from './customUseState';

function Counter() {
  const [count, setCount] = customUseState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>Decrement</button>
    </div>
  );
}

// In your test file:
// render(<Counter />);
// const countElement = screen.getByText('Count: 0');
// expect(countElement).toBeInTheDocument();

// fireEvent.click(screen.getByText('Increment'));
// expect(screen.getByText('Count: 1')).toBeInTheDocument();

// fireEvent.click(screen.getByText('Decrement'));
// expect(screen.getByText('Count: 0')).toBeInTheDocument();

Input (Conceptual Usage):

  1. Counter component is rendered initially.
  2. customUseState(0) is called, returning [0, setterFunction].
  3. The component displays "Count: 0".
  4. "Increment" button is clicked. setterFunction(1) is called.
  5. The component re-renders with the new state.

Output (Conceptual Rendering):

  • Initially: "Count: 0"
  • After clicking "Increment": "Count: 1"
  • After clicking "Decrement": "Count: 0"

Example 2:

Demonstrating functional updates.

// Assume your custom useState hook is imported as 'customUseState'
import { customUseState } from './customUseState';

function NameChanger() {
  const [name, setName] = customUseState("Alice");

  const updateName = () => {
    setName(prevName => {
      if (prevName === "Alice") {
        return "Bob";
      } else if (prevName === "Bob") {
        return "Charlie";
      }
      return "Alice";
    });
  };

  return (
    <div>
      <p>Name: {name}</p>
      <button onClick={updateName}>Change Name</button>
    </div>
  );
}

// In your test file:
// render(<NameChanger />);
// expect(screen.getByText('Name: Alice')).toBeInTheDocument();

// fireEvent.click(screen.getByText('Change Name'));
// expect(screen.getByText('Name: Bob')).toBeInTheDocument();

// fireEvent.click(screen.getByText('Change Name'));
// expect(screen.getByText('Name: Charlie')).toBeInTheDocument();

// fireEvent.click(screen.getByText('Change Name'));
// expect(screen.getByText('Name: Alice')).toBeInTheDocument();

Input (Conceptual Usage):

  1. NameChanger component is rendered.
  2. customUseState("Alice") is called.
  3. The component displays "Name: Alice".
  4. "Change Name" button is clicked. setName(prevName => ...) is called.
  5. The component re-renders.

Output (Conceptual Rendering):

  • Initially: "Name: Alice"
  • After first click: "Name: Bob"
  • After second click: "Name: Charlie"
  • After third click: "Name: Alice"

Constraints

  • Your implementation should be a single function named customUseState.
  • The function signature should be customUseState<T>(initialValue?: T): [T | undefined, (newValue: T | ((prevState: T) => T)) => void].
  • You cannot use React's built-in useState or any other React hooks directly within your customUseState implementation.
  • You will need a mechanism to simulate component re-renders. For the purpose of this challenge, you can assume a global or imported triggerRender function exists that, when called, causes the "component" to re-render. In a real scenario, this would be handled by React's reconciliation process.

Notes

This challenge is about understanding state management at a fundamental level. You'll need to consider how to store the state for each component instance that uses your hook and how to trigger a re-render when that state changes. Think about how React internally manages the order of hooks. You might need to simulate this by maintaining an array of state values and an index to track which state belongs to which hook call within a component's lifecycle.

Loading editor...
typescript