# How feedback widgets capture the right page in SPAs

> In a single-page app the page never reloads, so a feedback form can record a stale address. Keep the route current by calling setMetadata on every navigation, and check a test submission after each route change. The submission records the page URL it was sent from.

Source: https://escutaproduto.com/resources/feedback-in-single-page-apps
Last updated: 2026-10-11

## Why does feedback from an app point at the wrong screen?

In a single-page app, the browser loads the shell once, then a router changes the visible screen and the address bar without a full reload. A script that reads the address only when it first loads will keep reporting the first screen. A visitor who sends feedback from the billing screen can end up with a record that says they were on the home page.

The fix has two parts. First, make sure the page URL is read when the visitor sends, not when the page first loads. Second, add the current route as metadata whenever it changes, so the record also holds your app's own view of the screen.

## How does the widget record the page URL?

The Escuta Produto widget saves the page URL and the browser user agent with each submission automatically, so you do not need to pass them yourself. Treat that URL as the address the browser shows when the visitor sent the message. In a single-page app, check that it matches the screen in a test, because your own routing decides what the address bar shows.

For more control, the REST API accepts a `pageUrl` field in the JSON body. If you build your own feedback form, you can send exactly the route you want to record. The full field list is in the [API docs](/docs/api).

## How do you add the current route as metadata?

Use `EscutaProduto.setMetadata` to store a route value whenever your app navigates. The call takes a JSON object and stores it with later submissions. Use the same key name on every call, so the value you read later always comes from the same field.

```js
// Sends the current route to the widget. Safe to call before the script has loaded.
function syncRouteMetadata() {
  const api = window.EscutaProduto;
  if (!api || typeof api.setMetadata !== "function") return;
  api.setMetadata({ route: window.location.pathname + window.location.search });
}

syncRouteMetadata();
window.addEventListener("popstate", syncRouteMetadata);
```

The `popstate` event fires on back and forward navigation, but not when your code calls `history.pushState` directly. Most routers call `pushState` for every link click, so call `syncRouteMetadata` from the router's own navigation hook.

## What does the code look like in React Router and Next.js?

Both routers expose the current path to components, so you can sync the metadata from one effect. In React Router, read the location with `useLocation`:

```jsx
import { useEffect } from "react";
import { useLocation } from "react-router-dom";

export function FeedbackRouteSync() {
  const location = useLocation();

  useEffect(() => {
    const api = window.EscutaProduto;
    if (api && typeof api.setMetadata === "function") {
      api.setMetadata({ route: location.pathname + location.search });
    }
  }, [location.pathname, location.search]);

  return null;
}
```

In the Next.js App Router, `usePathname` from `next/navigation` plays the same role. Render the component once inside your root layout, so it runs on every client-side navigation. Check the import path for your router version, because the packages have moved between releases.

## How do you handle full page loads and back navigation?

A full page load starts a fresh script, so the first `setMetadata` call runs again. That is why the sync should run on load and on each route change. Back and forward navigation in a router usually updates the location object, so the effect above covers it. Your own `popstate` listener is the fallback for code that does not use a router.

Test the pattern with four moves: load a deep link directly, click a link inside the app, press the browser back button, and use a tab that opens with a new route. Send a short test submission after each move and check the route it records.

Hash-based routes need one extra step. When your app keeps the screen in the part of the address after the hash symbol, the path stays the same while the screen changes. A sync that reads only the path reports the same route for every screen. Include the hash in the value you send, so the record shows the screen the visitor actually saw.

Keep the sync out of render code. Calling setMetadata on every render runs far more often than a navigation does, and each call is wasted work. Tie the sync to the router's navigation event, or to the effect that runs when the location changes, so it runs once per screen. Two quick checks confirm the timing: open the browser's network tab and make sure no extra requests appear as you move between screens, and add a temporary console line to count how often the sync runs during a single navigation.

Finally, think about what the route reveals. A path that contains a record identifier, such as an invoice number, is useful for support but is also personal data in some products. Decide with your privacy adviser whether the full path belongs in the metadata, or whether a page name is enough.

## How do you check that the captured page is correct?

Keep a short checklist, and run it after every change to routing or to the widget script:

1. Open a deep link, such as a settings subpage, and send a test message from it.
2. Navigate within the app to another screen and send a second message.
3. Press back and send a third message.
4. Compare the page URL and the route metadata in each record in the dashboard.
5. Confirm that the two values agree with what the browser showed at the time.

If the two values disagree, check whether `setMetadata` replaces or merges earlier keys in your setup. Call it twice with different values and inspect the saved record to see which behavior you get, then build the sync around that result.

## Doing this with Escuta Produto in a single-page app

Add the widget script once in your app shell, with the `defer` attribute, and place the route sync component next to it. The setup steps for each framework are in the [widget docs](/docs/widget). For the wording of the trigger a visitor clicks before the panel opens, read [what a feedback button should say](/resources/feedback-button-copy). If your app also runs on phones, [designing a feedback widget for mobile screens](/resources/feedback-widget-mobile-design) covers how the panel behaves on a small screen with the keyboard open.

## Frequently asked questions

### Why does feedback from my single-page app show the wrong page?

The browser does not reload when the router changes screens, so anything read only at first load stays stale. Sync the route as metadata on every navigation, then send a test message from each screen to confirm the recorded page.

### Does the widget record the page URL automatically?

Yes. Each submission saves the page URL and the browser user agent. In a single-page app, confirm that the recorded address matches the screen in a test submission, and add the route as metadata for a second, app-level view.

### Should I send the page URL myself through the REST API?

Use the API when you build your own form, because the pageUrl field lets you send exactly the route you want. If you use the widget, the page URL is recorded for you and you only need the route metadata.
