May 12, 2020

Scrolling. You thought its trivial.

Well maybe it is. But not in our case. Let me explain.

Let's just say I have a React app. And let's just imagine for a second that all I want to do is make a page scroll & filter something on a page. Should be easy right?


A bit more context.

Suppose what I have is a page of multiple 'Cards'. Now, each card is a completely isolated component. By 'isolated' here we mean it is fully independent. So it loads on its own, has some unique API in it that it uses to retrieve data with say axios. Let's even say we get some nice skeleton on loading.

Point being, those 'cards' are completely decoupled from the main page and work on its own that encourages reusability and make our life easier in the future (and well, something that is motivated by React itself).

Now, what we want to achieve is click on a button in top right card and scroll to the bottom card as well as filter it somehow. For example, you might have several tabs in the bottom component and what you want is to do 2 main simple things:

  1. Scroll to this guy
  2. Filter it to the 2nd tab

Sounds so simple that you might be wondering "Whats it all about??".

Well, let's just think how this practically can be done in general?

  • Take myCoolCard.jsx and handle onClick event that would send a callback to index.jsx. And then in index.jsx we grab scrollAndFilter.jsx with createRef() (learn more) scroll to it with something like window.scrollTo(0, this.myRef.current.offsetTop) as well as pass a new prop to it e.g. Tab='2'.

Do we like it? Well, it would do the job. But sending data from a child (myCoolCard.jsx) to a parent (index.jsx) via callbacks is something that soon enough will end up in a massive unreadable codebase + it gets really dependant on the parent. What if you want to reuse myCoolCard.jsx in a similar way somewhere else in the app. You will have to completely repeat you logic (like we never heard of DRY).

  • Another way is to throw in Redux and instead of a callback save data onClick (Tab='2') in the store. And then get all components including parent accessing it and adjusting logic based on this (e.g. listening to it and filtering/scrolling accordingly). But personally, it is quite debatable whether this kind of data should be ever saved in Redux store. Its just too.. well, exclusive. Still thats better than callbacks.

Let me just cut it short as it already sounds like we are overkilling something here.

We could update the url and get scrollAndFilter.jsx listen to it to scroll & filter itself.
Oh wait, there it is, doesn't React Router solves it already?

Should do, so we could just use something like <Link to="index#about">Home</Link>
and just scroll to the hash (#about or in our case scrollAndFilter) - awesome! πŸ”₯

..Would be if it worked πŸ€·β€β™‚οΈ. You see, there is an issue with React-router that wouldn't let you do it, so you will have to install separate package which is based on custom <Link> component (meh πŸ˜„), which could be a solution, but we have bundlephobia and don't want to add extra stuff that we don't really need. Otherwise, we would use react-scroll-to in the first place.

And then we scratch head, break things and come up with a weird solution that works perfectly for our case. We don't install any extra stuff and just do it with react-router (that is something everyone use in React anyway right πŸ˜…?)

What we do is we pass state with <Link component> and set up a hash with it. The only difference the hash for us is just to make things more consistent, we don't use it for scrolling.

So,

somewhere in myCoolCard.jsx

<Link to={{ hash: 'filter', state: { tab: ' 2' } }}>click here</Link>

this would do 2 things: pass tab: ' 2' to this.props.location.state and append url with #filter

somewhere in scrollAndFilter.jsx

componentWillReceiveProps(nextProps) {
    if (nextProps.location.state && this.state.tab !== nextProps.location.state.tab) {
      if (nextProps.location.hash === '#filter') {
        this.setState({ tab: nextProps.location.state.tab.trim() });
        this.filterCardRef.current && this.filterCardRef.current.scrollIntoView({ behavior: 'smooth' });
        this.props.history.push({ hash: '' });
      }
    }
  }

Wait wait, I know. Let me explain.

What happens is we actually completely ignoring the parent. We don't even touch index.jsx here. Everything happens in the other 2 cards. First we just send state and hash from <Link> and then we listen in the other component for anything new to come in, we exclude it to only stuff that has tab and hash so anything else wouldn't trigger scrolling.

We also trim the incoming tab state so that we can scroll back click again and get automatically scrolled again (even though its already filtered to tab '2'). And also we clear hash so it works in the future link clicks without any bugs.

.scrollIntoView({ behavior: 'smooth' }) is something you attach to your ref so that it can be scrolled to easily and smoothly.

And that's actually it.

The same logic can be applied to any other cards completely isolated from the parent. In fact, you can take those 2 cards throw them into a different page and that would still work.

If anything is unclear join https://t.me/s/thefrontend

Not that it is gonna make things clearer, but hey - why not? πŸ˜‚