Hone logo
Hone
Problems

Build Your Own React-Inspired Custom Hooks System

Imagine building a library of reusable logic that can be shared across different components without resorting to complex inheritance patterns. This challenge asks you to create a simplified, custom hook system in JavaScript, inspired by React's custom hooks. This system will allow developers to encapsulate stateful logic and side effects, making it easy to reuse and compose across your application.

Problem Description

Your task is to implement a basic framework for creating and using custom hooks in JavaScript. This system should mimic the core concepts of React hooks:

  • Hook Registration: A mechanism to register custom hooks.
  • Hook Execution Context: A way to manage the state and effects associated with each hook instance within a "component" (which will be simulated).
  • State Management: A useState equivalent that allows hooks to manage their own local state.
  • Effect Management: A useEffect equivalent that allows hooks to run side effects.
  • Hook Chaining: The ability for custom hooks to call other custom hooks.

You will need to create a createHookSystem function that returns an object with methods for defining and running hooks. The system should support a simulated "component" context to track hook state and execution.

Key Requirements:

  1. createHookSystem(): This function should return an object containing defineHook and runHook (or similar) methods.
  2. defineHook(name, hookFunction):
    • Registers a named hook.
    • hookFunction will be the JavaScript function defining the hook's logic.
    • hookFunction should have access to the system's internal state management functions (like useState, useEffect).
  3. runHook(hookName, ...args):
    • Executes a registered hook.
    • If the hook has already been run in the current "component" context, it should resume its state.
    • If it's the first time running, it should initialize its state.
    • Should return the value(s) returned by the hookFunction.
  4. useState(initialValue) (within hookFunction):
    • Provides a way for hooks to declare state.
    • Returns an array [state, setState].
    • setState should trigger a re-execution of the relevant hook's logic within the component context.
  5. useEffect(callback, dependencies) (within hookFunction):
    • Provides a way for hooks to declare side effects.
    • callback is the function to execute.
    • dependencies is an array of values. The callback should only re-run if any of these dependencies have changed since the last execution.
    • The callback can optionally return a cleanup function, which should be executed before the next effect runs or when the component unmounts (simulated).
  6. Simulated Component Context: You'll need a way to manage which hooks are associated with a particular "component" instance and in what order they were called. This is crucial for correctly re-initializing state on subsequent runs.

Expected Behavior:

  • When runHook is called for the first time for a specific hook within a simulated component, its hookFunction is executed, and its initial state and effects are set up.
  • When runHook is called again for the same hook within the same simulated component context, the hookFunction is executed again, but it should receive its previously stored state. useState calls within the hook should return the current state and a function to update it. useEffect dependencies should be checked for changes.
  • The order of runHook calls matters for state management. Hooks should be called in the same order on every render of a simulated component.
  • useEffect cleanup functions should be invoked correctly.

Edge Cases:

  • Calling runHook for a hook that hasn't been defined.
  • Conditional calling of hooks (e.g., if (condition) { myHook(); }) will break the system because the order of hook calls must be consistent. You should not need to handle this explicitly in the implementation, but users of your system should be aware.
  • Multiple calls to setState within a single execution of a hook.
  • useEffect with no dependencies (always runs).
  • useEffect with an empty dependency array (runs only once).

Examples

Let's simulate a component's lifecycle with a simple runComponent function that orchestrates hook calls.

Example 1: Basic useState

// Assume createHookSystem is available and has been called.
const { defineHook, runHook, runComponent } = createHookSystem();

// Define a simple counter hook
defineHook('useCounter', (initialValue = 0) => {
    const [count, setCount] = runHook('useState', initialValue); // Using a built-in hook
    return { count, increment: () => setCount(count + 1) };
});

// Simulate a component's "render" cycle
const componentState = {}; // Stores state for the simulated component

function runComponent() {
    // Reset hook execution context for this "render"
    const hookRunner = createHookRunner(componentState);
    const runHookInContext = hookRunner.runHook;

    // Run our custom hook
    const { count, increment } = runHookInContext('useCounter', 0);

    console.log(`Current count: ${count}`);
    return { count, increment };
}

// --- Simulation ---
console.log("--- First render ---");
let renderedState = runComponent(); // Current count: 0

console.log("--- Second render (after simulated increment) ---");
renderedState.increment(); // This call would trigger a re-run of useCounter's logic in a real system.
                           // In this simulation, we manually simulate the re-run by calling runComponent again.
renderedState = runComponent(); // Current count: 1

console.log("--- Third render ---");
renderedState = runComponent(); // Current count: 1

Output:

--- First render ---
Current count: 0
--- Second render (after simulated increment) ---
Current count: 1
--- Third render ---
Current count: 1

Explanation:

The useCounter hook uses useState to manage its count. The runComponent function simulates the rendering of a component. The first call to runComponent initializes useCounter with initialValue = 0. The second call, after increment is called (which internally calls setCount), re-executes useCounter with the updated state.

Example 2: useEffect with Dependencies

// Assume createHookSystem is available and has been called.
const { defineHook, runHook, runComponent } = createHookSystem();

let logCalls = []; // To track effect executions

defineHook('useLogger', (message, dependencies) => {
    runHook('useEffect', () => {
        logCalls.push(`Effect ran for: ${message}`);
        return () => {
            logCalls.push(`Cleanup for: ${message}`);
        };
    }, dependencies);
    return () => { /* Hook returns nothing for simplicity */ };
});

const componentState = {};

function runComponent(userId) {
    const hookRunner = createHookRunner(componentState);
    const runHookInContext = hookRunner.runHook;

    runHookInContext('useLogger', `User ${userId} logged in`, [userId]);
    console.log(`Component rendered with userId: ${userId}`);
}

console.log("--- First render (userId: 1) ---");
runComponent(1); // Effect ran for: User 1 logged in

console.log("--- Second render (userId: 1, no change) ---");
runComponent(1); // No effect should run

console.log("--- Third render (userId: 2, dependency change) ---");
runComponent(2); // Cleanup for: User 1 logged in, Effect ran for: User 2 logged in

console.log("--- Fourth render (userId: 2, no change) ---");
runComponent(2); // No effect should run

// Simulate component unmount after third render (not explicitly part of the system, but good to show cleanup)
console.log("--- Simulated Unmount ---");
// In a real scenario, this would happen when the component is removed.
// For this simulation, we'd need a mechanism to trigger cleanups.
// For this challenge, we'll assume cleanups are triggered by dependency changes or a hypothetical unmount.
// The manual execution of cleanup for user 1 in the third render demonstrates the concept.

Output:

--- First render (userId: 1) ---
Component rendered with userId: 1
--- Second render (userId: 1, no change) ---
Component rendered with userId: 1
--- Third render (userId: 2, dependency change) ---
Cleanup for: User 1 logged in
Effect ran for: User 2 logged in
Component rendered with userId: 2
--- Fourth render (userId: 2, no change) ---
Component rendered with userId: 2
--- Simulated Unmount ---

Explanation:

The useLogger hook uses useEffect.

  • On the first render with userId: 1, the effect runs.
  • On the second render with the same userId: 1, the dependency hasn't changed, so the effect does not run.
  • On the third render with userId: 2, the userId dependency has changed. The previous effect's cleanup function (for userId: 1) is executed, and then the new effect for userId: 2 runs.

Example 3: Chaining Hooks

// Assume createHookSystem is available and has been called.
const { defineHook, runHook, runComponent } = createHookSystem();

// A hook to fetch data (simulated)
defineHook('useFetch', (url) => {
    const [data, setData] = runHook('useState', null);
    const [loading, setLoading] = runHook('useState', true);
    const [error, setError] = runHook('useState', null);

    runHook('useEffect', async () => {
        setLoading(true);
        setError(null);
        try {
            // Simulate API call
            await new Promise(resolve => setTimeout(resolve, 100));
            const fetchedData = `Data from ${url}`;
            setData(fetchedData);
        } catch (err) {
            setError(err.message);
        } finally {
            setLoading(false);
        }
        // No cleanup needed for this simple fetch
    }, [url]); // Re-fetch if URL changes

    return { data, loading, error };
});

// A hook that uses another hook
defineHook('useUserData', (userId) => {
    const url = `/api/users/${userId}`;
    const { data: userData, loading: userLoading, error: userError } = runHook('useFetch', url);

    // We could do more processing or add another hook here
    return { userData, userLoading, userError };
});

const componentState = {};

function runComponent(userId) {
    const hookRunner = createHookRunner(componentState);
    const runHookInContext = hookRunner.runHook;

    const { userData, userLoading, userError } = runHookInContext('useUserData', userId);

    if (userLoading) {
        console.log(`Loading user ${userId}...`);
    } else if (userError) {
        console.log(`Error loading user ${userId}: ${userError}`);
    } else if (userData) {
        console.log(`User data for ${userId}: ${userData}`);
    }
}

console.log("--- First render (userId: 1) ---");
runComponent(1); // Loading user 1... then User data for 1: Data from /api/users/1

console.log("--- Second render (userId: 1, delayed) ---");
// Simulate a small delay to see loading state if fetch was asynchronous
setTimeout(() => {
    runComponent(1); // User data for 1: Data from /api/users/1
}, 200);

Output:

--- First render (userId: 1) ---
Loading user 1...
User data for 1: Data from /api/users/1
--- Second render (userId: 1, delayed) ---
User data for 1: Data from /api/users/1

Explanation:

useUserData calls useFetch. useFetch itself uses useState and useEffect. This demonstrates how custom hooks can be composed. The runHook system must correctly track and manage state across nested hook calls within the same component context.

Constraints

  • Your createHookSystem function should be self-contained and not rely on external libraries like React.
  • The hook system should handle up to 100 registered hooks.
  • Each simulated component context should be able to manage state for up to 50 hook calls per render cycle.
  • useEffect dependency arrays will contain up to 10 elements.
  • Performance is not the primary concern for this challenge, but the implementation should be reasonably efficient and avoid excessive complexity.

Notes

  • The core challenge is managing the "fiber" or execution context for hooks. Think about how React determines which state belongs to which hook call. The order of hook calls within a component's render is crucial.
  • You'll need to design a way to store the state for each hook per simulated component instance. A map or object keyed by hook name and then perhaps by an index within that component's execution order might be a good starting point.
  • Consider how useState's setState will trigger a re-evaluation. In this simulation, you'll have to manually trigger re-renders (e.g., by calling runComponent again). In a real system, this would be an internal mechanism.
  • The createHookRunner function in the examples is a placeholder for how you might manage the context for a single "render" of a simulated component. You'll need to implement this or a similar mechanism within createHookSystem.
  • You'll likely need to implement a basic useState and useEffect hook within your hook system itself, which your custom hooks can then call using runHook.
Loading editor...
javascript