Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

react custom scrollbar with react-window #110

Closed
Rahul-Sagore opened this issue Dec 17, 2018 · 38 comments
Closed

react custom scrollbar with react-window #110

Rahul-Sagore opened this issue Dec 17, 2018 · 38 comments
Labels
💬 question Further information is requested

Comments

@Rahul-Sagore
Copy link

I am using react-custom-scrollbar and would like to integrate it with FixedSizeList.

I have checked the solution on this issue on react-virtualized: bvaughn/react-virtualized#692 (comment)

But the code is throwing error: Uncaught TypeError: Cannot read property 'handleScrollEvent' of undefined on scroll, in this function:

  handleScroll = ({ target }) => {
    const { scrollTop, scrollLeft } = target;

    const { Grid: grid } = this.List;

    grid.handleScrollEvent({ scrollTop, scrollLeft });
  }

I have added ref={ instance => { this.List = instance; } } on fixedSixe<List component.

@bvaughn
Copy link
Owner

bvaughn commented Dec 17, 2018

This library and react-virtualized are completely different implementations. General techniques you see on react-virtualized issues may often provide useful hints as to how to approach something, but specific implementation details like this are not applicable. (In react-virtualized, List decorates a Grid component. In react-window they are separate components, for better performance and size.)

I don't know if something like react-custom-scrollbar would work with react-window since I've never tried it. It's not something I'm very interested in supporting to be honest, since I think custom scrollbars are generally a bad idea because of how they impact performance. So I'm going to close this issue.

But we can continue to chat on it if you have follow up questions 😄

@bvaughn bvaughn closed this as completed Dec 17, 2018
@bvaughn bvaughn added the 💬 question Further information is requested label Dec 17, 2018
@Rahul-Sagore
Copy link
Author

I'll figure out something about this, I am interested in using react-window because it's lightweight and solves my issue. Even I don't like to use custom scrollbar, but it's not in my hand, we need custom design for scrollbar.

I'll use react-virtualized, in worst-case.
Thanks :)

@lottamus
Copy link

lottamus commented Jan 4, 2019

@Rahul-Sagore did you end up getting custom scrollbars working with react-window?

@Rahul-Sagore
Copy link
Author

No, did not get time for that. Need to check and figure out.

@simjes
Copy link

simjes commented Feb 9, 2019

Not sure if @bvaughn approves this, but it seems to work: https://codesandbox.io/s/00nw2w1jv

@piecyk
Copy link

piecyk commented Mar 3, 2019

Better approach would be to pass Scrollbars as outerElementType

const CustomScrollbars = ({ onScroll, forwardedRef, style, children }) => {
  const refSetter = useCallback(scrollbarsRef => {
    if (scrollbarsRef) {
      forwardedRef(scrollbarsRef.view);
    } else {
      forwardedRef(null);
    }
  }, []);

  return (
    <Scrollbars
      ref={refSetter}
      style={{ ...style, overflow: "hidden" }}
      onScroll={onScroll}
    >
      {children}
    </Scrollbars>
  );
};

const CustomScrollbarsVirtualList = React.forwardRef((props, ref) => (
  <CustomScrollbars {...props} forwardedRef={ref} />
));

// ...

<FixedSizeList
  outerElementType={CustomScrollbarsVirtualList}
  {...rest}
/>

example https://codesandbox.io/s/vmr1l0p463

@bvaughn
Copy link
Owner

bvaughn commented Mar 3, 2019

I love how many advanced things are possible using outerElementType or innerElementType

@elixirdada
Copy link

I am trying to detect the scroll position at the bottom of react-window.
Is it possible? Do you have any ideas for that? I'd like to have codesandbox example.

@bvaughn
Copy link
Owner

bvaughn commented Mar 26, 2019

You should be able to use the available ref props to ask the list for its scrollHeight @rufoot.

@elixirdada
Copy link

I have tried to use scrollHeight in the props. But I think there isn't scrollHeight property in that.
@bvaughn Do you have any idea about that?

https://codesandbox.io/s/github/bvaughn/react-window/tree/master/website/sandboxes/scrolling-to-a-list-item

scrollToRow200Auto = () => { this.listRef.current.scrollToItem(200); console.log("current position = ", this.listRef.current.props.scrollHeight) };

current position = undefined

@piecyk
Copy link

piecyk commented Mar 27, 2019

@rufoot something like this https://codesandbox.io/s/4zjwwq98j4 ?

@elixirdada
Copy link

https://codesandbox.io/embed/jzo2lool2y

When you scroll down at the bottom of react-window, then you can see one alert.

@jancama2
Copy link

jancama2 commented May 2, 2019

@piecyk I have tried your solution with outerElementType and it is super laggy, have you battletested it?:D

@jancama2
Copy link

jancama2 commented May 2, 2019

I have tried it with react-scrollbars-custom. Its is less laggy but still laggy. Have someone had any issues with lags using custom scrollbars?

@piecyk
Copy link

piecyk commented May 2, 2019

@jancama2 didn't battletested it 😂Can you share Code Sandbox when it gets laggy?

@ChristopherHButler
Copy link

@rufoot something like this https://codesandbox.io/s/4zjwwq98j4 ?

I am using your implementation but I get an error saying forwardedRef is not a function.

The problem in my app that I am trying to solve is that I have a large list of items and when I click on one, the app will redirect to another route so I need to keep track of the scroll position (probably in redux) and then set the scroll position after the route change.

@ranihorev
Copy link

ranihorev commented Aug 20, 2019

@piecyk have you tried combining it with the InfiniteLoader from react-window-infinite-loader.
I'm using react-scrollbars-custom and it seems to be breaking the infinite loader :(

My code:

<InfiniteLoader isItemLoaded={isItemLoaded} itemCount={items.total} loadMoreItems={loadMoreItems}>
      {({
        onItemsRendered,
        ref,
      }: {
        onItemsRendered: (props: ListOnItemsRenderedProps) => any;
        ref: React.Ref<any>;
      }) => (
        <List
          height={TABLE_HEIGHT}
          width={width}
          itemCount={items.total}
          itemSize={ROW_HEIGHT}
          onItemsRendered={onItemsRendered}
          itemData={{ items }}
          ref={ref}
          outerElementType={Scrollbar}
        >
          {Row}
        </List>
      )}
    </InfiniteLoader>

Update: I don't think that it's related to the infinite scroll component. I can't get it to work with the custom scrollbar.

I'd appreciate if someone can share the implementation for react-scrollbars-custom with me

@ChristopherHButler
Copy link

@ranihorev do you need to use that package? The code pen @piecyk shared above uses react-custom-scrollbars and works well (except when creating a ref, which for some reason is null in my code)

@ChristopherHButler
Copy link

ChristopherHButler commented Aug 21, 2019

Ok I need to ask again. @piecyk I am using your code sample from above (where you replied to @simjes ). instead of using a fixed sized list though, I am using the auto sizer to wrap a VariableSizeList and specify the outerElementType. Something like this:
<TreeRoot onDragOver={e => this.onDragOver(e)} > <AutoSizer> {({ height, width }) => ( <List treeRef={this.props.treeRef} width={width} height={height} itemSize={index => rowHeights[index]} itemCount={nodeTree.length} itemData={{ nodeTree, expandedNodes, nodesFetching, nodesFetchingFailed, selectedNode, selectedNodeId, resetRowHeight: this.resetRowHeight, }} outerElementType={TreeScrollbar} outerRef={this.props.outerRef} onScroll={({ scrollOffset, scrollUpdateWasRequested }) => { if (scrollUpdateWasRequested) { console.log('scrollOffset: ', scrollOffset); } }} > {Node} </List> )} </AutoSizer> </TreeRoot>

I implement the CustomScrollbars and CustomScrollbarsVirtualList the same as you have however the ref is always null. I have tried creating the ref in the component I am using the AutoSizer as well as in it's parent but it's always null. I would really appreciate some help understanding why. As I mentioned above, I need to keep track of the scroll position and be able to set it on a route change in my app so the list doesn't jump back to the top.

@ranihorev
Copy link

@ChristopherHButler I'm already using that package multiple times in my code, so I'd rather keep using the same package. I solution is pretty similar however it doesn't fetch new data ptoperly

@piecyk
Copy link

piecyk commented Aug 21, 2019

@ChristopherHButler really hard to say without any Code Sandbox example of the issue, can you share something?
Basic the idea here is to set the ref from react-custom-scrollbars so the react-window

AutoSizer or VariableSizeList will work with this approach, as in this example
https://codesandbox.io/s/react-window-custom-scrollbars-t4352

@piecyk
Copy link

piecyk commented Aug 21, 2019

@ranihorev can you also share Code Sandbox example?

@ranihorev
Copy link

@piecyk I create a simple (and broken) example:
https://codesandbox.io/s/bvaughnreact-window-fixed-size-list-vertical-64kzh?fontsize=14

I also tried other variations (e.g. setting the ref to be the scroller wrapper) but no success...

Thanks!

@piecyk
Copy link

piecyk commented Aug 22, 2019

@ranihorev this approach would be the same regardless of custom scroll implementation. They need to set reference to the same element that is responsible for scroll and handle that scroll event.

react-scrollbars-custom provides nicer api using render props pattern so you can do something like https://codesandbox.io/s/bvaughnreact-window-react-scrollbars-custom-pjyxs

Didn't do any profiling, hope this helps 👍

@ChristopherHButler
Copy link

@piecyk I am working with your demo here: https://codesandbox.io/s/4zjwwq98j4. I want to use the onScrollStop to dispatch an action in redux so I can set the scroll position (an idea from this thread: malte-wessel/react-custom-scrollbars#146 but onScrollStart and onScrollStop do not seem to work in my code or in your demo. Do you know why this is? Is there any way to pass props from the List to the Scrollbars?

@piecyk
Copy link

piecyk commented Aug 22, 2019

@ChristopherHButler I think your best option is to use context to pass the props, something like

https://codesandbox.io/s/bvaughnreact-window-fixed-size-list-vertical-v2-usi1m

--- Edited
If you unmount the whole list when route changes you can dispatch the action then, Last scrollOffset can be stored on ref and updated on every scroll change using the onScroll: function from VariableSizeList then you don't need to pass props to Scrollbars

@ranihorev
Copy link

@piecyk that's awesome, thanks!
btw, it seems that all you need is the onScroll function (that I missed) and there is no need to forward a ref at all.
https://codesandbox.io/s/bvaughnreact-window-react-scrollbars-custom-99dn1

@ChristopherHButler
Copy link

@piecyk sorry I should have mentioned my implementation of the VariableSizeList is wrapped in an AutoSizer and the component which renders it is a class based component so I'm not sure I can use context. Maybe this is why I cannot get onScrollStop to work?

@ChristopherHButler
Copy link

ChristopherHButler commented Aug 22, 2019

Quick update:
I added component state and set it onScroll.

`
class BaseTree extends Component {
...
state = { scrollPosition: 0, };
...
listRef = React.createRef();
outerRef = React.createRef();
...
render {
const { nodes, nodesFetching, nodesFetchingFailed, expandedNodes, selectedNode } = this.props;
return (

{({ height, width }) => (
<List
ref={this.listRef}
className="List"
outerRef={this.outerRef}
outerElementType={TreeScrollbar}
width={width}
height={height}
onScroll={({ scrollOffset }) => this.setState({ scrollPosition: scrollOffset })}
itemSize={index => rowHeights[index]}
itemCount={nodeTree.length}
itemData={{
nodeTree,
expandedNodes,
nodesFetching,
nodesFetchingFailed,
selectedNode,
selectedNodeId,
resetRowHeight: this.resetRowHeight,
}}
>
{Node}

)}

);
}}

const mapStateToProps = state => ({
scrollPosition: selectors.getTreeScrollPosition(state),
});

const mapDispatchToProps = dispatch => ({
setTreeScrollPosition: position => dispatch(actions.setTreeScrollPosition({ position })),
});

export default connect(mapStateToProps, mapDispatchToProps)(BaseTree);
`

then on componentWillUnmout I dispatch the action to set the position in redux like this:

componentWillUnmount() { this.props.setTreeScrollPosition(this.state.scrollPosition); }

my only issue now is being able to set the scroll position when the component re-mounts. I tried accessing the scrollTop method on this.listRef like this:

this.listRef.current.scrollTop() but I get an error that it is not a function. I'm not sure which property (this.listRef or this.outerRef) I can use to set the scroll position or in which method. I was thinking I could set it in the BaseTree's componentDidMount as follows:
componentDidMount() { const { scrollPosition } = this.props; if (this.listRef.current) { // console.log('initializing scroll position to : ', scrollPosition); this.listRef.current.scrollTop(scrollPosition); } }
Any help to get this working would be greatly appreciated!
edit: I'm very sorry but my code does not seem to be formatting correctly in this editor so I apologize for that :(

@piecyk
Copy link

piecyk commented Aug 22, 2019

@ranihorev forward ref to react-window is need for functions like scrollTo, scrollToItem to work

@ChristopherHButler context should work as exactly for this they are build, from docs

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

But yeah as mention before if you unmount the whole component on route change there is no need to pass the props. Would not store scrollPosition on state as you don't need it while scrolling is happening, less re-renders, store it on ref.

onScroll={({ scrollOffset }) => this.setState({ scrollPosition: scrollOffset })}

// to 

lastScrollOffsetRef = React.createRef(0);

<List
  onScroll={({ scrollOffset }) => {
    this.lastScrollOffsetRef.current = scrollOffset
  }}
  // ...rest
/>

// then in 
componentWillUnmount() { 
  this.props.setTreeScrollPosition(this.lastScrollOffsetRef.current); 
}

Regarding outerRef, it's not need in your case. listRef.current.scrollTo is the correct method you want to use. Hmm your idea to call the method in componentDidMount is correct and should work,

https://codesandbox.io/s/bvaughnreact-window-fixed-size-list-vertical-80zo7

looks like some timing problem while setting refs, maybe AutoSizer delays render of the List that makes the this.listRef.current to be undefined,

hacky option is to, break from react life cycle with setTimeout, and call the ref on next tick

componentDidMount() {
  window.setTimeout(() => {
    if (this.listRef.current) {
      this.listRef.current.scrollTo(this.props.scrollPosition);
    }
  }, 0);
}

@ranihorev
Copy link

@piecyk thanks a lot for the help, I really appreciate it :)

@ChristopherHButler
Copy link

Thanks @piecyk That seems to work. There is a slight flicker on mounting after the route change but I can live with it :) I also tried another hack which was to use the className to pass the scrollPosition and set scrollTop in the refSetter of CustomScrollbars:
if (scrollbarsRef) { forwardedRef(scrollbarsRef.view); // HACK scrollbarsRef.scrollTop(className); }
It seems to work well.
Thanks again for all your help, it's greatly appreciate!! 🙌🏻

@fourteenmeister
Copy link

fourteenmeister commented Oct 31, 2019

Unfortunately, all the examples in this issue do not work correctly (sometimes the mouse stops responding to scroll).

But this example work perfect!

@ranihorev this approach would be the same regardless of custom scroll implementation. They need to set reference to the same element that is responsible for scroll and handle that scroll event.

react-scrollbars-custom provides nicer api using render props pattern so you can do something like https://codesandbox.io/s/bvaughnreact-window-react-scrollbars-custom-pjyxs

@liran
Copy link

liran commented May 25, 2020

I have tried it with react-scrollbars-custom. Its is less laggy but still laggy. Have someone had any issues with lags using custom scrollbars?

Did you get it done? What's the final solution?

@joa-queen
Copy link

If this helps anyone, I've managed to make it work with OverlayScrollbars. You just need to pass the scroll event to the corresponding element. This is how:

const Overflow = ({ children, onScroll }) => {
  const ofRef = useRef(null);

  useEffect(() => {
    const el = ofRef.current.osInstance().getElements().viewport;

    if (onScroll) el.addEventListener('scroll', onScroll);

    return () => {
      if (onScroll) el.removeEventListener('scroll', onScroll);
    };
  }, [onScroll]);

  return (
    <OverlayScrollbarsComponent
      options={options}
      ref={ofRef}
    >
      {children}
    </OverlayScrollbarsComponent>
  );
};

And then, in your virtualized component:

<FixedSizeGrid
  {...props}
   outerElementType={Overflow}
>

I'm using this with FixedSizeGrid but it should work the same for lists.

Hope it helps.

@partha-0103
Copy link

partha-0103 commented Mar 9, 2022

@joa-queen is this working, I am getting this error in thetypescript

overlayscrollbars-react.esm.js:37 Uncaught TypeError: Cannot read properties of undefined (reading '_osInstance') at osInstance (overlayscrollbars-react.esm.js:37:1)

@foxy17
Copy link

foxy17 commented Aug 22, 2022

If this helps anyone, I've managed to make it work with OverlayScrollbars. You just need to pass the scroll event to the corresponding element. This is how:

const Overflow = ({ children, onScroll }) => {
  const ofRef = useRef(null);

  useEffect(() => {
    const el = ofRef.current.osInstance().getElements().viewport;

    if (onScroll) el.addEventListener('scroll', onScroll);

    return () => {
      if (onScroll) el.removeEventListener('scroll', onScroll);
    };
  }, [onScroll]);

  return (
    <OverlayScrollbarsComponent
      options={options}
      ref={ofRef}
    >
      {children}
    </OverlayScrollbarsComponent>
  );
};

And then, in your virtualized component:

<FixedSizeGrid
  {...props}
   outerElementType={Overflow}
>

I'm using this with FixedSizeGrid but it should work the same for lists.

Hope it helps.

For some reason with using this with lists it does not display all the elements, if I have 29 rows in the table it will only show 15 after scrolling to the bottom

@TotooriaHyperion
Copy link

TotooriaHyperion commented Jul 13, 2023

Better approach would be to pass Scrollbars as outerElementType

const CustomScrollbars = ({ onScroll, forwardedRef, style, children }) => {
  const refSetter = useCallback(scrollbarsRef => {
    if (scrollbarsRef) {
      forwardedRef(scrollbarsRef.view);
    } else {
      forwardedRef(null);
    }
  }, []);

  return (
    <Scrollbars
      ref={refSetter}
      style={{ ...style, overflow: "hidden" }}
      onScroll={onScroll}
    >
      {children}
    </Scrollbars>
  );
};

const CustomScrollbarsVirtualList = React.forwardRef((props, ref) => (
  <CustomScrollbars {...props} forwardedRef={ref} />
));

// ...

<FixedSizeList
  outerElementType={CustomScrollbarsVirtualList}
  {...rest}
/>

example https://codesandbox.io/s/vmr1l0p463

If someone has problem with
https://stackoverflow.com/questions/6421966/css-overflow-x-visible-and-overflow-y-hidden-causing-scrollbar-issue
or
https://developer.chrome.com/blog/chrome-114-beta/#alias-overflow-overlay-to-overflow-auto
this is your answer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💬 question Further information is requested
Projects
None yet
Development

No branches or pull requests