Repository: kyeotic/raviger (ts)

Issue:
useNavigate does not allow me to set a State
If I use the `navigate()` function from https://kyeotic.github.io/raviger/navigate/, I can pass `state` as the 4th parameter and set some state. However if I call `useNavigate()` from https://kyeotic.github.io/raviger/use-navigate/, the function it returns does not allow `state` as the 4th parameter, but the docs say:

> The function returned by useNavigate has the same signature as the non-hook navigate function. 

Are the docs wrong or is the implementation incorrect?

Additionally, in the types that are generated for the project, the `navigate()` function has the following definitions:

```
export declare function navigate(url: string): void;
export declare function navigate(url: string, replace: boolean): void;
export declare function navigate(url: string, query: QueryParam | URLSearchParams): void;
export declare function navigate(url: string, query: QueryParam | URLSearchParams, replace: boolean): void;
export declare function navigate(url: string, queryOrReplace?: QueryParam | URLSearchParams | boolean | null, replace?: boolean): void;
export declare function navigate(url: string, query: QueryParam | URLSearchParams, replace: boolean, state: unknown): void;
```

this means there is no valid way to pass a `null` for the `query` and `replace` parameters when you try to also pass a `state` parameter.
Could we have the location.search value returned next to path in useLocationChange?
I'm doing some magic with this side effect to handle a referral value in the URL

And I need to compare the `window.location.search` value at the moment of the side effect callback being called, so it would be nice if we could get it next to path, maybe something like

```
useLocationChange(location, { ... } )
// and then we can just deconstruct what we need

useLocationChange({ pathname, search }, {...})
```

This way we don't need to use window.location.search if we need it's value and risk closure issues or someone moving this value out of the side effect thinking they can reuse a cached `const`

What do you think?
Using navigate() can cause some strange issues
Sandbox: https://codesandbox.io/s/keen-mahavira-jfxvk

In this example, I'm trying to return the user to the main page if somehow they arrive into my App on a sub-page in an unexpected state (no state in window.history).

To do this, on my sub-page, I check for the absence of `window.history.state`, and if its missing, I navigate back to `/`:

```
const { state } = window.history;

  if (!state) {
    navigate("/");
    return null;
  }
```

In my App, I log which path we are currently on:

```
console.log(usePath());
```

When the App is first loaded, we see `/` as the path, and if we navigate to the error page, we correctly see `/error` as the path.

But if we now *reload* the App while we are on the `/error` path, we correctly see 1 log for `/error`, and then we see `navigating to /`, and now *incorrectly* the path appears to be `/error` again, but the App renders the `Main` component. And now the app is stuck and further navigation is impossible.


https://user-images.githubusercontent.com/1782643/152330809-1e58f016-d928-4a0d-90e8-10ec8b2ed4fd.mp4



useHistoryState
From [this comment](https://github.com/kyeotic/raviger/issues/113#issuecomment-1012333176)

`state` is accepted by `navigate` and set on history, but it is not accessible via the raviger API. We need a hook to expose it

```js
function useHistoryState() {
  const { state } = window.history;

  return state;
}
```

Pull request description:
V4
closes #113
closes #115
closes #116 
closes #117

This PR replaces #114

I would like to have a codemod for the change to `navigate`, but if I am being realistic I don't have the energy to put one together. I just started a new job and will be moving out of state in a few months, so my OSS time is extremely limited. If someone wants to write one I would be very grateful. 

**Changelog Preview**
---
## [4.0.0] - Unreleased
### Added
- `useHistory`
- `usePath`
### Changed
- **BREAKING**: `navigate` now has `options` object instead of overloads
- **BREAKING**: `useNavigate` uses updated `navigate` params
- **BREAKING**: `useLocationChange` now invokes the setter with a `RavigerLocation` object instead of the `path` string
### Fixed
-  `usePath` getting an old path if `navigate` was called during the initial render
---

~The docs have not yet been updated for any of this. **External Contributions Welcome**~
Edit: docs have been updated

I have released a beta for this code, it can be installed with

```
npm i raviger@beta
// OR
npm i raviger@4.0.0-2
```

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2ce2338..c968c3e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,14 +4,25 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [4.0.0] - Unreleased
+### Added
+- `useHistory`
+- `usePath`
+### Changed
+- **BREAKING**: `navigate` now has `options` object instead of overloads
+- **BREAKING**: `useNavigate` uses updated `navigate` params
+- **BREAKING**: `useLocationChange` now invokes the setter with a `RavigerLocation` object instead of the `path` string
+### Fixed
+-  `usePath` getting an old path if `navigate` was called during the initial render
+
 ## [3.1.0] - 2022-01-17
 ### Added
 - `__tag` to the PopState events dispatched by `navigate`
 
 ## [3.0.0] - 2021-11-02
 ### Changed
-- **BREAKING**:`usePath` returns `decodeURIComponent`-ed path
-- **BREAKING**:`useRoutes`, `useMatch`, and `usePathParams` match paths that have been `decodeURIComponent`-ed (e.g. `/weird (route)` will match a path of `/weird%20(route)`)
+- **BREAKING**: `usePath` returns `decodeURIComponent`-ed path
+- **BREAKING**: `useRoutes`, `useMatch`, and `usePathParams` match paths that have been `decodeURIComponent`-ed (e.g. `/weird (route)` will match a path of `/weird%20(route)`)
 - **BREAKING**: type `RouteParams` renamed to `Routes`
 - **BREAKING**: `usePathParams` return type now depends on the input params. When a string it returns just the props, when an array it returns `[path, props]`
 ### Added
diff --git a/docs/api/RouterProvider.md b/docs/api/RouterProvider.md
index ec294b0..ba3df42 100644
--- a/docs/api/RouterProvider.md
+++ b/docs/api/RouterProvider.md
@@ -1,7 +1,7 @@
 ---
 title: "RouterProvider"
 permalink: /router-provider/
-nav_order: 99
+nav_order: 59
 ---
 
 # STOP
diff --git a/docs/api/navigate.md b/docs/api/navigate.md
index 346e0e2..4e61f7d 100644
--- a/docs/api/navigate.md
+++ b/docs/api/navigate.md
@@ -11,12 +11,18 @@ This function causes programmatic navigation and causes all **raviger** hooks to
 ## API
 
 ```typescript
-export function navigate(url: string, replace?: boolean): void
 export function navigate(
   url: string,
-  query?: QueryParam | URLSearchParams,
-  replace?: boolean,
-  state?: unknown
+  options?: {
+    /**
+     * Use a `replace` instead of `push` for navigation
+     * @default false */
+    replace?: boolean
+    /** Values to serialize as a querystring, which will be appended to the `url` */
+    query?: QueryParam | URLSearchParams
+    /**  value to pass as the state/data to history push/replace*/
+    state?: unknown
+  }
 ): void
 ```
 
@@ -42,33 +48,33 @@ import { navigate } from 'raviger'
 
 export async function createUser () {
   let user = await createUser()
-  navigate(`/users/${user.id}`, true)
+  navigate(`/users/${user.id}`, { replace: true })
 }
 ```
 
 ## Navigating with Query Params
 
-To navigate with a serialized query string pass an object as the second parameter.
+To navigate with a serialized query string pass an object to the `query` option.
 
 ```jsx
 import { navigate } from 'raviger'
 
 export async function createUser () {
   let user = await createUser()
-  navigate(`/users/${user.id}` { ref: 'create page' })
+  navigate(`/users/${user.id}`, { query: { ref: 'create page' }})
 }
 ```
 
 ## Navigating with History State
 
-By default the `state` is `null`. You can control the `state` passed to `history.pushState | history.replaceState` using the fourth parameter
+By default the `state` is `null`. You can control the `state` passed to `history.pushState | history.replaceState` using the `state` option.
 
 ```jsx
 import { navigate } from 'raviger'
 
 export async function createUser () {
   let user = await createUser()
-  navigate(`/users/${user.id}`, undefined, undefined, { user: user })
+  navigate(`/users/${user.id}`, { state: { user: user } })
 }
 ```
 
diff --git a/docs/api/useLocationChange.md b/docs/api/useLocationChange.md
index 9f4522d..01fecf6 100644
--- a/docs/api/useLocationChange.md
+++ b/docs/api/useLocationChange.md
@@ -6,19 +6,40 @@ nav_order: 10
 
 # `useLocationChange`
 
-This hook invokes a setter whenever the page location is updated.
+This hook invokes a setter whenever the page location is updated. It uses a `window.location` like object instead of the actual `window.location`; the type is exported as `RavigerLocation`.
 
 ## API
 
 ```typescript
+export interface RavigerLocation {
+  /** The current path; alias of `pathname` */
+  path: string | null
+  /** The current path; alias of `path` */
+  pathname: string | null
+  /** The full path, ignores any `basePath` in the context */
+  fullPath: string
+  basePath?: string
+  search: string
+  hash: string
+  host: string
+  hostname: string
+  href: string
+  origin: string
+}
+
 export function useLocationChange(
-  setFn: (path: string) => any,
-  options?: {
-    inheritBasePath: boolean
-    basePath: string
-    isActive: () => boolean | boolean
-    onInitial?: boolean
-  }
+  setFn: (location: RavigerLocation) => void,
+  {
+    inheritBasePath = true,
+    basePath = '',
+    isActive,
+    onInitial = false,
+  }: {
+  inheritBasePath?: boolean
+  basePath?: string
+  isActive?: boolean | (() => boolean)
+  onInitial?: boolean
+} = {}
 ): void
 ```
 
@@ -28,7 +49,7 @@ By default this hook will not run on the initial mount for the component. You ca
 
 ## Basic
 
-The first parameter is a setter-function that is invoked with the new path whenever the url is changed. It does not automatically cause a re-render of the parent component (see _re-rendering_ below).
+The first parameter is a setter-function that is invoked with the new *RavigerLocation* whenever the url is changed. It does not automatically cause a re-render of the parent component (see _re-rendering_ below).
 
 ```jsx
 import { useLocationChange } from 'raviger'
@@ -43,7 +64,7 @@ function App () {
 }
 ```
 
-You should try to provide the same function (referential-equality) to `useLocationChange` whenever possible. If you are unable to create the function outside the component scope use [`useCallback`](https://reactjs.org/docs/hooks-reference.html#usecallback) to get a memoized function.
+You should try to provide the same function (referential-equality) to `useLocationChange` whenever possible. If you are unable to create the function outside the component scope use the [`useCallback`](https://reactjs.org/docs/hooks-reference.html#usecallback) to get a memoized function.
 
 ```jsx
 import { useCallback } from 'react'
@@ -51,7 +72,7 @@ import { useLocationChange } from 'raviger'
 import { pageChanged } from './monitoring'
 
 function App () {
-  const onChange = useCallback(path => pageChanged('App', path), [])
+  const onChange = useCallback(location => pageChanged('App', location.path), [])
   useLocationChange(onChange)
 
   return (
@@ -66,18 +87,36 @@ When `options.isActive` is both **defined** and **falsey** the `setFn` will not
 
 ## Re-rendering
 
-**useLocationChange** does not itself cause re-rendering. If you are trying to get the current path in your component that is better done with the [usePath](/api/link) hook, which returns the value. However, it is possible to trigger re-rendering by combining **useLocationChange** with **useState**.
+**useLocationChange** does not itself cause re-rendering. However, it is possible to trigger re-rendering by combining **useLocationChange** with **useState**.
 
 ```jsx
 import { useState } from 'react'
 import { useLocationChange } from 'raviger'
 
 function Route () {
-  const [path, setPath] = useState('/')
-  useLocationChange(setPath)
+  const [location, setLoc] = useState(null)
+  useLocationChange(setLoc, { onInitial: true })
 
   return (
     // ...
   )
 }
 ```
+
+## Previous Behavior
+
+Prior to v4 this hook called `setFn` with the path instead of a `RavigerLocation`. If you prefer the previous behavior you can use this wrapper
+
+
+```typescript
+export function usePathChange(
+  setFn: (path: string | null) => void,
+  options: LocationChangeOptionParams = {}
+): void {
+  useLocationChange(
+    useCallback((location: RavigerLocation) => setFn(location.path), [setFn]),
+    options
+  )
+}
+```
+
diff --git a/docs/api/useNavigationPrompt.md b/docs/api/useNavigationPrompt.md
index 1af93ae..12985c5 100644
--- a/docs/api/useNavigationPrompt.md
+++ b/docs/api/useNavigationPrompt.md
@@ -1,7 +1,7 @@
 ---
 title: "useNavigationPrompt"
 permalink: /use-navigation-prompt/
-date: 2019-10-26T11:32:24-07:00
+nav_order: 58
 ---
 
 # `useNavigationPrompt`
diff --git a/docs/guides/migration_to_v4.md b/docs/guides/migration_to_v4.md
new file mode 100644
index 0000000..527b6ce
--- /dev/null
+++ b/docs/guides/migration_to_v4.md
@@ -0,0 +1,59 @@
+---
+title: "Migration Guide - v4"
+permalink: /migraiton-to-v4/
+nav_order: 99
+---
+
+# v4 Migration Guide
+
+Raviger v4 comes with several high-impact breaking changes.
+
+- **BREAKING**: `navigate` now has `options` object instead of overloads
+- **BREAKING**: `useNavigate` uses updated `navigate` params
+- **BREAKING**: `useLocationChange` now invokes the setter with a `RavigerLocation` object instead of the `path`
+
+## navigate
+
+The changes to `navigate` create a more readable API but require manual changes to migrate to. This guide will provide some find/replace regexs, but they do not account for calls that might be broken onto multiple lines. You will need to inspect every call to `navigate` in your app to ensure it is changed correctly.
+
+Here are some sample migrations
+
+
+```js
+// Replace
+/*
+  find: navigate\((.+?)(, (true|false))\)
+  replace: navigate($1, { replace: $3 })
+*/
+navigate('/path', true) // -> navigate('/path', { replace: true })
+navigate('/path', false) // -> navigate('/path', { replace: false })
+
+// Query
+/*
+  find: navigate\((.+?),\s?(\{.+?\})\)
+  replace: navigate($1, { query: $2 })
+*/
+navigate('/path', { name: 'raviger' }) // -> navigate('/path', { query: { name: 'raviger' } })
+
+// State
+navigate('/path', undefined, undefined, { name: 'raviger'}) // -> navigate('/path', { state: { name: 'raviger' } })
+```
+
+The old `navigate` signature had complex combinations of overloads, so it is unlikely that simple substitutions will cover all the cases in even smaller applications. I'm sorry for the pain this change is going to cause, and doubly sorry because I already knew of the code smell of boolean arguments in functions with more than two parameters. I should have used an **options object** to begin with and I won't be making exceptions like this again.
+
+
+## useLocationChange
+
+Before v4 useLocationChange provided just the **path** to its `setFn`. It now provides a `window.location` like object that contains a `path` (and `pathname`, just to be consistent with `window.location`). While this new signature is strictly more useful, changes to your application can be avoided by adding a small utility method that emulates the previous behavior. You can replace all old usage of `useLocationChange` with this wrapper and be done.
+
+```typescript
+export function usePathChange(
+  setFn: (path: string | null) => void,
+  options: LocationChangeOptionParams = {}
+): void {
+  useLocationChange(
+    useCallback((location: RavigerLocation) => setFn(location.path), [setFn]),
+    options
+  )
+}
+```
\ No newline at end of file
diff --git a/example/package.json b/example/package.json
index 1fad804..7ce91d0 100644
--- a/example/package.json
+++ b/example/package.json
@@ -18,7 +18,7 @@
   "devDependencies": {
     "@types/react": "^17.0.16",
     "@types/react-dom": "^17.0.9",
-    "parcel": "^2.0.0",
+    "parcel": "^2.3.2",
     "typescript": "^4.3.5"
   }
 }
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 6be0507..d767110 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -1,15 +1,24 @@
-import * as React from 'react'
-import {} from 'react-dom'
+// eslint-disable-next-line import/no-extraneous-dependencies
+import React from 'react'
 
-import { useRoutes, Link, usePath, useQueryParams, Redirect } from '../../src/main'
+import {
+  useRoutes,
+  Link,
+  usePath,
+  useQueryParams,
+  Redirect,
+  navigate,
+  useHistory,
+} from '../../src/main'
 import Nav from './Nav'
 import Form from './Form'
 
 const routes = {
-  '/': () => <span>Home</span>,
+  '/': () => <Home />,
   '/about': () => <span>about</span>,
   '/contact': () => <span>contact</span>,
   '/form': () => <Form />,
+  '/error': () => <Error />,
   '/weird (route)': () => <span>Weird Route</span>,
   '/users/:userId': ({ userId }: { userId: string }) => <span>User: {userId}</span>,
   '/filter': () => <Filter />,
@@ -20,11 +29,11 @@ const routes = {
 }
 
 let renders = 0
-const App = () => {
+function App(): JSX.Element {
   renders++
   const route = useRoutes(routes)
   const path = usePath()
-  // console.log('rendered', renders)
+  console.log('rendered', renders, route?.props?.children?.type?.name)
   return (
     <div>
       <Nav>
@@ -56,6 +65,69 @@ const App = () => {
 
 export default App
 
+function Home(): JSX.Element {
+  console.log('rendering Home')
+
+  return (
+    <div>
+      <p>Home</p>
+      <button
+        onClick={() => {
+          console.log('replace clicked, so navigating to /error')
+          navigate('/error', {
+            replace: true,
+            state: {
+              historyState: 'yes',
+            },
+          })
+        }}
+      >
+        Go to Error (Replace)
+      </button>
+      <button
+        onClick={() => {
+          console.log('push clicked, so navigating to /error')
+          navigate('/error', {
+            replace: false,
+            state: {
+              historyState: 'yes',
+            },
+          })
+        }}
+      >
+        Go to Error (Push)
+      </button>
+    </div>
+  )
+}
+
+function Error(): JSX.Element | null {
+  console.log('rendering /error')
+
+  const { state } = useHistory()
+
+  if (!state) {
+    console.log('no history state, so navigating to /')
+    navigate('/')
+    return null
+  }
+
+  return (
+    <div>
+      <p>Error</p>
+      <p>{JSON.stringify(state)}</p>
+      <button
+        onClick={() => {
+          console.log('button clicked, so navigating to /')
+          navigate('/')
+        }}
+      >
+        Go to main
+      </button>
+    </div>
+  )
+}
+
 const DeepAbout = () => (
   <Nav>
     <Link href="/contact">Deep Contact</Link>
diff --git a/package-lock.json b/package-lock.json
index 8aa8309..f32d0a7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "raviger",
-  "version": "3.1.0",
+  "version": "4.0.0-3",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "raviger",
-      "version": "3.1.0",
+      "version": "4.0.0-3",
       "license": "MIT",
       "devDependencies": {
         "@kyeotic/eslint-config": "^2.0.1",
diff --git a/package.json b/package.json
index 049dc2b..cc9e4ca 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "raviger",
-  "version": "3.1.0",
+  "version": "4.0.0-3",
   "description": "React routing with hooks",
   "keywords": [
     "react",
@@ -61,7 +61,7 @@
   "size-limit": [
     {
       "path": "dist/main.js",
-      "limit": "2.8kb"
+      "limit": "2.9kb"
     },
     {
       "webpack": false,
@@ -94,7 +94,7 @@
     "mkdirp": "^1.0.4",
     "np": "^7.5.0",
     "npm-run-all": "^4.1.5",
-    "prettier": "^2.3.2",
+    "prettier": "^2.5.1",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
     "rimraf": "^3.0.2",
diff --git a/src/Link.tsx b/src/Link.tsx
index 16f5612..eac21f1 100644
--- a/src/Link.tsx
+++ b/src/Link.tsx
@@ -1,7 +1,7 @@
 import React, { useCallback, forwardRef, Ref } from 'react'
 
 import { navigate } from './navigate'
-import { useBasePath, useFullPath } from './path'
+import { useBasePath, useFullPath } from './location'
 
 export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
   href: string
diff --git a/src/context.tsx b/src/context.tsx
index b85e973..a4859ff 100644
--- a/src/context.tsx
+++ b/src/context.tsx
@@ -21,6 +21,8 @@ export function RouterProvider({
   children?: React.ReactNode
 }): JSX.Element {
   return (
+    // The ordering here is important, the basePath will change less often
+    // So putting it on the outside reduces its need to re-render
     <BasePathContext.Provider value={basePath}>
       <PathContext.Provider value={path ?? null}>{children}</PathContext.Provider>
     </BasePathContext.Provider>
diff --git a/src/path.ts b/src/location.ts
similarity index 65%
rename from src/path.ts
rename to src/location.ts
index 51980f1..42916ef 100644
--- a/src/path.ts
+++ b/src/location.ts
@@ -6,8 +6,29 @@ import { getSsrPath, isNode } from './node'
 import { shouldCancelNavigation } from './intercept'
 import { isFunction } from './typeChecks'
 
+export interface RavigerLocation {
+  /** The current path; alias of `pathname` */
+  path: string | null
+  /** The current path; alias of `path` */
+  pathname: string | null
+  /** The full path, ignores any `basePath` in the context */
+  fullPath: string
+  basePath?: string
+  search: string
+  hash: string
+  host: string
+  hostname: string
+  href: string
+  origin: string
+}
+
+export interface RavigerHistory {
+  scrollRestoration: 'auto' | 'manual'
+  state: unknown
+}
+
 export interface LocationChangeSetFn {
-  (path: string | null): void
+  (location: RavigerLocation): void
 }
 export interface LocationChangeOptionParams {
   inheritBasePath?: boolean
@@ -21,13 +42,23 @@ export function usePath(basePath?: string): string | null {
   const contextBasePath = useBasePath() // hooks can't be called conditionally
   basePath = basePath || contextBasePath
 
-  const [path, setPath] = useState(getFormattedPath(basePath))
-  useLocationChange((newPath) => setPath(newPath), {
+  // Don't bother tracking the actual path, it can get out of sync
+  // due to React parent/child render ordering, especially with onmount navigation
+  // See issues:
+  // https://github.com/kyeotic/raviger/issues/116
+  // https://github.com/kyeotic/raviger/issues/64
+  //
+  // This is just used to force a re-render
+  const [, setPath] = useState(getFormattedPath(basePath))
+  const onChange = useCallback(({ path: newPath }) => setPath(newPath), [])
+  useLocationChange(onChange, {
     basePath,
     inheritBasePath: !basePath,
+    // Use on initial to handle to force state updates from on-mount navigation
+    onInitial: true,
   })
 
-  return contextPath || path
+  return contextPath || getFormattedPath(basePath)
 }
 
 export function useBasePath(): string {
@@ -36,17 +67,21 @@ export function useBasePath(): string {
 
 export function useFullPath(): string {
   const [path, setPath] = useState<string | null>(getCurrentPath())
-  useLocationChange(setPath, { inheritBasePath: false })
+  const onChange = useCallback(({ path: newPath }) => setPath(newPath), [])
+  useLocationChange(onChange, { inheritBasePath: false })
 
   return path || '/'
 }
 
 export function useHash({ stripHash = true } = {}): string {
   const [hash, setHash] = useState(window.location.hash)
-  const handleHash = useCallback(() => {
-    if (window.location.hash === hash) return
-    setHash(window.location.hash)
-  }, [setHash, hash])
+  const handleHash = useCallback(
+    ({ hash: newHash }) => {
+      if (newHash === hash) return
+      setHash(newHash)
+    },
+    [setHash, hash]
+  )
 
   useLayoutEffect(() => {
     window.addEventListener('hashchange', handleHash, false)
@@ -101,7 +136,7 @@ export function useLocationChange(
     // No predicate defaults true
     if (isActive !== undefined && !isPredicateActive(isActive)) return
     if (shouldCancelNavigation()) return
-    setRef.current(getFormattedPath(basePath))
+    setRef.current(getFormattedLocation(basePath))
   }, [isActive, basePath])
 
   useLayoutEffect(() => {
@@ -114,13 +149,25 @@ export function useLocationChange(
   useMountedLayout(
     () => {
       if (isActive !== undefined && !isPredicateActive(isActive)) return
-      setRef.current(getFormattedPath(basePath))
+      setRef.current(getFormattedLocation(basePath))
     },
     [basePath, isActive],
     { onInitial }
   )
+}
 
-  /* eslint-enable react-hooks/rules-of-hooks */
+export function useHistory(): RavigerHistory {
+  const [history, setHistory] = useState(getRavigerHistory())
+  useLocationChange(useCallback(() => setHistory(getRavigerHistory()), [setHistory]))
+  return history
+}
+
+function getRavigerHistory(): RavigerHistory {
+  if (isNode) return { scrollRestoration: 'manual', state: null }
+  return {
+    scrollRestoration: window.history.scrollRestoration,
+    state: window.history.state,
+  }
 }
 
 /**
@@ -136,6 +183,22 @@ export function getFormattedPath(basePath: string): string | null {
   return decodeURIComponent(!basePath ? path : path.replace(basePathMatcher(basePath), '') || '/')
 }
 
+function getFormattedLocation(basePath: string): RavigerLocation {
+  const path = getFormattedPath(basePath)
+  return {
+    basePath,
+    path,
+    pathname: path,
+    fullPath: getCurrentPath(),
+    search: window.location.search,
+    hash: getCurrentHash(),
+    host: window.location.host,
+    hostname: window.location.hostname,
+    href: window.location.href,
+    origin: window.location.origin,
+  }
+}
+
 function isPredicateActive(predicate: boolean | (() => boolean)): boolean {
   return isFunction(predicate) ? predicate() : predicate
 }
diff --git a/src/main.ts b/src/main.ts
index 560a766..96848f0 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -9,10 +9,16 @@ export type { RedirectProps, UseRedirectProps } from './redirect'
 export { Link, ActiveLink } from './Link'
 
 export { navigate, useNavigate, useNavigationPrompt } from './navigate'
-export type { NavigateWithReplace, NavigateWithQuery } from './navigate'
 
-export { usePath, useHash, useFullPath, useBasePath, useLocationChange } from './path'
-export type { LocationChangeSetFn, LocationChangeOptionParams } from './path'
+export {
+  usePath,
+  useHash,
+  useFullPath,
+  useBasePath,
+  useLocationChange,
+  useHistory,
+} from './location'
+export type { LocationChangeSetFn, LocationChangeOptionParams } from './location'
 
 export { useQueryParams } from './querystring'
 export type { QueryParam, setQueryParamsOptions } from './querystring'
diff --git a/src/navigate.ts b/src/navigate.ts
index 2c48457..6d1d61a 100644
--- a/src/navigate.ts
+++ b/src/navigate.ts
@@ -1,6 +1,6 @@
 import { useCallback, useLayoutEffect } from 'react'
 
-import { useBasePath } from './path'
+import { useBasePath } from './location'
 import { isNode } from './node'
 import type { QueryParam } from './querystring'
 import {
@@ -11,51 +11,31 @@ import {
   undoNavigation,
 } from './intercept'
 
-export interface NavigateWithReplace {
-  (url: string, replace?: boolean): void
-}
-export interface NavigateWithQuery {
-  (url: string, query?: QueryParam | URLSearchParams, replace?: boolean): void
+export interface NavigateOptions {
+  /**
+   * Use a `replace` instead of `push` for navigation
+   * @default false */
+  replace?: boolean
+  /** Values to serialize as a querystring, which will be appended to the `url` */
+  query?: QueryParam | URLSearchParams
+  /**  value to pass as the state/data to history push/replace*/
+  state?: unknown
 }
 
 let lastPath = ''
 
-export function navigate(url: string): void
-export function navigate(url: string, replace: boolean): void
-export function navigate(url: string, query: QueryParam | URLSearchParams): void
-export function navigate(url: string, query: QueryParam | URLSearchParams, replace: boolean): void
-export function navigate(
-  url: string,
-  queryOrReplace?: QueryParam | URLSearchParams | boolean | null,
-  replace?: boolean
-): void
-export function navigate(
-  url: string,
-  query: QueryParam | URLSearchParams,
-  replace: boolean,
-  state: unknown
-): void
-export function navigate(
-  url: string,
-  replaceOrQuery?: QueryParam | URLSearchParams | boolean | null,
-  replace?: boolean,
-  state: unknown = null
-): void {
+export function navigate(url: string, options?: NavigateOptions): void {
   if (typeof url !== 'string') {
     throw new Error(`"url" must be a string, was provided a(n) ${typeof url}`)
   }
 
-  if (Array.isArray(replaceOrQuery)) {
-    throw new Error('"replaceOrQuery" must be boolean, object, or URLSearchParams')
+  if (Array.isArray(options?.query)) {
+    throw new Error('"query" a serializable object or URLSearchParams')
   }
 
   if (shouldCancelNavigation()) return
-  if (replaceOrQuery !== null && typeof replaceOrQuery === 'object') {
-    url += '?' + new URLSearchParams(replaceOrQuery).toString()
-  } else if (replace === undefined && replaceOrQuery !== undefined) {
-    replace = replaceOrQuery ?? undefined
-  } else if (replace === undefined && replaceOrQuery === undefined) {
-    replace = false
+  if (options?.query) {
+    url += '?' + new URLSearchParams(options.query).toString()
   }
 
   lastPath = url
@@ -67,8 +47,8 @@ export function navigate(
     return
   }
 
-  if (replace) window.history.replaceState(state, '', url)
-  else window.history.pushState(state, '', url)
+  if (options?.replace) window.history.replaceState(options?.state, '', url)
+  else window.history.pushState(options?.state, '', url)
 
   const event = new PopStateEvent('popstate')
   // Tag the event so navigation can be filtered out from browser events
@@ -111,13 +91,13 @@ function cancelNavigation(event: BeforeUnloadEvent, prompt: string) {
   return prompt
 }
 
-export function useNavigate(optBasePath = ''): NavigateWithReplace & NavigateWithQuery {
+export function useNavigate(optBasePath = ''): typeof navigate {
   const basePath = useBasePath()
-  const navigateWithBasePath = useCallback<NavigateWithReplace & NavigateWithQuery>(
-    (url: string, replaceOrQuery?: boolean | QueryParam | URLSearchParams, replace?: boolean) => {
+  const navigateWithBasePath = useCallback<typeof navigate>(
+    (url: string, options?: NavigateOptions) => {
       const base = optBasePath || basePath
       const href = url.startsWith('/') ? base + url : url
-      navigate(href, replaceOrQuery, replace)
+      navigate(href, options)
     },
     [basePath, optBasePath]
   )
diff --git a/src/querystring.ts b/src/querystring.ts
index 1d62e10..d782b8b 100644
--- a/src/querystring.ts
+++ b/src/querystring.ts
@@ -2,7 +2,7 @@ import { useState, useCallback } from 'react'
 
 import { navigate } from './navigate'
 import { isNode, getSsrPath } from './node'
-import { getCurrentPath, getCurrentHash, useLocationChange } from './path'
+import { getCurrentPath, getCurrentHash, useLocationChange } from './location'
 
 export interface QueryParam {
   [key: string]: any
diff --git a/src/redirect.ts b/src/redirect.ts
index 6f0f687..5bd09f8 100644
--- a/src/redirect.ts
+++ b/src/redirect.ts
@@ -1,6 +1,6 @@
 import { useLayoutEffect } from 'react'
 
-import { getCurrentHash, usePath } from './path'
+import { getCurrentHash, usePath } from './location'
 import { navigate } from './navigate'
 import { QueryParam, useQueryParams } from './querystring'
 
@@ -55,7 +55,7 @@ export function useRedirect(
 
   useLayoutEffect(() => {
     if (currentPath === predicateUrl) {
-      navigate(url, undefined, replace)
+      navigate(url, { replace })
     }
   }, [predicateUrl, url, replace, currentPath])
 }
diff --git a/src/router.tsx b/src/router.tsx
index 73245e2..1e2e5e5 100644
--- a/src/router.tsx
+++ b/src/router.tsx
@@ -1,8 +1,8 @@
-import React, { useMemo, useState, useLayoutEffect } from 'react'
+import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react'
 
 import { RouterProvider } from './context'
 import { isNode, setSsrPath, getSsrPath } from './node'
-import { getFormattedPath, usePath } from './path'
+import { getFormattedPath, usePath } from './location'
 import type { NonEmptyRecord, Split, ValueOf } from './types'
 
 const emptyPathResult: [null, null] = [null, null]
@@ -53,11 +53,10 @@ export function useRoutes<Path extends string>(
     If usePath returns latest it causes render thrashing
     If useRoutes hacks itself into the latest path nothing bad happens (...afaik)
   */
-
   const path = usePath(basePath) && getFormattedPath(basePath)
 
   // Handle potential <Redirect /> use in routes
-  useRedirectDetection(basePath, path)
+  useRedirectDetection(basePath, usePath(basePath))
 
   // Get the current route
   const route = useMatchRoute(routes, path, {
@@ -160,7 +159,6 @@ function usePathOptions(
   { basePath, matchTrailingSlash = true }: PathParamOptions
 ): [string | null, RouteMatcher[]] {
   const routes = (!Array.isArray(routeOrRoutes) ? [routeOrRoutes] : routeOrRoutes) as string[]
-
   const matchers = useMatchers(routes)
 
   return [trailingMatch(usePath(basePath), matchTrailingSlash), matchers]
@@ -230,12 +228,14 @@ function hashParams(params: string[]): string {
 // to fire, but without this hack useRoutes suppresses the update
 // TODO: find a better way to cause a synchronous update from useRoutes
 function useRedirectDetection(basePath: string, path: string | null) {
-  const [, forceRender] = useState(false)
+  const [, updateState] = useState({})
+  const forceRender = useCallback(() => updateState({}), [])
+
   useLayoutEffect(() => {
     if (path !== getFormattedPath(basePath)) {
-      forceRender((s) => !s)
+      forceRender()
     }
-  }, [basePath, path])
+  }, [forceRender, basePath, path])
 }
 
 function trailingMatch(path: string | null, matchTrailingSlash: boolean): string | null {


Test patch:
diff --git a/test/path.spec.tsx b/test/location.spec.tsx
similarity index 89%
rename from test/path.spec.tsx
rename to test/location.spec.tsx
index b5ef781..c1d092e 100644
--- a/test/path.spec.tsx
+++ b/test/location.spec.tsx
@@ -1,14 +1,6 @@
 import React, { useCallback, useEffect } from 'react'
 import { render, act, fireEvent } from '@testing-library/react'
-import {
-  navigate,
-  useLocationChange,
-  usePath,
-  useRoutes,
-  useHash,
-  LocationChangeSetFn,
-  RouteParams,
-} from '../src/main'
+import { navigate, useLocationChange, usePath, useRoutes, useHash, RouteParams } from '../src/main'
 
 beforeEach(() => {
   act(() => navigate('/'))
@@ -21,12 +13,12 @@ describe('useLocationChange', () => {
     basePath,
     onInitial = false,
   }: {
-    onChange: LocationChangeSetFn
+    onChange: (path: string) => void
     isActive?: boolean
     basePath?: string
     onInitial?: boolean
   }) {
-    useLocationChange(onChange, { isActive, basePath, onInitial })
+    useLocationChange(({ path }) => onChange(path), { isActive, basePath, onInitial })
     return null
   }
   test("setter doesn't get updated on mount", async () => {
@@ -234,6 +226,39 @@ describe('usePath', () => {
 
     expect(getByTestId('path')).toHaveTextContent('not found')
   })
+
+  test('has correct path after navigate during mount', async () => {
+    const trace = jest.fn()
+
+    const routes = {
+      '/': () => <Main />,
+      '/sub': () => <Sub />,
+    }
+
+    function App() {
+      const route = useRoutes(routes)
+
+      trace(usePath())
+
+      return route
+    }
+
+    function Main() {
+      return <span data-testid="render">main</span>
+    }
+
+    function Sub() {
+      navigate('/')
+      return <span data-testid="hash">sub</span>
+    }
+
+    act(() => navigate('/sub'))
+
+    const { getByTestId } = render(<App />)
+
+    expect(trace).toHaveBeenCalledWith('/')
+    expect(getByTestId('render')).toHaveTextContent('main')
+  })
 })
 
 describe('useHash', () => {
diff --git a/test/navigate.spec.tsx b/test/navigate.spec.tsx
index 8b74f14..3afd738 100644
--- a/test/navigate.spec.tsx
+++ b/test/navigate.spec.tsx
@@ -27,7 +27,7 @@ describe('useNavigate', () => {
     return (
       <button
         onClick={() => {
-          navigateWithBasePath(newPath, query, replace)
+          navigateWithBasePath(newPath, { query, replace })
         }}
       >
         click to navigate
@@ -222,39 +222,39 @@ describe('navigate', () => {
     window.history.pushState = jest.fn()
     const url = '/foo'
 
-    navigate(url, false)
+    navigate(url, { replace: false })
     expect(window.history.pushState).toHaveBeenCalled()
     jest.clearAllMocks()
 
-    navigate(url, true)
+    navigate(url, { replace: true })
     expect(window.history.replaceState).toHaveBeenCalled()
     jest.clearAllMocks()
 
-    navigate(url, null, true)
+    navigate(url, { replace: true })
     expect(window.history.replaceState).toHaveBeenCalled()
     jest.clearAllMocks()
 
-    navigate(url, null, false)
+    navigate(url, { replace: false })
     expect(window.history.pushState).toHaveBeenCalled()
     jest.clearAllMocks()
 
-    navigate(url, undefined, true)
+    navigate(url, { replace: true })
     expect(window.history.replaceState).toHaveBeenCalled()
     jest.clearAllMocks()
 
-    navigate(url, undefined, false)
+    navigate(url, { replace: false })
     expect(window.history.pushState).toHaveBeenCalled()
     jest.clearAllMocks()
 
-    navigate(url, {})
+    navigate(url, { query: {} })
     expect(window.history.pushState).toHaveBeenCalled()
     jest.clearAllMocks()
 
-    navigate(url, {}, true)
+    navigate(url, { replace: true, query: {} })
     expect(window.history.replaceState).toHaveBeenCalled()
     jest.clearAllMocks()
 
-    navigate(url, {}, false)
+    navigate(url, { replace: false, query: {} })
     expect(window.history.pushState).toHaveBeenCalled()
     jest.clearAllMocks()
   })
diff --git a/test/querystring.spec.tsx b/test/querystring.spec.tsx
index e3d4068..bce283f 100644
--- a/test/querystring.spec.tsx
+++ b/test/querystring.spec.tsx
@@ -12,21 +12,21 @@ describe('useQueryParams', () => {
     return <span data-testid="label">{JSON.stringify(query)}</span>
   }
   test('parses query', async () => {
-    act(() => navigate('/about', { foo: 'bar' }))
+    act(() => navigate('/about', { query: { foo: 'bar' } }))
     const { getByTestId } = render(<Route />)
     expect(getByTestId('label')).toHaveTextContent(JSON.stringify({ foo: 'bar' }))
   })
   test('navigation updates query', async () => {
     const q1 = { foo: 'bar' }
     const q2 = { bar: 'foo' }
-    act(() => navigate('/about', q1))
+    act(() => navigate('/about', { query: q1 }))
     const { getByTestId } = render(<Route />)
     expect(getByTestId('label')).toHaveTextContent(JSON.stringify(q1))
-    act(() => navigate('/check', q2))
+    act(() => navigate('/check', { query: q2 }))
     expect(getByTestId('label')).toHaveTextContent(JSON.stringify(q2))
     // Jest doesn't appear to do this...
     // act(() => window.history.back())
-    act(() => navigate('/about', q1))
+    act(() => navigate('/about', { query: q1 }))
     expect(getByTestId('label')).toHaveTextContent(JSON.stringify(q1))
   })
 })
@@ -41,14 +41,14 @@ describe('setQueryParams', () => {
     )
   }
   test('updates query', async () => {
-    act(() => navigate('/about', { bar: 'foo' }))
+    act(() => navigate('/about', { query: { bar: 'foo' } }))
     const { getByTestId } = render(<Route replace />)
 
     act(() => void fireEvent.click(getByTestId('update')))
     expect(document.location.search).toEqual('?foo=bar')
   })
   test('handles encoded values', async () => {
-    act(() => navigate('/about', { foo: 'foo' }))
+    act(() => navigate('/about', { query: { foo: 'foo' } }))
     const { getByTestId } = render(<Route replace={false} foo={'%100'} />)
 
     act(() => void fireEvent.click(getByTestId('update')))
@@ -56,7 +56,7 @@ describe('setQueryParams', () => {
     expect(getByTestId('update')).toHaveTextContent('Set Query: %100')
   })
   test('merges query', async () => {
-    act(() => navigate('/about', { bar: 'foo' }))
+    act(() => navigate('/about', { query: { bar: 'foo' } }))
     const { getByTestId } = render(<Route replace={false} />)
 
     act(() => void fireEvent.click(getByTestId('update')))
@@ -64,7 +64,7 @@ describe('setQueryParams', () => {
     expect(document.location.search).toContain('foo=bar')
   })
   test('merges query without null', async () => {
-    act(() => navigate('/about', { bar: 'foo' }))
+    act(() => navigate('/about', { query: { bar: 'foo' } }))
     const { getByTestId } = render(<Route replace={false} foo={null} />)
 
     act(() => void fireEvent.click(getByTestId('update')))
diff --git a/test/router.spec.tsx b/test/router.spec.tsx
index eed37d2..992df17 100644
--- a/test/router.spec.tsx
+++ b/test/router.spec.tsx
@@ -348,7 +348,7 @@ describe('navigate', () => {
 
   test('allows query string objects', async () => {
     // console.log(URLSearchParams)
-    act(() => navigate('/', { q: 'name', env: 'test' }))
+    act(() => navigate('/', { query: { q: 'name', env: 'test' } }))
     expect(window.location.search).toContain('q=name')
     expect(window.location.search).toContain('env=test')
   })