How does react trigger componentDidMount with safari cache?
Asked Answered
A

1

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.

Alonzoaloof answered 22/5, 2019 at 14:18 Comment(10)
Are you using React Router? SPA's handle forward/backward by utilizing history.push/pop of the browser's exposed history APIWheatworm
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.Alonzoaloof
So apparently its known limitation of page cache from Safari: 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.Wheatworm
I read the articles and they gave no additional information.Alonzoaloof
Maybe they do something like suggested here 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.Alchemize
I put together a project on codesandbox.io replicating your code with react 15.3 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 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 differenceMenstruate
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 snippetsAlonzoaloof
gist.github.com/oshell/bb1b3eec49a98cf6d59cef44806f0fa6 simply use this and replace react cdn links with 15.3Alonzoaloof
try to change the state it will trigger render() along with compoDidMount()Axenic
could you not simply reset the state before redirecting?Menstruate
M
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/[email protected]/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.

Menstruate answered 31/5, 2019 at 21:56 Comment(1)
I do not really like any solutions with interval and timeout, but I guess it works, so I will accept it.Alonzoaloof

© 2022 - 2024 — McMap. All rights reserved.