11

React 16 triggers componentDidMount() when going back in Safari, even tho the component never unmounted. How does react know when to mount?

class Foo extends React.Component {
  state = {
    loading: false
  }

  componentDidMount() {
    // when going back in safari
    // triggers in react 16, but not in 15.3 or preact
    console.log('mounted');
  }

  componentWillUnmount() {
    // will never trigger
    console.log('will unmount');
  }

  leave() {
    this.setState({
      loading: true
    });
    setTimeout(() => {
      window.location.href = 'https://github.com/';
    }, 2000);
  }

  render() {
    return this.state.loading ? <div>loading...</div> : <button onClick={this.leave.bind(this)}>leave</button>;
  }
}

Background

Safari uses bfcache. If you go back it takes the last page from cache.

When using react 15.3 or libraries such as preact, leaving the page will not trigger componentWillUnmount and going back will not trigger componentDidMount.

This behaviour causes several issues - for example when you set your page state to loading before redirecting. If the user goes back, the state is still set to loading and you cannot even reset the state using componentDidMount, because it never triggers.

There is a solution, by using onpageshow, but since it only triggers one time, you have to reload the whole page using window.location.reload(). This is also the reason react cannot rely on this solution.

oshell
  • 8,923
  • 1
  • 29
  • 47
  • Are you using React Router? SPA's handle forward/backward by utilizing history.push/pop of the browser's exposed history API – Rikin May 22 '19 at 16:44
  • no - the redirect goes to another page. I am using preact and want to figure out how react is doing it to eventually use this functionality. – oshell May 22 '19 at 16:50
  • So apparently its known limitation of page cache from Safari: https://webkit.org/blog/427/webkit-page-cache-i-the-basics/ you may want to explore their docs further as I'm sure they may have solution already since its been implemented in 2009 it seems based on post date. – Rikin May 22 '19 at 17:22
  • I read the articles and they gave no additional information. – oshell May 23 '19 at 15:30
  • Maybe they do something like suggested here https://madhatted.com/2013/6/16/you-do-not-understand-browser-history but i have not found a reference to "unload" in the react source. That doesn't mean it's not happening in one of the dependencies though. – konqi May 28 '19 at 12:52
  • I put together a project on codesandbox.io replicating your code with react 15.3 https://codesandbox.io/s/goofy-haibt-nxgpm although you are right abou componentWillUnmount (it's expected) I think you are wrong about the componentDidMount not being called. I open this https://nxgpm.codesandbox.io/ in safari and the console always shows the `mounted` message, even after pressing the back button... do you have server side rendering in your setup? that will maybe make a difference – Tiago Coelho May 29 '19 at 14:22
  • try with a plain file in safari. codesandbox will use an iframe or similar things, which do not really replicate the situation. I can post copy and paste snippets – oshell May 29 '19 at 15:00
  • 2
    https://gist.github.com/oshell/bb1b3eec49a98cf6d59cef44806f0fa6 simply use this and replace react cdn links with 15.3 – oshell May 29 '19 at 15:01
  • try to change the state it will trigger `render()` along with `compoDidMount()` – Ashish Kamble May 31 '19 at 08:57
  • could you not simply reset the state before redirecting? – Tiago Coelho May 31 '19 at 21:48

1 Answers1

5

I do not know exactly how React 16 is calling the mount, but it is a completely different engine, so It may be on purpose or not. One thing you can do to work around the issue is to schedule a state reset just before redirecting, like this:

<html>
  <head>
    <script
      crossorigin
      src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.1/react.js"
    ></script>
    <script
      crossorigin
      src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.1/react-dom.js"
    ></script>
    <script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script type="text/babel">
      class Foo extends React.Component {
        state = {
          loading: false
        };
        componentDidMount() {
          console.log("mounted");
        }
        leave() {
          this.setState({
            loading: true
          });
          setTimeout(() => {
            this.setupReset();
            window.location.href = "https://github.com";
          }, 2000);
        }

        setupReset() {
          let interval = setInterval(() => {
            if (
              !!window.performance &&
              window.performance.navigation.type === 2
            ) {
              clearInterval(interval);
              console.log('reseting');
              this.setState({ loading: false });
            }
          },500);
        }

        render() {
          return this.state.loading ? (
            <div>loading...</div>
          ) : (
            <button onClick={this.leave.bind(this)}>leave</button>
          );
        }
      }
      ReactDOM.render(<Foo />, document.getElementById("app"));
    </script>
  </body>
</html>

and then when you go back the execution resumes and it is possible to detect if its coming from history and reset the state.

you could actually setup this reset mechanism right on componentDidMount, the first time.

Tiago Coelho
  • 5,023
  • 10
  • 17
  • I do not really like any solutions with interval and timeout, but I guess it works, so I will accept it. – oshell Jun 04 '19 at 14:58