0

Hi guys hope you're well and all set for Christmas

So I have this functional component which plays an animation when you press a link to change the page. This is really nice but is completely bugged on browsers like Safari for iOS, due to gestures where you can swipe back and forth.

I need for a component to be able to check if a rerender is caused by BACK/FORWARD BUTTON PRESSES/GESTURES, and then change variables to stop animations and such.

Here's the component, with all the unimportant stuff removed obviously.

This component has all attempts at back button/forward button detection code removed.

We want the reference noTransition.current to change when the URL changes due to these buttons being pressed. This value MUST change before any rerender happens after the buttons are pressed.

const Central = (props) => {
    let location = useLocation();

    var noTransition = useRef(true);
    const history = useNavigate()

    noTransition.current = false;

    return (
        <div className="centralMain">
            <SlickContainer noTransition={noTransition.current}/>
            {/* actual component here doesn't matter, just the fact that the "noTransition" attribute must change */}
        </div>
    )
}

export default Central

I've tested about a billion solutions, some kind of promising but all very hacky. I got very excited about the idea of having a window.onpopstate listener which would change the noTransition.current value but I quickly (after several hours) learnt that the render when the URL changes actually happens before the listener fires off, so it is futile.

I did also try having useEffects for stuff like Navigator, but then I learnt that useEffect actually fires after render...

I also tried some really wacky ideas with passing time values from Links via state and comparing how long it had been. This is ultimately a hack, and I need a proper solution.

If you have any ideas, or need any additional information or code, please do let me know.

Please note that we are unfortunately using react-router v6.

Thank you for listening, please help me.

Jamie Adams
  • 307
  • 1
  • 3
  • 11
  • Did you try any of the promising methods but with the [useLayoutEffect](https://reactjs.org/docs/hooks-reference.html#uselayouteffect) instead of `useEffect`? What about sniffing the user agent and disabling animations on specific iOS versions? – Drew Reese Dec 21 '21 at 21:50
  • @DrewReese I will try useLayoutEffect, I have considered disabling animations on iOS, but it's quite a big move since most people who've seen earlier versions on phones remarked how the animations between pages looked quite cool. Thank you, I'll update after trying useLayoutEffect – Jamie Adams Dec 22 '21 at 15:18
  • Oh, are you using transitions *between* routes and not just some animation when the new route component mounts? I'm not sure route transitions work in RRDv6.... at least not like they did in v5. May have to check the docs. – Drew Reese Dec 22 '21 at 21:58

1 Answers1

0

You can use a custom history to listen to navigation events.

// history.js

import history from 'history/browser'; // v6
// import { createBrowserHistory } from 'history'; // v5
// const history = createBrowserHistory(); // v5

export default history;


// app.js

import { CustomRouter } from './customRouter';
import history from './history';

const App = () => {

  ...
  /* Use Router instead of BrowserRouter and add custom-history */
  <CustomRouter navigator={history}> {/* prop is called history for v5 */}
    ... routes
  </CustomRouter>

  ...
}
...

// component

const Central = (props) => {
    const noTransition = useRef(false);

    useEffect(() => {
        const unlisten = history.listen(({ action, location }) => {
            // POP === popstate event; browser back/forward navigation
            noTransition.current = action === 'POP' ? false : true;
        });
        return unlisten;
    }, []);

    return (
        <div className="centralMain">
            {/* Pass ref instead of value and access ref.current inside container */}
            <SlickContainer noTransition={noTransition.current}/>
            {/* actual component here doesn't matter, just the fact that the "noTransition" attribute must change */}
        </div>
    )
}

export default Central

See this answer for a basic CustomRouter implementation!

MarcRo
  • 2,434
  • 1
  • 9
  • 24
  • OP is using `react-router-dom` v6, which doesn't expose out the `history` object so easily. – Drew Reese Dec 22 '21 at 01:52
  • It does, I'll update my answer – MarcRo Dec 22 '21 at 01:58
  • 1
    Sure, you can create a custom router instance, but `location` is also a required prop. See one of my answers [here](https://stackoverflow.com/a/70000286/8690857) for basic implementation. – Drew Reese Dec 22 '21 at 02:14
  • Thank you for your answer. It does look quite promising but the line `location.state = action === 'POP' ? false : true;` makes the program quite upset: "TypeError: Cannot assign to read only property `'state'` of object `'#'`" – Jamie Adams Dec 22 '21 at 16:32
  • I wasn't aware location state is read-only. You basically just want to determine whether transition should be disabled or not - and pass this info to the component. Will update the answer – MarcRo Dec 22 '21 at 16:43
  • Thank you again. This somewhat works but when you go back or forward (by pressing the browser buttons), after pressing/clicking a Link, it renders twice and the first render has the animation enabled. Here is it in action: https://imgur.com/a/pG53rCL – Jamie Adams Dec 22 '21 at 18:15
  • I don't understand what the issue is, sorry. Via history.listen you can handle side-effects before route changes take place. What part of your code triggers unwanted re-renders is maybe out of scope for this question. Maybe someone can help you if you share how you implemented the transition - or try to pinpoint more precisely what the problem is! – MarcRo Dec 22 '21 at 22:18