Hone logo
Hone
Problems

Implement useHash Hook for React Router Navigation

This challenge focuses on creating a custom React hook, useHash, that abstracts the logic for reading and writing to the URL's hash fragment. This hook will be particularly useful for applications using client-side routing, allowing for state management or deep linking via the hash, independent of traditional query parameters or route paths.

Problem Description

Your task is to implement a custom React hook named useHash in TypeScript. This hook should provide a clean interface for interacting with the browser's hash fragment (window.location.hash).

Key Requirements:

  1. Read Hash: The hook should return the current hash value (excluding the leading #). If there is no hash, it should return an empty string.
  2. Update Hash: The hook should return a function that allows updating the hash value. This function should correctly set window.location.hash and trigger a re-render of components using the hook when the hash changes.
  3. Synchronization: The hook must ensure that changes to the hash from outside the hook (e.g., user directly typing in the URL, another script manipulating window.location.hash) also update the hook's state and trigger re-renders.
  4. Type Safety: The hook should be implemented in TypeScript, ensuring type safety for the hash value and the update function.

Expected Behavior:

  • When a component mounts that uses useHash, it should receive the current hash value.
  • When the returned update function is called with a new hash string, window.location.hash should be updated, and the hook should re-render, returning the new hash.
  • When the browser's hash changes for any reason, the hook should detect this change and update its internal state, causing components using it to re-render with the new hash value.
  • The leading # symbol should not be part of the returned hash string, but should be automatically prepended when setting the hash.

Edge Cases:

  • Initial State: The hook should correctly handle cases where the URL initially has no hash.
  • Empty Hash: Setting the hash to an empty string should result in window.location.hash being set to "" (or effectively removing the hash).
  • Browser Events: Ensure the hook correctly listens for and reacts to the hashchange event.

Examples

Example 1:

// Component using the hook
function HashDisplay() {
  const [hash, setHash] = useHash();

  return (
    <div>
      <p>Current Hash: {hash}</p>
      <button onClick={() => setHash('section-a')}>Go to Section A</button>
      <button onClick={() => setHash('')}>Clear Hash</button>
    </div>
  );
}

// Assume the initial URL is http://example.com/page
// After rendering HashDisplay, the output will be:
// Current Hash:
// [Buttons: Go to Section A, Clear Hash]

// User clicks "Go to Section A"
// The URL becomes: http://example.com/page#section-a
// The HashDisplay component will re-render and output:
// Current Hash: section-a
// [Buttons: Go to Section A, Clear Hash]

Example 2:

// Component using the hook
function AnotherHashHandler() {
  const [hash, setHash] = useHash();

  // Simulate an external hash change
  useEffect(() => {
    const timer = setTimeout(() => {
      window.location.hash = 'external-update';
    }, 1000);
    return () => clearTimeout(timer);
  }, []);

  return (
    <div>
      <p>Hash detected: {hash}</p>
      <button onClick={() => setHash('custom-state')}>Set Custom State</button>
    </div>
  );
}

// Assume the initial URL is http://example.com/app
// Initial render:
// Hash detected:
// [Button: Set Custom State]

// After 1 second, the hash changes externally to '#external-update'
// The Hash detected paragraph will update to:
// Hash detected: external-update
// [Button: Set Custom State]

// If the user clicks "Set Custom State" afterwards, it might become:
// Hash detected: custom-state

Example 3: (Edge case with initial hash)

// Assume the initial URL is http://example.com/settings#profile
// Component using the hook
function ProfileSettings() {
  const [hash, setHash] = useHash();

  return (
    <div>
      <p>Active Tab: {hash || 'general'}</p>
      <button onClick={() => setHash('profile')}>Profile</button>
      <button onClick={() => setHash('account')}>Account</button>
    </div>
  );
}

// After rendering ProfileSettings, the output will be:
// Active Tab: profile
// [Buttons: Profile, Account]

Constraints

  • The hook must be implemented using React's useState, useEffect, and potentially useRef hooks.
  • The hook should handle string values for the hash.
  • The hashchange event listener should be properly added and removed to prevent memory leaks.
  • The implementation should be efficient and avoid unnecessary re-renders.
  • The code must be valid TypeScript.

Notes

  • Consider how you will handle the initial synchronization of the hash value when the hook first mounts.
  • Think about how to manage the event listener to ensure it's only active when the component using the hook is mounted.
  • The window.location.hash property returns the hash including the #, but it's conventional to work with the hash without the # in your application logic. Your hook should abstract this.
  • You might find useRef helpful for storing the handler function to ensure the useEffect closure always has access to the latest setter function from useState.
Loading editor...
typescript