Hone logo
Hone
Problems

Implement a Custom useHistory Hook in React with TypeScript

In React applications, managing navigation and history is crucial for providing a seamless user experience. The built-in useHistory hook (historically from react-router-dom) allows components to access the browser's history object, enabling programmatic navigation. This challenge asks you to recreate a simplified version of this hook in TypeScript, understanding its core functionalities and implementation details.

Problem Description

Your task is to implement a custom React hook named useHistory that mimics the essential behavior of the useHistory hook found in libraries like react-router-dom. This hook should provide access to a history object that allows components to push new entries onto the browser's history stack, replace the current entry, and go back or forward in history.

Key Requirements:

  1. push(path: string): A function to navigate to a new path. This should add a new entry to the browser's history.
  2. replace(path: string): A function to navigate to a new path, replacing the current history entry. This means the user cannot go back to the previous page using the browser's back button.
  3. goBack(): A function to navigate to the previous entry in the history.
  4. goForward(): A function to navigate to the next entry in the history.
  5. location: A reactive object representing the current location, including at least a pathname property.
  6. listen(callback: (location: Location) => void): A function to register a callback that will be executed whenever the location changes. This listener should also be returned by the hook so it can be unmounted.

Expected Behavior:

  • The hook should be usable within any functional React component.
  • Changes triggered by push, replace, goBack, or goForward should update the location state and trigger registered listeners.
  • The hook should provide a way to clean up listeners when a component unmounts to prevent memory leaks.

Edge Cases to Consider:

  • What happens when goBack() is called on the first history entry?
  • What happens when goForward() is called on the last history entry?
  • How should the hook handle initial mounting and potential server-side rendering scenarios (though full SSR is out of scope for this simplified version)?

Examples

Example 1: Basic Navigation

Let's assume you have a simple App component that uses your useHistory hook.

// Assume this is your App component
function App() {
  const { location, push } = useHistory();

  return (
    <div>
      <h1>Current Path: {location.pathname}</h1>
      <button onClick={() => push('/about')}>Go to About</button>
      <button onClick={() => push('/contact')}>Go to Contact</button>
    </div>
  );
}

// In a separate component (e.g., AboutPage)
function AboutPage() {
  const { location, goBack } = useHistory();

  return (
    <div>
      <h1>About Page</h1>
      <p>Current Path: {location.pathname}</p>
      <button onClick={goBack}>Go Back</button>
    </div>
  );
}

Input: The user clicks the "Go to About" button. Output:

  • The App component's location.pathname updates from (e.g.) / to /about.
  • The AboutPage component re-renders, displaying "Current Path: /about".

Explanation: The push('/about') call adds /about to the history and updates the current location.

Example 2: Replacing History

function LoginSuccessComponent() {
  const { replace } = useHistory();

  // After a successful login, redirect to the dashboard
  useEffect(() => {
    replace('/dashboard');
  }, []); // Run once on mount

  return <p>Logging you in...</p>;
}

Input: The LoginSuccessComponent mounts. Output: The browser's history is updated, and the current location becomes /dashboard. The user cannot click the back button to return to the component that initiated the login.

Explanation: replace('/dashboard') navigates to /dashboard but removes the entry for the previous page from history.

Example 3: Listening for Location Changes

function NavigationLogger() {
  const { location, listen } = useHistory();

  useEffect(() => {
    const unlisten = listen((newLocation) => {
      console.log('Navigation occurred:', newLocation.pathname);
    });

    // Cleanup the listener on component unmount
    return () => {
      unlisten();
    };
  }, [listen]); // listen is stable, but good practice

  return <p>Logging navigation events...</p>;
}

Input: The user navigates from /page1 to /page2. Output: The console.log('Navigation occurred: /page2') message appears in the browser console.

Explanation: The listen callback is executed whenever the location object changes, allowing external logic to react to navigation.

Constraints

  • Your implementation must be in TypeScript.
  • The hook should rely on the browser's native window.history API.
  • The location object exposed by the hook should be reactive and trigger component re-renders when it changes.
  • The listen function should return an unlisten function that effectively removes the registered callback.
  • The hook should be efficient and avoid unnecessary re-renders.
  • Assume the initial route is correctly set in the browser when your application starts.

Notes

  • Consider how to make the location object reactive. React's useState hook is a good starting point.
  • The browser's window.history API has methods like pushState, replaceState, back, and forward. You'll also need to listen for popstate events.
  • Think about how to manage multiple listeners for the listen function. An array or a Set could be useful.
  • When implementing pushState and replaceState, remember to provide a title and url argument, even if you don't use them extensively in this challenge. For simplicity, you can use an empty string or null for the title.
  • The useHistory hook should be a singleton pattern in the sense that all calls to useHistory within your app should refer to the same history instance. This can be achieved by using a React Context or by managing the history instance outside the hook itself. For this challenge, you can manage a single history instance internally.
Loading editor...
typescript