Open Hooks
Device and Browser

useLocation

Track and manage browser location, URL parameters, and navigation state in React applications.

useLocation

Track and manage browser location, URL parameters, and navigation state in React applications.

Status: stable
Version: 2.0.0
Bundle Size: ~1.2kb
Requires: React 16.8+
Pathname
Search(empty)
Hash(empty)
Full URL

Installation

npx open-hook add useLocation
yarn dlx open-hook add useLocation
pnpm dlx open-hook add useLocation

API Reference

Parameters

This hook takes no parameters.

Returns

PropTypeDefault
setHash?
(hash: string) => void
-
removeSearchParam?
(key: string) => void
-
setSearchParam?
(key: string, value: string) => void
-
reload?
(forceReload?: boolean) => void
-
forward?
() => void
-
back?
() => void
-
navigate?
(url: string, replace?: boolean) => void
-
hash?
string
-
searchParams?
URLSearchParams
-
location?
LocationState
-

TypeScript Signature

interface LocationState {
  pathname: string;
  search: string;
  hash: string;
  host: string;
  hostname: string;
  origin: string;
  port: string;
  protocol: string;
  href: string;
}

interface UseLocationReturn {
  location: LocationState;
  searchParams: URLSearchParams;
  hash: string;
  navigate: (url: string, replace?: boolean) => void;
  back: () => void;
  forward: () => void;
  reload: (forceReload?: boolean) => void;
  setSearchParam: (key: string, value: string) => void;
  removeSearchParam: (key: string) => void;
  setHash: (hash: string) => void;
}

function useLocation(): UseLocationReturn;

Advanced Usage Examples

function ProductPage() {
  const { searchParams, setSearchParam, removeSearchParam } = useLocation();

  const category = searchParams.get("category") || "all";
  const sortBy = searchParams.get("sortBy") || "name";
  const page = parseInt(searchParams.get("page") || "1");

  const updateFilter = (filter: string, value: string) => {
    if (value === "all" || !value) {
      removeSearchParam(filter);
    } else {
      setSearchParam(filter, value);
    }
    // Reset to page 1 when filtering
    if (filter !== "page") {
      setSearchParam("page", "1");
    }
  };

  return (
    <div>
      <h1>Products</h1>

      <select
        value={category}
        onChange={(e) => updateFilter("category", e.target.value)}
      >
        <option value="all">All Categories</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
      </select>

      <select
        value={sortBy}
        onChange={(e) => updateFilter("sortBy", e.target.value)}
      >
        <option value="name">Sort by Name</option>
        <option value="price">Sort by Price</option>
        <option value="rating">Sort by Rating</option>
      </select>

      {/* Pagination */}
      <div>
        <button
          onClick={() => updateFilter("page", (page - 1).toString())}
          disabled={page <= 1}
        >
          Previous
        </button>
        <span>Page {page}</span>
        <button onClick={() => updateFilter("page", (page + 1).toString())}>
          Next
        </button>
      </div>
    </div>
  );
}
function TabContainer() {
  const { searchParams, setSearchParam, hash, setHash } = useLocation();

  const activeTab = searchParams.get("tab") || "overview";
  const activeSection = hash.replace("#", "") || "top";

  const tabs = [
    { id: "overview", label: "Overview" },
    { id: "details", label: "Details" },
    { id: "reviews", label: "Reviews" },
    { id: "related", label: "Related" },
  ];

  const sections = {
    overview: ["intro", "features", "pricing"],
    details: ["specifications", "requirements", "installation"],
    reviews: ["recent", "top-rated", "all"],
    related: ["similar", "alternatives", "bundles"],
  };

  const switchTab = (tabId: string) => {
    setSearchParam("tab", tabId);
    setHash(sections[tabId][0]); // Jump to first section
  };

  const jumpToSection = (sectionId: string) => {
    setHash(sectionId);
  };

  return (
    <div>
      {/* Tab Navigation */}
      <nav>
        {tabs.map((tab) => (
          <button
            key={tab.id}
            onClick={() => switchTab(tab.id)}
            className={activeTab === tab.id ? "active" : ""}
          >
            {tab.label}
          </button>
        ))}
      </nav>

      {/* Section Navigation */}
      <aside>
        <h3>Jump to:</h3>
        {sections[activeTab]?.map((section) => (
          <button
            key={section}
            onClick={() => jumpToSection(section)}
            className={activeSection === section ? "active" : ""}
          >
            {section}
          </button>
        ))}
      </aside>

      {/* Content based on active tab and section */}
      <main>
        <TabContent activeTab={activeTab} activeSection={activeSection} />
      </main>
    </div>
  );
}
function SearchPage() {
  const { searchParams, setSearchParam, removeSearchParam, navigate } =
    useLocation();

  const query = searchParams.get("q") || "";
  const type = searchParams.get("type") || "all";
  const dateRange = searchParams.get("date") || "all";
  const sortBy = searchParams.get("sort") || "relevance";

  const [searchInput, setSearchInput] = useState(query);

  const updateSearch = (newQuery: string) => {
    if (newQuery.trim()) {
      setSearchParam("q", newQuery.trim());
    } else {
      removeSearchParam("q");
    }
  };

  const clearAllFilters = () => {
    navigate(window.location.pathname); // Remove all query params
  };

  const buildShareableLink = () => {
    const currentUrl = window.location.href;
    navigator.clipboard.writeText(currentUrl);
  };

  const hasActiveFilters = Array.from(searchParams.entries()).length > 0;

  return (
    <div>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          updateSearch(searchInput);
        }}
      >
        <input
          value={searchInput}
          onChange={(e) => setSearchInput(e.target.value)}
          placeholder="Search..."
        />
        <button type="submit">Search</button>
      </form>

      <div className="filters">
        <select
          value={type}
          onChange={(e) => setSearchParam("type", e.target.value)}
        >
          <option value="all">All Types</option>
          <option value="posts">Posts</option>
          <option value="users">Users</option>
          <option value="comments">Comments</option>
        </select>

        <select
          value={dateRange}
          onChange={(e) => setSearchParam("date", e.target.value)}
        >
          <option value="all">All Time</option>
          <option value="day">Last 24 hours</option>
          <option value="week">Last week</option>
          <option value="month">Last month</option>
        </select>

        <select
          value={sortBy}
          onChange={(e) => setSearchParam("sort", e.target.value)}
        >
          <option value="relevance">Relevance</option>
          <option value="date">Date</option>
          <option value="popularity">Popularity</option>
        </select>

        {hasActiveFilters && (
          <button onClick={clearAllFilters}>Clear Filters</button>
        )}

        <button onClick={buildShareableLink}>Share Search</button>
      </div>

      <SearchResults
        query={query}
        type={type}
        dateRange={dateRange}
        sortBy={sortBy}
      />
    </div>
  );
}
// Custom router hook using useLocation
function useRouter() {
  const { location, navigate, back, forward, searchParams } = useLocation();

  const push = useCallback(
    (path: string) => {
      navigate(path);
    },
    [navigate]
  );

  const replace = useCallback(
    (path: string) => {
      navigate(path, true);
    },
    [navigate]
  );

  const query = useMemo(() => {
    const params: Record<string, string> = {};
    searchParams.forEach((value, key) => {
      params[key] = value;
    });
    return params;
  }, [searchParams]);

  return {
    pathname: location.pathname,
    search: location.search,
    hash: location.hash,
    query,
    push,
    replace,
    back,
    forward,
  };
}

// Route component
function Route({
  path,
  component: Component,
}: {
  path: string;
  component: React.ComponentType<any>;
}) {
  const { pathname } = useRouter();

  // Simple path matching (you'd want more sophisticated matching in real apps)
  const isMatch = pathname === path || pathname.startsWith(path + "/");

  return isMatch ? <Component /> : null;
}

// App with routing
function App() {
  const router = useRouter();

  return (
    <div>
      <nav>
        <button onClick={() => router.push("/")}>Home</button>
        <button onClick={() => router.push("/about")}>About</button>
        <button onClick={() => router.push("/contact")}>Contact</button>
      </nav>

      <main>
        <Route path="/" component={HomePage} />
        <Route path="/about" component={AboutPage} />
        <Route path="/contact" component={ContactPage} />
      </main>

      <footer>
        <button onClick={router.back}>← Back</button>
        <button onClick={router.forward}>Forward →</button>
        <span>Current: {router.pathname}</span>
      </footer>
    </div>
  );
}

Best Practices

Handle URL changes gracefully and maintain URL state consistency:

const { searchParams, setSearchParam, location } = useLocation();

// Always validate URL parameters
const page = Math.max(1, parseInt(searchParams.get("page") || "1"));
const validSortOptions = ["name", "date", "price"];
const sortBy = validSortOptions.includes(searchParams.get("sort"))
  ? searchParams.get("sort")
  : "name";

// Batch URL updates to avoid multiple history entries
const updateFilters = (newFilters: Record<string, string>) => {
  const newSearchParams = new URLSearchParams(location.search);

  Object.entries(newFilters).forEach(([key, value]) => {
    if (value) {
      newSearchParams.set(key, value);
    } else {
      newSearchParams.delete(key);
    }
  });

  const newUrl = `${location.pathname}?${newSearchParams.toString()}`;
  navigate(newUrl, true); // Replace current entry
};

URL Management Guidelines

  • 🔗 Shareable URLs: Keep important state in URL parameters
  • 📱 Deep Linking: Support direct navigation to specific app states
  • 🔄 History Management: Use replace for filter updates, push for navigation
  • ✅ Validation: Always validate URL parameters before using them

Do's and Don'ts

  • ✅ Use URL parameters for shareable application state
  • ✅ Validate URL parameters and provide fallbacks
  • ✅ Batch URL updates to avoid excessive history entries
  • ✅ Provide clear navigation feedback
  • ❌ Don't store sensitive data in URL parameters
  • ❌ Avoid creating too many history entries with rapid updates
  • ❌ Don't rely on URL state for temporary UI state
  • ❌ Avoid breaking the back button behavior

Performance Metrics

MetricValueDescription
Bundle Size~1.2kbMinified + gzipped
Update Speed< 1msURL state synchronization
Memory UsageLowMinimal state tracking
Browser Events3 listenerspopstate, hashchange, history

Browser Compatibility

BrowserSupportedNotes
Chrome 4+YesFull history API support
Firefox 4+YesFull history API support
Safari 5+YesFull history API support
Edge 12+YesFull history API support
IE 10+YesLimited history API support
Mobile browsersYesModern mobile browsers

Use Cases

URL State Management

Keep application state synchronized with URL for shareability and bookmarking.

Custom Routing

Build lightweight routing solutions without external router libraries.

Deep Linking

Enable direct navigation to specific application states via URLs.

Navigation Control

Programmatically control browser navigation and history.

Accessibility

Accessibility Considerations

  • Announce route changes to screen readers - Ensure focus management during navigation - Provide skip links for complex navigation - Use semantic HTML for navigation elements - Maintain logical tab order during route changes

Troubleshooting

Internals

How It Works

The hook maintains synchronized state with the browser's location object by listening to popstate, hashchange, and custom history events. It provides a reactive interface to the History API while maintaining compatibility with browser navigation controls.

The implementation intercepts pushState and replaceState calls to ensure all location changes trigger React re-renders.

Testing Example

import { renderHook, act } from "@testing-library/react";
import { useLocation } from "./useLocation";

// Mock window.location and history
const mockLocation = {
  pathname: "/test",
  search: "?foo=bar",
  hash: "#section",
  host: "example.com",
  hostname: "example.com",
  origin: "https://example.com",
  port: "",
  protocol: "https:",
  href: "https://example.com/test?foo=bar#section",
};

Object.defineProperty(window, "location", {
  value: mockLocation,
  writable: true,
});

const mockHistory = {
  pushState: jest.fn(),
  replaceState: jest.fn(),
  back: jest.fn(),
  forward: jest.fn(),
};

Object.defineProperty(window, "history", {
  value: mockHistory,
  writable: true,
});

describe("useLocation", () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it("should return current location state", () => {
    const { result } = renderHook(() => useLocation());

    expect(result.current.location.pathname).toBe("/test");
    expect(result.current.location.search).toBe("?foo=bar");
    expect(result.current.location.hash).toBe("#section");
    expect(result.current.searchParams.get("foo")).toBe("bar");
  });

  it("should navigate to new URL", () => {
    const { result } = renderHook(() => useLocation());

    act(() => {
      result.current.navigate("/new-path");
    });

    expect(mockHistory.pushState).toHaveBeenCalledWith(null, "", "/new-path");
  });

  it("should replace current URL", () => {
    const { result } = renderHook(() => useLocation());

    act(() => {
      result.current.navigate("/new-path", true);
    });

    expect(mockHistory.replaceState).toHaveBeenCalledWith(
      null,
      "",
      "/new-path"
    );
  });

  it("should set search parameters", () => {
    const { result } = renderHook(() => useLocation());

    act(() => {
      result.current.setSearchParam("test", "value");
    });

    expect(mockHistory.replaceState).toHaveBeenCalledWith(
      null,
      "",
      expect.stringContaining("test=value")
    );
  });

  it("should remove search parameters", () => {
    const { result } = renderHook(() => useLocation());

    act(() => {
      result.current.removeSearchParam("foo");
    });

    expect(mockHistory.replaceState).toHaveBeenCalledWith(
      null,
      "",
      expect.not.stringContaining("foo=bar")
    );
  });

  it("should handle browser navigation", () => {
    const { result } = renderHook(() => useLocation());

    act(() => {
      result.current.back();
    });

    expect(mockHistory.back).toHaveBeenCalled();

    act(() => {
      result.current.forward();
    });

    expect(mockHistory.forward).toHaveBeenCalled();
  });
});

FAQ

Changelog

  • 2.0.0 — Enhanced TypeScript support, improved search parameter handling, better browser compatibility, comprehensive navigation methods
  • 1.3.0 — Added hash management and search parameter utilities
  • 1.2.0 — Improved history API integration and event handling
  • 1.1.0 — Added navigation methods (back, forward, reload)
  • 1.0.0 — Initial release with basic location tracking