React-window: Compatibility with ScrollSync

Created on 8 Nov 2018  ·  52Comments  ·  Source: bvaughn/react-window

I have a use case where i need to create a Grid that has fixed headers that stay pinned to the top. In react-virtualized this could be accomplished by creating two grid components (one for headers, one for the grid body) and synchronizing the scroll position so the header grid's horizontal scrolling is synced up with the main grid.

I know that you are trying to keep react-window lighter weight, both in terms of bundle size and conceptual complexity, in favor of building additional functionality as separate packages. That is a direction that I agree with. I think that the ScrollSync component from react-virtualized could be extracted or adapted to work with react-window. However, in order to do so, react-window would need to accept props for scrollLeft and scrollTop so that the scroll offset of the header grid could be directly managed.

Is this a use case you would be willing to support? If not, do you have any advice for what direction i should go in to implement this myself?

Thanks for you work on this library. As someone who has used react-virtualized for a couple of years, I appreciate the simplicity of getting started with react-window and how performant it has been for me without much manual intervention.

Most helpful comment

That makes complete sense. Thanks for the suggestion! I got it working, and it's actually quite easy to set up. So easy that it definitely doesn't warrant a standalone package. Maybe an example in the docs, but that's your call I suppose. I'll leave a link to my working Code Sandbox example here in case anyone else stumbles upon this issue in the future.

https://codesandbox.io/s/y3pyp85zm1

TLDR - put a ref on the header grid, and put this on the body grid.

onScroll={({ scrollLeft }) => this.headerGrid.current.scrollTo({ scrollLeft })}

All 52 comments

First off, thanks for the kind words and positive feedback. I'm glad to hear that react-window has been working well for you so far!

I agree that a component like ScrollSync could be released as a standalone package that depends on react-window, but I wouldn't want to add it to this project since it's not core to windowing.

With regard to your specific question about scroll props, this is not a change I would be willing to make to the project because after using it with react-virtualized, I came to realize it has some serious drawbacks. I actually wrote about it on the React blog if you're curious:

https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#anti-pattern-erasing-state-when-props-change

You could use the imperative scroll API that react-window offers to achieve a similar sync behavior. You would just have to call these methods from commit lifecycles rather than by passing props.

Hopefully this makes sense but feel free to ask follow up questions if not!

That makes complete sense. Thanks for the suggestion! I got it working, and it's actually quite easy to set up. So easy that it definitely doesn't warrant a standalone package. Maybe an example in the docs, but that's your call I suppose. I'll leave a link to my working Code Sandbox example here in case anyone else stumbles upon this issue in the future.

https://codesandbox.io/s/y3pyp85zm1

TLDR - put a ref on the header grid, and put this on the body grid.

onScroll={({ scrollLeft }) => this.headerGrid.current.scrollTo({ scrollLeft })}

Thanks for the link! That's very thoughtful.

On Thu, Nov 8, 2018, 1:09 PM Reagan Keeler <[email protected] wrote:

That makes complete sense. Thanks for the suggestion! I got it working,
and it's actually quite easy to set up. So easy that it definitely doesn't
warrant a standalone package. Maybe an example in the docs, but that's your
call I suppose. I'll leave a link to my working Code Sandbox example here
in case anyone else stumbles upon this issue in the future.

https://codesandbox.io/s/y3pyp85zm1

TLDR - put a ref on the header grid, and put this on the body grid.

onScroll={({ scrollLeft }) => this.headerGrid.current.scrollTo({ scrollLeft })}


You are receiving this because you modified the open/close state.
Reply to this email directly, view it on GitHub
https://github.com/bvaughn/react-window/issues/86#issuecomment-437156749,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AABznTUunzEIs6bVQfVsz7T21L2-Pkkoks5utJ17gaJpZM4YVMd7
.

To anyone who stumbled across this, I had a scroll-synced top/right/bottom/left frozen super grid in react-virtualized that I just migrated to this lib. I'll say that the above solution is at least as performant if not better, and substantially less painful that using ScrollSync in the other library.

It also pairs quite nicely with the new useRef react hook (https://reactjs.org/docs/hooks-reference.html#useref)

const topRef = useRef();
const rightRef = useRef();
const bottomRef = useRef();
const leftRef = useRef();
...
<Grid
  onScroll={({ scrollLeft, scrollTop }) => {
    if (leftRef.current) {
      leftRef.current.scrollTo({ scrollTop });
    }
    if (rightRef.current) {
      rightRef.current.scrollTo({ scrollTop });
    }
    if (topRef.current) {
      topRef.current.scrollTo({ scrollLeft });
    }
    if (bottomRef.current) {
      bottomRef.current.scrollTo({ scrollLeft });
    }
  }}
  ...
/>

Nice! Thanks for sharing @ranneyd!

It also pairs quite nicely with the new useRef react hook (https://reactjs.org/docs/hooks-reference.html#useref)

Thank you @ranneyd

Would it be possible to share a working example?

@ranneyd thanks again, I followed your approach and the scroll sync works fine.

The hidden overflow on the scrollbars causes misalignment near the end of the grid:

gridalignement

Any suggestions on how to fix this?

CodeSandbox Example

Thanks in advance

@carlosagsmendes it's because of the scroll bar. On the left one make the height the height minus the scroll bar size. To get consistency across devices I manually hard-code the scroll bar size with CSS. If your content is so dynamic that there may or may not be a scroll bar, do a thing like "num items * size items < width (you should have all those values right there)" and then change the height depending on that.

To get consistency across devices I manually hard-code the scroll bar size with CSS.

FWIW the dom-helpers package has a handy scrollbarSize function that tells you what the width is on the current device. Might be nicer than hard-coding.

@bvaughn yes! I forgot to mention that. However, it didn't work for us. I think the problem was we were already doing hard-coded scroll bars and that confused it

Thank yo. I will give it a try!

Does this work with horizontal VariableSizeList? I tried but my browser freezes.

onScroll={({ scrollLeft }) => this.headerGrid.current.scrollTo({ scrollLeft })}

It works well with VariableSizeGrid but my header lags behind a little on scroll.

@ajaymore so re the header lagging: scroll sync struggles when you use the scroll wheel on a Mac. MacOSX has this built-in natural scrolling thing where it interpolates the scrolling to make it "smoother". The refresh rate on this is actually faster than the animation frame rate in Chrome. Thus, the element you're scrolling is going to animate before V8/ScrollSync has time to update the DOM. This is a technical limitation that I have not seen a solution to.

Fun fact: if you manually use the scroll bar in the browser (like scrolling the old fashioned way by dragging the little thing) it works totally fine. The interpolation thing is built into the scroll wheel/track pad swiping

@ajaymore so re the header lagging: scroll sync struggles when you use the scroll wheel on a Mac. MacOSX has this built-in natural scrolling thing where it interpolates the scrolling to make it "smoother". The refresh rate on this is actually faster than the animation frame rate in Chrome. Thus, the element you're scrolling is going to animate before V8/ScrollSync has time to update the DOM. This is a technical limitation that I have not seen a solution to.

Fun fact: if you manually use the scroll bar in the browser (like scrolling the old fashioned way by dragging the little thing) it works totally fine. The interpolation thing is built into the scroll wheel/track pad swiping

@ranneyd Thank you for such a quick response. I agree it's a limitation. It will work just fine on majority of the devices so not a big concern then.

@bvaughn have you ever played around with position: sticky? I haven't looked at it in a while and I know the browser support is scant, but I wonder if there's a way to leverage it in browsers where it is supported...

I have the same issue as @ajaymore, that is, syncing two components is laggy (as stated, it does work fine on manual scroll). I'm testing with Chrome on a PC so apparently that's not a Mac only issue... Has someone had success with working around this issue somehow?

@alonrbar I have the same problem on a Windows PC.

I've actually just found a workaround that works for me.
The general idea is to create a "shadow grid" that hides to original grid and steals his onScroll event and then uses it to manually scroll the original grid as well as any other grid necessary. It slows down performance a bit but keeps all grids nicely synced so you'll have to consider the trade off.

The code looks like this;

import styled from '@emotion/styled';
import * as React from 'react';
import { VariableSizeGrid, VariableSizeGridProps } from 'react-window';
import { SizeUtils } from '../utils';

export interface SyncableGridProps extends VariableSizeGridProps {
    mainGridRef: React.Ref<VariableSizeGrid>;
    shadowGridRef: React.Ref<VariableSizeGrid>;
    hideVerticalScrollbar?: boolean;
}

export class SyncableGrid extends React.PureComponent<SyncableGridProps> {
    public render() {
        const { height, width } = this.props;
        const {
            onScroll,
            mainGridRef: mainGridRef1,
            shadowGridRef: shadowGridRef1,
            ...mainProps
        } = this.props;
        const {
            children,
            style,
            overscanRowsCount,
            overscanColumnsCount,
            overscanCount,
            useIsScrolling,
            onItemsRendered,
            mainGridRef: mainGridRef2,
            shadowGridRef: shadowGridRef2,
            innerRef,
            outerRef,
            ...shadowProps
        } = this.props;
        return (
            <SyncWrapper
                style={{
                    height,
                    width
                }}
            >
                <MainGrid
                    {...mainProps}
                    style={Object.assign({}, style, {
                        overflowY: 'scroll'
                    })}
                    ref={mainGridRef1}
                />
                <GridShadow
                    {...shadowProps}
                    style={{
                        position: 'absolute',
                        top: 0,
                        left: 0
                    }}
                    ref={shadowGridRef1}
                >
                    {() => null}
                </GridShadow>
            </SyncWrapper>
        );
    }
}

// ---------------- //
//      styles      //
// ---------------- //

const SyncWrapper = styled.div`
    position: relative;
    overflow: hidden;
`;

export interface MainGridProps extends VariableSizeGridProps {
    hideVerticalScrollbar?: boolean;
}

export const MainGrid = styled(VariableSizeGrid) <MainGridProps>`
    overflow-y: scroll;
    box-sizing: content-box;
    ${props => {
        if (!props.hideVerticalScrollbar)
            return '';
        const paddingDir = (props.theme.dir === 'rtl' ? 'padding-left' : 'padding-right');
        return `${paddingDir}: ${SizeUtils.scrollbarWidth}px;`;
    }}
`;

export const GridShadow = styled(MainGrid)`
    opacity: 0;
`;

Then in another file:

<SyncableGrid
    mainGridRef={this.firstGridMain}
    shadowGridRef={this.firstGridShadow}
    onScroll={this.handleFirstGridScroll}
    // other props omitted for bravity...
>
   // children omitted for bravity...
</SyncableGrid>
<SyncableGrid
    mainGridRef={this.secondGridMain}
    shadowGridRef={this.secondGridShadow}
    onScroll={this.handleSecondGridScroll}
    // other props omitted for bravity...
>
   // children omitted for bravity...
</SyncableGrid>

private handleFirstGridScroll = (e: GridOnScrollProps) => {
    const { scrollTop, scrollLeft } = e;

    // synchronize self
    if (this.firstGridMain.current) {
        this.firstGridMain.current.scrollTo({ scrollTop, scrollLeft });
    }

    // synchronize other grid
    if (this.secondGridMain.current) {
        this.secondGridMain.current.scrollTo({ scrollTop, scrollLeft });
        this.secondGridShadow.current.scrollTo({ scrollTop, scrollLeft });
    }
}

@alonrbar this is fascinating! I'll try this out soon.

It looks like the shadow grid lives above the real grid, yes? If so, click events on the "real grid" won't work will they? I suppose you could do something a little hacky with click events + x/y coords and somehow apply that to the main grid.

Also @barbalex re: fails in windows too

It might be a chrome flag actually. See if there's anything in chrome://flags. Microsoft may have also implemented something similar.

The issue certainly seems to be related to scrolling being "smoothed" faster than the browser animation frame. If you're concerned it's purely a performance/lag thing try making a very basic scroll sync implementation (where on scroll of one div sets the scroll position of another) and see if you get the same problem. Maybe even do it in pure vanilla JS to eliminate React as the culprit

Ahhh @ranneyd you're right it blocks click events... Need to do some more thinking on that...

I think the Threaded scrolling feature in chrome://flags has a big influence: turning it off makes scrolling with the mouse wheel about as smooth as scrolling with the scrollbar for me.

2019-07-15_00h03_42

@alonrbar what about this:

Add a scroll handler to the main grid. In it you do e.preventDefault(); or something to prevent the actual scrolling. Then you look at the event to figure out how much it WOULD HAVE scrolled, which this does to move the other synced stuff, but then you use that to manually scroll that same element. So instead of scrolling A, then using that info to scroll B, you intercept the scroll on A, cancel it, then use that to scroll B and A itself. Would that work?

I'm not by a computer to test. Pretty hacky but I could work. @bvaughn thoughts?

@barbalex you're right, it works for me too but I can't have all my users turning chrome flags on and off 😔

@ranneyd I've just looked into it and fiddled a bit with the source code (link) but apparently you can not disable scroll events the only thing you can do is to hijack all mouse wheel, touch and keyboard events that may cause scrolling and prevent them. The link (SO) has a short code snippet for that but this makes it even more hacky so I'm still pondering about it...

@alonrbar yeah, asking users to do this will not work. You _can_ give them a direkt link if you _do_ want to try: chrome://flags/#disable-threaded-scrolling

@alonrbar eh this is not good but it's not TERRIBLE if we're just applying it to the grid we'll already hacking together.

It's just scroll wheel right? Do arrow keys make it happen also?

This is a snippet from the accepted answer here: https://stackoverflow.com/questions/4770025/how-to-disable-scrolling-temporarily

function disableScroll() {
  if (window.addEventListener) // older FF
      window.addEventListener('DOMMouseScroll', preventDefault, false);
  document.addEventListener('wheel', preventDefault, {passive: false}); // Disable scrolling in Chrome
  window.onwheel = preventDefault; // modern standard
  window.onmousewheel = document.onmousewheel = preventDefault; // older browsers, IE
  window.ontouchmove  = preventDefault; // mobile
  document.onkeydown  = preventDefaultForScrollKeys;
}

As you can see there are several events you need to handle and consider possible consequences of doing so.

For the moment, as I can not invest more time in trying to solve this right now I decided to use a non-virtual solution (which isn't perfect either but works better for my use cases). You can find the code I use here and here (it's part of library I wrote that wraps react-window).

@alonrbar yeah I agree that SO solution is pretty crummy.

I find your solution, however, very interesting. I'm going to see if I can take my own crack at it and make it work with the virtualized table.

@alonrbar so I made a non-virtualized implementation that actually uses absolute positioning. The only thing that scrolls is the outer container. On scroll it updates top/left values which are used to absolute-position the inner elements. So no scrollTo or scrollTop = ..., which I found was causing me a lot of grief. On top of that, the scroll bars are ALWAYS on the outside of the ENTIRE grid.

This thing I made can dynamically have "frozen headers" on all sides. It's a VERY rough example/poc.

Obviously it lacks virtualization which is a serious problem. Unfortunately I don't know if I can wrap Grid from this library in it, since this lib basically runs on scrolling and this eliminates the interior-grid scrolling. Not sure what the next step is.

https://codesandbox.io/embed/non-virtual-scroll-synced-table-ot467

I think the same virtualization logic that fuels this lib can be applied to this.

@ranneyd very cool implementation!

I like the dynamic grid composition :)

But most importantly the idea of using absolute positioning instead of scrolling may be the key to solving this issue. By introducing another div you've disconnected the onscroll event from the actual content scrolling which I think opens new possibilities.

If you could hook into react-window's render method you may be able to replace lines 459 and on with your implementation. Then you'll be able to hook the onscroll event and disable it's effect when required (the scrollbars will still always move but you can control the content to prevent it from changing).

@alonrbar so I actually re-implemented the virtualization algorithm. It isn't as good but looking at the source code when this Grid gets to the actual virtualization it's basically the same alg. However, the performance is actually decent. I can get to a 200x200 grid without any lag, and 500x500 with only a little bit. If my boss let's me work on this any more I'll try to do the render method implementation like you suggested, which is probably actually pretty easy.

If you're interested in the grid I made, I can post it somewhere. If you want to take it and hook it up with this lib then be my guest 😏

@ranneyd yes I would like to see the virtualization solution you've made. Don't know if I'll use it but it would certainly be interesting :)

Also, I was trying to use your code today to implement the changes to the render method as I suggested but after re-reading your code I noticed that you haven't actually disconnected the scrolling the way I thought you have. What I mean by that is that the scrollbars and the onscroll event are still inseparable from the actual content scroll. I took your idea a step further and implemented a truly disconnected scroll:

https://codesandbox.io/embed/absolute-position-scrolling-1u7vj

If you'll look at the code you'll see that if we remove the top and left properties from the Content component the content isn't scrolled even if the scrollbars do. I've also verified that this time mouse events are not blocked. 😂
I believe it is now possible to use this implementation to ignore the onscroll event and sync multiple grids manually. But that of course requires some more work so it will have to wait for next time...

This discussion got me curious, so I setup a project to compare two scroll-synced react-window Grids with a react-virtualized MultiGrid, so that I can get a sense of how perf compares:
https://github.com/bvaughn/react-window-vs-react-virtualized-synced-grids

I tried to make each usage as similar as possible. Some initial observations:

  • react-window looks noticeably slower in DEV mode but feels a bit faster/more responsive in a production build. (It's hard to rule out bias on my part here. The difference in production is smaller.)
  • react-window also does a lot more commits, since the scroll event first updates the active grid, then the passive grid via a cascading update. I made a patch to Grid to support passing a "native" (React) scroll event handler (to be called within React's batched update) rather than the current onScroll (which is called during the commit phase). This seems like a pretty promising change when I tested it locally, since it avoids separate cascading renders, so maybe I'll just change the default onScroll timing.

@bvaughn I tried making the most basic, vanilla scroll effect code, with divA -> onScroll -> setState -> ref.scrollTop and it still couldn't get around this chrome threaded-scrolling thing. I admittedly didn't circumvent react state and set ref.scrollTop within the onScroll handler, but other than that I can't think of a more basic way to do it. If you can't get onScroll -> scrollTop fast enough in the fewest possible number of steps, how do you fix it? Am I totally missing something? It seems like the grids need to not move based on scrolling (or setting scrollTop).

The goal is always for JavaScript to be as fast as possible so that it keeps up with the thread managing scrolling.

I admittedly didn't circumvent react state and set ref.scrollTop within the onScroll handler

Just to be clear, this isn't what my repo is doing either. I'm just setting the scroll offset in the passive grid in the same (React-wrapped) event handler as the active grid, so React batches their updates into a single render+commit. This probably doesn't make _much_ of a difference to be honest.

@ranneyd and @bvaughn sharing with you my observations so far:

  1. Many implementations works well enough on desktop browser, but my primary use case is actually on mobile devices and there I see tremendous performance differences.

  2. Synchronizing two simple non-virtual grids works pretty well (not 100% perfect but close enough, even on mobile). This is my naive, yet working, implementation and a test case to see it in action (yarn storybook).

  3. Using a "controlled" scroll (as I suggested in this comment) keeps both grids 100% in sync but is quite slow on desktop and totally too slow on mobile. This is my crack at it (very rough...): https://github.com/alonrbar/react-window/commit/c39ce7006dbd590d9c640e37f8a1e78826e4688e

    Even so, I'm temped to see if the "controlled" strategy can somehow work to solve this issue on a single virtual list.

  4. I was thinking, as you two had already suggested, that maybe moving the _callPropsCallbacks to the _onScroll handler directly may help to reduce the synchronization lag to minimum. EDIT - just tried it now, not really helping 😞

  5. In any way you address it a good idea IMHO is to separate the virtualization logic from the rest of the component (maybe even a hook?) so the render and onscroll stuff can be handled separately, compared easily and even switched at runtime based on the component props. It will also make it possible to export this logic and allow users to implement custom components (such as @ranneyd solution in this comment) based on the same logic.

Thoughts?

@alonrbar my solution, which does absolute positioning, so none of the grids are the ones doing the scrolling, works very well up until about 300x300 where it gets a tad sluggish (note it stays in sync 100% of the time, the scrolling just gets a little laggy). At larger sizes I think it is just processing/mapping over a big array. I think there are several optimizations
I can do, though, and I'm not entirely convinced it has anything to do with the scroll sync so much as my fairly simple virtualization implementation. I can do more caching, for instance, and I can calculate if a cell should be virtualized or not earlier to avoid calling the render function at all (and then cache that better perhaps).

I haven't tested anything on mobile. I'll provide some code for you to try out in a bit.

I really want to look at the @bvaughn test. He says hooking directly into the native scroll fixes it, you say it doesn't. I want to see for myself.

As far as pulling the virtualization logic into a hook or an independent function, it gets pretty tricky because the logic is intrinsically connected to the view. It also seems like a lot of the performance tweaks involve caching and memoization stuff that might be hard to encapsulate in one hook or function, or at least to an extent where it gets the same performance. But I'll look at what I have and see how much of the logic I can pull out.

PS:
One thing I just thought of which would probably not work is to do a denounce-like thing + css transitions. If we only execute one scroll event every 100 ms or something and animate the movement it might look better. It might also seem substantially less responsive. It's kind of like how they did World of Warcraft (if there is lag or high latency, they will make a character move in a straight line and then correct it once they got the info of where they actually went).

I really want to look at the @bvaughn test. He says hooking directly into the native scroll fixes it, you say it doesn't. I want to see for myself.

I didn't say that :smile: I just said it avoids an extra unnecessary render and DOM mutation. How much of an impact that has on actual performance is unclear to me. It does seem like an overall positive change though, regardless.

@alonrbar @bvaughn I need to actually document my code, but here's the latest version:

https://github.com/ranneyd/synced-table

@alonrbar @bvaughn so here's a fun thing I just discovered:

My solution does NOT work in Chrome 75 on a macbook screen. When my coworkers hadn't updates chrome, it worked. If they use an external monitor it works. On their laptop screen it lags.
😩

Hmm...there are some differences with external monitors, with refresh rate or scaling. When you say it "doesn't work" what do you mean, specifically?

My bad. I mean the scrolling is no longer synchronized. If you clone my repo and run it, you can compare external monitor vs laptop screen. On the monitor they are perfectly in sync. On the laptop screen the headers flicker (don't update as the same speed as the scrolling element).

I've actually given up and I'm trying it with position: sticky. It's actually working. Browser support isn't 100%, but it's actually a lot better than it was even a year ago.

There's this lib that is a non-polyfill polyfill that might work, but the browsers we target happen to support the feature.
https://github.com/dollarshaveclub/stickybits

@alonrbar @bvaughn here is the latest version. It uses position: sticky. I explain it a little in the readme. The code still needs to be documented.

https://github.com/ranneyd/sticky-table

My solution does NOT work in Chrome 75 on a macbook screen. When my coworkers hadn't updates chrome, it worked. If they use an external monitor it works. On their laptop screen it lags.
😩

@ranneyd It occurred to me today that this might be related to frame rate. The frame rate for an external monitor might be e.g. 30 fps, in which case I think the browser might dispatch fewer "scroll" events (since I think most browsers only dispatch one per frame/paint). Maybe this is why the lag is more noticeable for you on an external monitor?

@bvaughn I think that has to be it. The OS SAYS it's sending 60hz and I THINK the specs of the monitor say 60hz, but it wouldn't surprise me if someone is lying 😂

@ranneyd I'm sorry to say that I'm not very available right now so I couldn't take a proper look at your solution but from a short glance I took I noticed it looks pretty neat and that you've create a useVirtual hook and separated elegantly the logic from rendering.
I think it would be great if you could somehow create a pull request for react-window, use it there and maybe expose a renderTable method of some sort where you could plug in your render logic. This way your library could just wrap react-window instead of replacing it. I believe it would be preferred if we can leverage this approach and solve the issue inside react-window which is already pretty much battle tested and wide spread.

On another matter, I think that if scrollTo will use direct DOM manipulation (maybe in addition to setting the state but in any case before doing so) then the outcome of syncing two tables will get smoother. If I'm not mistaken @bvaughn was also suggesting something int this direction.

@ranneyd your sticky-table solution looks really great and I'd be very happy to be able to use it. Would you consider releasing a usable api for it on npm?

@bvaughn @ranneyd

So, after quite some time I recently came back to this issue. Using code and ideas from both your libraries as well as from recyclerlistview and react-virtual-grid I was finally able to achieve the behavior I wanted, that is, a high performance grid with fixed rows and columns that works well both on desktop and mobile devices (smooth scrolling without any empty/white cells).

The TLDR of it is that I use recycling together with sticky positioning and the most interesting piece of code can be found here in this method.

Credits and more on the motivation for writing yet another solution are here. Thanks!

I'll leave a link to my working Code Sandbox example here in case anyone else stumbles upon this issue in the future.
https://codesandbox.io/s/y3pyp85zm1

Very nice, until you scroll completely to the right: then the headers are misaligned with the content (by the width of the scroll vertical scroll bar).
Would you have by any chance found a solution to that?
Thanks

I'll leave a link to my working Code Sandbox example here in case anyone else stumbles upon this issue in the future.
https://codesandbox.io/s/y3pyp85zm1

Very nice, until you scroll completely to the right: then the headers are misaligned with the content (by the width of the scroll vertical scroll bar).
Would you have by any chance found a solution to that?
Thanks

Full disclosure: I've actually made my own version from scratch. It has its own virtualization based heavily on this lib. The reason I did this was because there's an issue on MacBooks and with certain Chrome flags where the animation for scrolling happens with different timing than the JS. Using ScrollSync required too many passed-around function calls and was too slow. I basically had to remake the library with sticky headers at its core (I didn't use position: sticky like I say earlier in this thread. I need to upload what I have now at some point).

That said, the way I get around this issue is either by overflow: scroll to force a scrollbar and then add that padding to the last cell, or I dynamically determine if there's a scrollbar (and what its width is) using a ref and putting an invisible div inside it, measuring the scroll bar size, and deleting the DOM node.

I have used overflow-y: overlay which makes the scrollbar overlay. Unfortunately it only works for webkit browsers.

Ok thanks for the info. It really seems that sticky headers are such a common need, that just adding the sticky first row option to react-window would make it a replacement of react-virtualized in many more cases. Don't you think so?

@ranneyd thanks again, I followed your approach and the scroll sync works fine.

The hidden overflow on the scrollbars causes misalignment near the end of the grid:

gridalignement

Any suggestions on how to fix this?

CodeSandbox Example

Thanks in advance

Late to the party, but if you simply add one row to the leftRef and put its overflow to hidden, you basically solve the pb. (I did it also not to have to sync the leftRef -> main Grid

  const headerRef = React.useRef();
  const leftRef   = React.useRef();

  return <Box classes={{
            root:classes.tableContainer
          }}>
      <AutoSizer>
        {({ height, width }) => (<>
        {/*---------------- LA TABLE -------------*/}
          <Grid
            columnCount={1000}
            columnWidth={100}
            height={height}
            rowCount={1000}
            rowHeight={35}
            width={width}
            onScroll={({ scrollLeft, scrollTop }) => {
              if (leftRef.current) {
                leftRef.current.scrollTo({ scrollTop });
              }
              if (headerRef.current) {
                headerRef.current.scrollTo({ scrollLeft });
              }
            }}
          >
            {({ columnIndex, rowIndex, style }) => (
              <Box style={style} classes={{root:classes.cell}}>
                Item {rowIndex},{columnIndex}
              </Box>
            )}
          </Grid>
          {/*---------------- HEADER -------------*/}
          <Grid
            ref={headerRef}
            outerElementType={React.forwardRef((props, ref) => (
              <div ref={ref}  {...props} style={{...props.style,position:"absolute",overflow:"hidden",top:0,right:0,left:150}} />
            ))}
            columnCount={1001}  /*columns count +1 for scroll problems*/
            columnWidth={100}
            height={60}
            rowCount={1}
            rowHeight={60}
            width={width}
          >
            {({ columnIndex, rowIndex, style }) => (
              <Box style={style} classes={{root:classes.headerCell}}>
                Header {rowIndex},{columnIndex}
              </Box>
            )}
          </Grid>  
          {/*---------------- LEFT COL -------------*/}
          <Grid
            ref={leftRef}
            outerElementType={React.forwardRef((props, ref) => (
              <div ref={ref}  {...props} style={{...props.style,position:"absolute",overflow:"hidden",top:60,left:0}} />
            ))}
            columnCount={1}
            columnWidth={150}
            height={height}
            rowCount={251} /** add 1 for scroll problems at the end */
            rowHeight={140}
            width={150}
          >
            {({ columnIndex, rowIndex, style }) => (
              <Box style={style} classes={{root:classes.headerCell}}>
                Left {rowIndex},{columnIndex}
              </Box>
            )}
          </Grid>  
        </>)}
      </AutoSizer>
    </Box>
Was this page helpful?
0 / 5 - 0 ratings