Does react-virtual work with an infinite feed?

336 Views Asked by At

We have a log viewer that is fed from SignalR - basically a live view of logs coming in from a set of services and websites. The log viewer bogs down as more and more dom elements are created, so I was hoping I could use a virtualization library to relieve that pressure. I am trying react-virtual right now, but it doesn't appear my use case works. I've based the following POC on the React examples in the docs.

import { Button, ButtonGroup, CardBody, CardHeader, Icon, Input } from 'our-common-lib';
import * as signalR from '@microsoft/signalr';
import { useVirtualizer } from '@tanstack/react-virtual';
import React, { useCallback, useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import Constants from '../../constants';
import { IApplicationState } from '../../store';
import { LogRow } from './LogRow';

const CustomCard = styled.div`
    width: 96%;
    margin: 30px auto 50px auto;
    position: relative;
    display: flex;
    flex-direction: column;
    min-width: 0;
    word-wrap: break-word;
    background-clip: border-box;
    border-radius: 4px;
    background-color: #ffffff !important;
    color: #444444 !important;
`;

const ButtonsContainer = styled.div`
    float: right;
`;

const InputContainer = styled.span`
    margin-left: 15px;
`;

const StyledCardBody = styled(CardBody)`
    height: 700px;
    overflow: auto;
`;

export default function ViewLogs() {
    const [autoScroll, setAutoScroll] = React.useState(false);
    const allRows = useSelector((state: IApplicationState) => state.logStream.messages || []);
    const user = useSelector((state: IApplicationState) => state.oidc.user);
    const dispatch = useDispatch();
    const parentRef = useRef<HTMLDivElement>(null);

    const scrollCount = allRows.length - 1;

    const rowVirtualizer = useVirtualizer({
        count: allRows.length,
        getScrollElement: () => parentRef.current,
        estimateSize: () => 35,
        overscan: 5,
    });

    const openConnection = useCallback(() => {
        const hubConnection = new signalR.HubConnectionBuilder()
            .withUrl(Constants.API_SIGNALR_URL, {
                accessTokenFactory: () => user?.access_token,
            } as signalR.IHttpConnectionOptions)
            .build();

        hubConnection.serverTimeoutInMilliseconds = Constants.SignalRServerTimeoutInMilliseconds;
        hubConnection.keepAliveIntervalInMilliseconds = Constants.SignalRKeepAliveIntervalInMilliseconds;
        hubConnection.start();

        hubConnection.on('logStreamMessage', (receivedMessage: string) => {
            dispatch({ type: 'logStream/receiveMessage', payload: receivedMessage });
        });

        return hubConnection;
    }, [dispatch, user]);

    useEffect(() => {
        const hubConnection = openConnection();

        return () => {
            hubConnection.stop();
        };
    }, [openConnection]);

    useEffect(() => {
        if (autoScroll) {
            // eslint-disable-next-line no-console
            console.log(`scrolling to ${scrollCount}`);
            rowVirtualizer.scrollToIndex(scrollCount);
        }
    }, [autoScroll, rowVirtualizer, scrollCount]);

    const rows = rowVirtualizer.getVirtualItems();

    return (
        <CustomCard>
            <CardHeader size='md'>
                <InputContainer>
                    <Input size='md' id='correlationid-filter' placeholder='CorrelationId Filter' />
                </InputContainer>
                <ButtonsContainer>
                    <ButtonGroup type='column'>
                        <Button id='goto-top' onClick={() => rowVirtualizer.scrollToIndex(0)} color='primary'>
                            Start
                        </Button>
                        <Button id='goto-bottom' onClick={() => rowVirtualizer.scrollToIndex(scrollCount)} color='primary'>
                            End
                        </Button>
                        {autoScroll ? (
                            <Button id='pause-scroll' onClick={() => setAutoScroll(false)} color='primary'>
                                || Pause scroll
                            </Button>
                        ) : (
                            <Button id='auto-scroll' onClick={() => setAutoScroll(true)} color='primary'>
                                <Icon name='caret-right' />
                                Auto Scroll
                            </Button>
                        )}
                    </ButtonGroup>
                </ButtonsContainer>
            </CardHeader>
            <StyledCardBody>
                <div ref={parentRef}>
                    <div
                        style={{
                            height: `${rowVirtualizer.getTotalSize()}px`,
                            width: '100%',
                            position: 'relative',
                        }}
                    >
                        {rows.map((virtualRow) => {
                            const text = JSON.parse(allRows[virtualRow.index]);

                            return (
                                <div
                                    key={virtualRow.index}
                                    className={virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven'}
                                    style={{
                                        position: 'absolute',
                                        top: 0,
                                        left: 0,
                                        width: '100%',
                                        height: `${virtualRow.size}px`,
                                        transform: `translateY(${virtualRow.start}px)`,
                                    }}
                                >
                                    <LogRow logMessage={text} rowNumber={virtualRow.index} />
                                </div>
                            );
                        })}
                    </div>
                </div>
            </StyledCardBody>
        </CustomCard>
    );
}

What I'm seeing is that a row is created in the dom for every item coming in from the feed. The scrolling doesn't work at all.

I'm guessing this is because I'm giving the useVirtualizer hook a new total count on every render (I'm chunking results in my Redux thunk).

Has anyone come up with a solution using react-virtual that works with an "infinite" feed?

0

There are 0 best solutions below