Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React, Single Page Apps, and the Browser's back button

I know my question could simply have a "This cannot be done, this defines the purpose of SPA". But...

I navigate to mydomain.com in my REACT web app. This page loads data from the backend and populates elaborate grids. It takes it about 2 seconds to load and render.

Now I click a link on that elaborate page and navigate to mydomain.com/otherPage. When I click the browser's BACK button to return to mydomain.com, it's blank, and has to be rebuilt from scratch as SPA dictates the DOM must be erased and re-built with every page change (at least the page-specific dynamic parts of it, as routes can be inside a fixed layout of header/footer etc). I get that...

Other than migrating to nextJS and using SSR....

Is there any magic solution in REACT to somehow 'retain' the DOM for a page when navigating out of it, so that when you browser-back into it, that page is instantly shown and not rendered from scratch?

like image 296
JasonGenX Avatar asked Feb 17 '21 18:02

JasonGenX


People also ask

How do you handle back button of browser in React?

Intercept or Handle the Browser's Back Button in React Router. We can listen to back button actions by running the setRouteLeaveHook event for back button actions. } export default withRouter(App);

Is React good for single page application?

React is great for single page applications because it allows developers to create applications that are mobile first and responsive. React allows developers to create applications that are fast and easy to update, which is great for mobile applications.

How do you go back to one page in React?

To go back to the previous page, pass -1 as a parameter to the navigate() function, e.g. navigate(-1) . Calling navigate with -1 is the same as hitting the back button. Similarly, you can call the navigate function with -2 to go 2 pages back.

How do you prevent go back to previous page in react JS?

Using componentDidUpdate method of React page lifecycle, you can handled or disabled go back functionality in browser. basically componentDidUpdate method will call automatocally when component got updated. so once your component is updated you can prevent to go back as below.


Video Answer


2 Answers

Yes, it is very much possible to switch routes while keeping the DOM rendered, but hidden! If you are building a SPA, it would be a good idea to use client side routing. This makes your task easy:

For hiding, while keeping components in the DOM, use either of the following css:

  1. .hidden { visibility: hidden } only hides the unused component/route, but still keeps its layout.

  2. .no-display { display: none } hides the unused component/route, including its layout.

For routing, using react-router-dom, you can use the function children prop on a Route component:

children: func

Sometimes you need to render whether the path matches the location or not. In these cases, you can use the function children prop. It works exactly like render except that it gets called whether there is a match or not.The children render prop receives all the same route props as the component and render methods, except when a route fails to match the URL, then match is null. This allows you to dynamically adjust your UI based on whether or not the route matches.

Here in our case, I'm adding the hiding css classes if the route doesn't match:

App.tsx:

export default function App() {
  return (
    <div className="App">
      <Router>
        <HiddenRoutes hiddenClass="hidden" />
        <HiddenRoutes hiddenClass="no-display" />
      </Router>
    </div>
  );
}

const HiddenRoutes: FC<{ hiddenClass: string }> = ({ hiddenClass }) => {
  return (
    <div>
      <nav>
        <NavLink to="/1">to 1</NavLink>
        <NavLink to="/2">to 2</NavLink>
        <NavLink to="/3">to 3</NavLink>
      </nav>
      <ol>
        <Route
          path="/1"
          children={({ match }) => (
            <li className={!!match ? "" : hiddenClass}>item 1</li>
          )}
        />
        <Route
          path="/2"
          children={({ match }) => (
            <li className={!!match ? "" : hiddenClass}>item 2</li>
          )}
        />
        <Route
          path="/3"
          children={({ match }) => (
            <li className={!!match ? "" : hiddenClass}>item 3</li>
          )}
        />
      </ol>
    </div>
  );
};

styles.css:

.hidden {
  visibility: hidden;
}
.no-display {
  display: none;
}

Working CodeSandbox: https://codesandbox.io/s/hidden-routes-4mp6c?file=/src/App.tsx

Compare the different behaviours of visibility: hidden vs. display: none.

Note that in both cases, all of the components are still mounted to the DOM! You can verify with the inspect tool in the browser's dev-tools.

Reusable solution

For a reusable solution, you can create a reusable HiddenRoute component.

In the following example, I use the hook useRouteMatch, similar to how the children Route prop works. Based on the match, I provide the hidden class to the new components children:

import "./styles.css";
import {
  BrowserRouter as Router,
  NavLink,
  useRouteMatch,
  RouteProps
} from "react-router-dom";

// Reusable components that keeps it's children in the DOM
const HiddenRoute = (props: RouteProps) => {
  const match = useRouteMatch(props);
  return <span className={match ? "" : "no-display"}>{props.children}</span>;
};

export default function App() {
  return (
    <div className="App">
      <Router>
        <nav>
          <NavLink to="/1">to 1</NavLink>
          <NavLink to="/2">to 2</NavLink>
          <NavLink to="/3">to 3</NavLink>
        </nav>
        <ol>
          <HiddenRoute path="/1">
            <li>item 1</li>
          </HiddenRoute>
          <HiddenRoute path="/2">
            <li>item 2</li>
          </HiddenRoute>
          <HiddenRoute path="/3">
            <li>item 3</li>
          </HiddenRoute>
        </ol>
      </Router>
    </div>
  );
}  

Working CodeSandbox for the reusable solution: https://codesandbox.io/s/hidden-routes-2-3v22n?file=/src/App.tsx

like image 163
deckele Avatar answered Oct 23 '22 01:10

deckele


For API calls

You can simply put your generated elements that need intensive calculation in a state, in a component that never gets unmounted while changing page.

Here is an example with a Parent component holding 2 children and some JSX displayed after 5 seconds. When you click on the links you navigate to children, and when you click on browser's back button, you get back on the URL path. And when on / path again, the "intensive" calculation needing element is displayed immediately.

import React, { useEffect, useState } from "react";
import { Route, Link, BrowserRouter as Router } from "react-router-dom";

function Parent() {
  const [intensiveElement, setIntensiveElement] = useState("");
  useEffect(() => {
    const intensiveCalculation = async () => {
      await new Promise((resolve) => setTimeout(resolve, 5000));
      return <p>Intensive paragraph</p>;
    };
    intensiveCalculation().then((element) => setIntensiveElement(element));
  }, []);
  return (
    <Router>
      <Link to="/child1">Go to child 1</Link>
      <Link to="/child2">Go to child 2</Link>
      <Route path="/" exact>
        {intensiveElement}
      </Route>
      <Route path="/child1" exact>
        <Child1 />
      </Route>
      <Route path="/child2" exact>
        <Child2 />
      </Route>
    </Router>
  );
}

function Child1() {
  return <p>Child 1</p>;
}

function Child2() {
  return <p>Child 2</p>;
}

About redisplaying quickly the DOM

My solution above works for not doing slow things twice like API calls. But following the remarks of Mordechai, I have made an example repository to compare DOM loading time of really big HTML for 4 solutions when using browser back button:

  1. Plain html without javascript (for reference)
  2. React with the code example I gave above
  3. Next.js with next's page routing
  4. A CSS solution with React and overflow: hidden; height: 0px; (more efficient than display: none; and the elements do not take any space contrary to visibility: hidden;, opacity: 0; etc. but maybe there is a better CSS way)

Each exemple loads an initial page of 100 000 <span> elements, and has links to navigate to small pages, so that we can try the back button on the browser.

You can test yourself the static version of the examples on github pages here (the pages take several seconds to load on a normal computer, so maybe avoid clicking on them if on mobile or so).

I've added some CSS to make the elements small enough to see all of them on the screen, and compare how does the browser update the whole display.

And here are my results:

On Firefox:

  1. Plain HTML loads in ~2 sec, and back button displays page in ~1 sec
  2. Next app loads in ~2 sec, and back button displays page in ~1 sec
  3. CSS solution in React app loads in ~2 sec, and back button displays page in ~1 sec
  4. React app loads in ~2.5 sec, and back button displays page in ~2 sec

On Chrome:

  1. CSS solution in React app loads in ~2 sec, and back button displays page in ~1 sec
  2. React app loads in ~2.5 sec, and back button displays page in ~2 sec
  3. Plain HTML loads in ~8 sec, and back button displays page in ~8 sec
  4. Next app loads in ~8 sec, and back button displays page in ~8 sec

Something important to note also: for Chrome when Next.js or plain HTML take 8 seconds, they actually load elements little by little on the page, and I have no cache with the back button.

On Firefox I don't have that little by little displaying, either there is nothing or everything is displayed (like what I have on Chrome with react state usage).

I don't really know what I can conclude with that, except maybe that testing things is useful, there are sometimes surprises...

like image 45
Roman Mkrtchian Avatar answered Oct 23 '22 00:10

Roman Mkrtchian