Navigate back in a multi-page app

Goal:

  • I have a multi-page app that is embedded in another (non-Retool) application. I would like to implement a "details view", where clicking on an item in a list navigates to a page with more details on that item.
  • I would like to add a link to this page that takes the user back to the previous page.
  • This should all happen within the multi-page app, as I don't want to navigate away from the embed.

Steps:

  • I've found Navigate back with window.history.back, but frankly I don't understand the suggestions there. The screenshots don't appear to line up with any UI that I can find in Retool! If I'm reading it correctly, the suggestion is to make a custom component but it seems I would need to scaffold a whole React component to do this(?) If so, it seems quite a reach for what seems like fairly simple functionality.
  • I've also tried digging around in a history global, but this doesn't appear to have the back method. It doesn't doesn't appear to surface the history stack so I can't manually call pushState. This leaves me to manually implement my own history stack - which again, seems like quite a lot of overhead to me.

Am I missing something?

If not, would you consider adding a "back" option to the Go to page functionality.

To accomplish similar "detail drill-down" in our own app, without the hassles of navigating away from (and then back to) the current page in the app, we display the desired detail data in a drawer frame. This way, the state of all components on the page remain as-is and nothing has to reload.

May not apply to your use case, but for us, since the advent of drawer frames, we've put this scenario to use probably a half-dozen times.

Hi, thanks - that makes sense, and is actually what we've been doing up to this point (although with a modal view instead of a drawer).

However, my PM/Designer now want to reuse this view across several different pages. I've looked into moving the modal into the global scope, but this means all of the queries needed to render the view now need to also move into the global scope. We may end up having to go down this route because I don't see other options, but I'm worried about maintaining the code when there's a lot of stuff in the global scope.

Hence it would be much simpler to make these views individual pages, to constrain the scope. But without a back button, the PM/Designer are worried about the navigation flow.

1 Like

Quick update on this: I've come up with a (pretty hacky!) workaround.

The core idea is that any page where we want a back button, we need to navigate to that page with a back_to URL search parameter. The back button component then uses the dynamic code option to set {{ url.searchParams.back_to }} for the target page field:

Screenshot 2025-01-28 at 11.03.11

This means every link to the page must add a query param of {{ retoolContext.currentPage }}:

Obviously this is quite a fiddly hack, as you have to remember to add this every time.

There's also not a true back/forward stack - so going back and then forward again isn't supported. So I still think having access to a fully implemented history API would be the best option. (From what I gather from my other workaround ideas, it's using React Router underneath, so I wonder/hope if it shouldn't be too much of a stretch to expose this to us).

Hi @Schteevynn ,

Thanks for reaching out. We don't currently support a "navigate back" feature for multipage apps, but I've logged this feature request internally.

In the meantime, what you've described makes sense to me! Another option is to create a global variable and add to it each time you navigate a page to keep track of all the pages you've navigated to, and use that variable to go "back" (pop the post recent element from the list).

Thanks for confirming!

create a global variable and add to it each time you navigate a page to keep track of all the pages you've navigated to, and use that variable to go "back" (pop the post recent element from the list).

I did also try this and from what I could tell, the transformer I used to achieve this wasn't updating when {{ retoolContext.currentPage }} changed - so couldn't figure out a way of achieving this. I couldn't see a way of adding an event handler to page navigations either.

1 Like

I'm afraid I continue to hit pretty major limitations because of this.

My back_to hack breaks if the page depends on a search param. So for example, I have an individual view page that uses an id search param to determine which site (a concept within our app) to show. The page doesn't really make sense without an id search param as it uses this to query the DB for the site to show.

I would like to link from this page to another page, which means I somehow also need to encode the id search param somewhere as part of this link - so that on the other page, clicking the back button will go back to the site page with the correct id search param.

I thought about further hacks like setting back_param_key and back_param_value search params, which could then used by the back button to set the key/value pair on the back link. But it seems Retool doesn't let me programmatically set search param keys!

Screenshot 2025-01-29 at 14.43.15

So unfortunately, I think just have to give up on back navigation here.

Here's what I was thinking for the global variable:

  1. Create a pageStack variable with initial value []

  2. Add an additional event handler to your navigation component that appends the current page to the end of the pageStack variable:

  3. Create a goBack button that has 2 event handlers:
    3a. Navigate to the last page in the pageStack variable. Here you also configure query params (you'd have to store those in the pageStack also), which I haven't shown.


    3b. Pop the last element off of the pageStack variable:

Hopefully this helps!

1 Like

Hi thanks for the help!

That does make sense in terms of a history stack implementation!

However I still couldn't find a way to integrate query params because of the issue above where query param keys can't be set programmatically.

Hm I see -- maybe you can configure passing data to the page with a variable instead of via URL params? Or, you can configure a Run Script or a JS Query event handler - you can dynamically compute url param keys in code and pass them to utils.openPage.

In the meantime I can ask the team about the possibility of supporting dynamic query/hash param keys.

Just to add to this answer (it's an idea, will try to implement it and come back to show my results), but if you store the url.href (for any multipage App) or pageUrl.href (for single page apps) value instead of retoolContext.currentPage in the stack, you'll have all the queryParams ? & hashParams # of the stored page in the array, i'm pretty sure this will work because i've already implemented a custom navigation component which propagates some params to all our apps by default

Guys comming back to this.

I managed to implement a pretty barebones routing solution based on my last response, here's how anyone can replicate it (as a heads up, this was implemented inside a module that's used in all of our existing apps as a Navbar component)

first of all, i'll contextualize everyone on the component we have, it's this module:

which looks something like this when used:

The module expects a pageUrl variable, which should always be the url.href value of the app (for multipage-apps)
image

or urlparams.href for singlepage-apps
image

this means that the module will always have the current url the user is viewing as context, with this configuration, I created 3 JS queries to manage the pageStack array with localStorage, here's how i did it:
image

I'll start with the pageStackRouter query

function splitUrl(url) {
  const [base, hash] = url.split("#");
  return { base, hash: hash || "" };
}

const currentUrl = splitUrl(pageUrl.value);
const sanitizedBase = currentUrl.base;
const currentHash = currentUrl.hash;

let pageStackStorage = localStorage.values?.pageStack || [];
let currentPageId = localStorage.values?.currentPageId ?? (pageStackStorage[pageStackStorage.length - 1]?.id ?? -1);

// Check if the current URL matches the expected entry at `currentPageId`
const expectedEntry = pageStackStorage.find(page => page.id === currentPageId);

if (expectedEntry?.base === sanitizedBase) {
  // Navigation is due to back/forward: update hash, do NOT add to stack
  expectedEntry.hash = currentHash;
} else {
  // User-initiated navigation: check against last entry
  const lastPage = pageStackStorage[pageStackStorage.length - 1];
  
  if (!lastPage || lastPage.base !== sanitizedBase) {
    // Truncate the stack if navigating from a non-latest position (like browser history)
    const truncateIndex = pageStackStorage.findIndex(page => page.id === currentPageId);
    pageStackStorage = pageStackStorage.slice(0, truncateIndex + 1);

    // Add new entry
    const newEntry = { id: Date.now(), base: sanitizedBase, hash: currentHash }; // Use unique ID (e.g., timestamp)
    pageStackStorage = [...pageStackStorage, newEntry];
    currentPageId = newEntry.id;
  } else {
    // Update hash of the last entry
    lastPage.hash = currentHash;
  }
}

// Persist changes
localStorage.setValue("pageStack", pageStackStorage);
localStorage.setValue("currentPageId", currentPageId);

This code uses the pageUrl variable value, if the pageStack is empty or doesn't exist in localStorage, initializes the variable & stores it, if there's an existing value, it'll update the array and add the url to it (with hash params included, if they come in the variable) IF the last page visited is not the same, if it's the same, it'll only update the params, it's worth mentioning that this query runs on every page load

For the Back & forwad navigation i defined these scripts:

  • Back navigation:
const currentEntry = localStorage.values?.pageStack?.find(page => page.id === localStorage.values?.currentPageId);
if (currentEntry) {
  // Find the index of the current entry
  const currentIndex = localStorage.values?.pageStack?.findIndex(page => page.id === localStorage.values?.currentPageId);
  
  // Check if the current index is valid and not the first entry
  if (currentIndex > 0) {
    // Get the previous page in the stack
    const previousPage = localStorage.values?.pageStack[currentIndex - 1];
    
    // Construct the full URL with the hash (if it exists)
    const fullUrl = previousPage.hash 
      ? `${previousPage.base}#${previousPage.hash}` 
      : previousPage.base;

    // Update currentPageId to the previous entry's ID
    localStorage.setValue("currentPageId", previousPage.id);
    
    // Navigate to the previous page
    utils.openUrl(fullUrl, {newTab: false, forceReload: true});
  }
}
  • Forward navigation:
const currentEntry = localStorage.values?.pageStack?.find(page => page.id === localStorage.values?.currentPageId);
if (currentEntry) {
  // Find the index of the current entry
  const currentIndex = localStorage.values?.pageStack?.findIndex(page => page.id === localStorage.values?.currentPageId);
  
  // Check if the current index is valid and not the last entry
  if (currentIndex < localStorage.values?.pageStack?.length - 1) {
    // Get the next page in the stack
    const nextPage = localStorage.values?.pageStack[currentIndex + 1];
    
    // Construct the full URL with the hash (if it exists)
    const fullUrl = nextPage.hash 
      ? `${nextPage.base}#${nextPage.hash}` 
      : nextPage.base;

    // Update currentPageId to the next entry's ID
    localStorage.setValue("currentPageId", nextPage.id);
    
    // Navigate to the next page
    utils.openUrl(fullUrl, {newTab: false, forceReload: true});
  }
}

with this, the configuration had to be done ONCE (navbar component is used as default header of our apps) so the propagation of this new feature was easy to implement in our ecosystem.

almost forgot, here's an example of how the localStorage.values.pageStack looks after the user navigates to a few apps:

[
    {
        "id": 1741706427501,
        "base": "url0",
        "hash": "code=&country=&pageNumber=&pageSize=&phone=&userId="
    },
    {
        "id": 1741707727308,
        "base": "url1",
        "hash": ""
    },
    {
        "id": 1741707807641,
        "base": "url2",
        "hash": ""
    },
    {
        "id": 1741707811489,
        "base": "url3",
        "hash": ""
    }
]

And a really important key currentPageId: 1741707811489, this one is also stored in localStorage & is used to find the currentPage in the stack & navigate accordingly based on it's position (to avoid a navigation loop when going backwards/forwards)

I know there might be a few caveats i'm missing but as far as this implementation goes, it gives us a head start to experiment with some routing from now on (until we got a native routing solution from Retool)

Let me know if it works for you, hit me up if you have any doubts about this implementation!

1 Like