'How to intercept back button in a React SPA using function components and React Router v5

I'm working in a SPA in React that doesn't use React Router to create any Routes; I don't need to allow users to navigate to specific pages. (Think multi-page questionnaire, to be filled in sequentially.)

But, when users press the back button on the browser, I don't want them to exit the whole app; I want to be able to fire a function when the user presses the back button that simply renders the page as it was before their last selection. (Each page is a component, and they're assembled in an array, tracked by a currentPage state variable (from a state hook), so I can simply render the pages[currentPage -1].

Using (if necessary) the current version of React Router (v5), function components, and Typescript, how can I access the back-button event to both disable it and replace it with my own function?

(Other answers I've found either use class components, functions specific to old versions of React Router, or specific frameworks, like Next.js.)

Any and all insight is appreciated.



Solution 1:[1]

After way too many hours of work, I found a solution. As it ultimately was not that difficult once I found the proper path, I'm posting my solution in the hope it may save others time.

  1. Install React Router for web, and types - npm install --save react-router-dom @types/react-router-dom.
  2. Import { BrowserRouter, Route, RouteComponentProps, withRouter } from react-router-dom.
  3. Identify the component whose state will change when the back button is pressed.
  4. Pass in history from RouteComponentProps via destructuring:
    function MyComponent( { history }: ReactComponentProps) {
        ...
    }
    
  5. On screen state change (what the user would perceive as a new page) add a blank entry to history; for example:

    function MyComponent( { history }: ReactComponentProps) {
    
        const handleClick() {
            history.push('')
        }
    
    }
    

    This creates a blank entry in history, so history has entries that the back button can access; but the url the user sees won't change.

  6. Handle the changes that should happen when the back-button on the browser is pressed. (Note that component lifecycle methods, like componentDidMount, won't work in a function component. Make sure useEffect is imported from react.)
    useEffect(() => {
        // code here would fire when the page loads, equivalent to `componentDidMount`.
        return () => {
            // code after the return is equivalent to `componentWillUnmount`
            if (history.action === "POP") {
                // handle any state changes necessary to set the screen display back one page.
            }
        }
    })
    
  7. Wrap it in withRouter and create a new component with access to a Route's properties:
    const ComponentWithHistory = withRouter(MyComponent);
    
  8. Now wrap it all in a <BrowserRouter /> and a <Route /> so that React Router recognizes it as a router, and route all paths to path="/", which will catch all routes unless Routes with more specific paths are specified (which will all be the same anyway, with this setup, due to history.push(""); the history will look like ["", "", "", "", ""]).

    function App() {
    
    return (
        <BrowserRouter>
            <Route path="/">
                <ThemeProvider theme={theme}>
                    <ComponentWithHistory />
                </ThemeProvider>
            </Route>
        </BrowserRouter>
        );
    }
    
    export default App;
    
  9. A full example now looks something like this:

    function MyComponent( { history }: ReactComponentProps) {
        // use a state hook to manage a "page" state variable
        const [page, setPage] = React.useState(0)
    
    
        const handleClick() {
            setPage(page + 1);
            history.push('');
        }
    
        useEffect(() => {
        return () => {
            if (history.action === "POP") {
                // set state back one to render previous "page" (state)
                setPage(page - 1)
            }
        }
     })
    }
    
    const ComponentWithHistory = withRouter(MyComponent);
    
    function App() {
    
        return (
            <BrowserRouter>
                <Route path="/">
                    <ThemeProvider theme={theme}>
                        <ComponentWithHistory />
                    </ThemeProvider>
                </Route>
            </BrowserRouter>
            );
        }
    
    export default App;
    

If there are better ways, I would love to hear; but this is working very well for me.

Solution 2:[2]

The answer Andrew mentioned works, but there's better ways to do the same thing.

Method 1

Instead of wrapping your component with 'withRouter' and getting the history via props, you can simply use the useHistory hook to do the same.

That would be something like this:

import { useHistory } from "react-router-dom";

const MyComponent = () => {
    const history = useHistory();

    useEffect(() => {
        return () => {
            if(history.action === "POP") {
                //DO SOMETHING
            }
        }
    });
}

Method 2

Simply use the component provided by react-router.

Use it something like this:

import { Prompt } from "react-router-dom";

const MyComponent = () => {
    return (
        <>
            <div className="root">
            //YOUR PAGE CONTENT
            </div>
            <Prompt message="You have unsaved changes. Do you still want to leave?"/>
        </>
    );
}

If you want to run some specific code:

<Prompt
  message={(location, action) => {
    if (action === 'POP') {
        //RUN YOUR CODE HERE
        console.log("Backing up...")
    }

    return location.pathname.startsWith("/app")
      ? true
      : `Are you sure you want to go to ${location.pathname}?`
  }}
/>

Refer to the docs for more info

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2