bvaughn / react-window

React components for efficiently rendering large lists and tabular data
https://react-window.now.sh/
MIT License
15.72k stars 782 forks source link

setting height of items for VariableSizeList itemSize is not working. (help) #665

Closed sumanthyedoti closed 3 weeks ago

sumanthyedoti commented 2 years ago

I followed this example. It is working fine. But there is a problem with my implementation and I could not see what I am missing. I am getting just the default height || 60 even when I console.log rowRef.current in MeesageRow when setting the height, it is the default 60 why is this? Where is the mistake, please? Thnaks.

Code

  const listRef = useRef(null)
  const rowHeights = useRef<Record<number, number>>({})
  const { data: messages } = useQueryChannelMessages(channelId)
  //...
    useEffect(() => {
    if (messages && messages.length > 0) {
      listRef.current?.scrollToItem(messages.length - 1, 'end')
    }
  }, [messages])

   const getRowHeight = useCallback((index: number) => {
      return rowHeights.current[index] || 60
    }, [])

    const setRowHeight = useCallback((index: number, size: number) => {
      listRef.current?.resetAfterIndex(0)
      rowHeights.current = { ...rowHeights.current, [index]: size }
    }, [])

   //...
    return (
      <article
        className={`h-full overflow-y-auto
          flex flex-col main-view-padding
        `}
      >
        <AutoSizer>
          {({ height, width }: { height: number; width: number }) => {
            return (
              <List
                height={height}
                width={width}
                ref={listRef}
                itemCount={messages.length}
                itemSize={getRowHeight}
              >
                {({ index: i, style }) => {
                  return (
                    <MessageRow
                      message={messages[i]}
                      previousMessageDate={
                        messages[i - 1] ? messages[i - 1].created_at : null
                      }
                      index={i}
                      style={style}
                      setRowHeight={setRowHeight}
                    />
                  )
                }}
              </List>
            )
          }}
        </AutoSizer>
      </article>
    )

MessageRow

//...
    const rowRef = useRef<HTMLDivElement>(null)
    const { data: users } = useQuerySpaceUsers(spaceId)
    useEffect(() => {
      if (rowRef.current) {
         console.log(rowRef.current.clientHeight)
        setRowHeight(i, rowRef.current.clientHeight)
      }
      // eslint-disable-next-line
    }, [rowRef])

   return (
            <div ref={rowRef} style={style}>
            //...
            </div>
    )
//...
danmolitor commented 1 year ago

I had this same issue. I'm using a table with table rows in my example, but it would seem the problem has the same cause.

My solution was to add a span wrapping the content of the row, and have that be the ref to set the height based on.

batu0b commented 1 year ago

I am also suffering from the same problem. Can you show your solution with an example?

danmolitor commented 1 year ago

@batu0b Sure.

I followed this example: https://codesandbox.io/s/react-chat-simulator-yg114?file=/src/ChatRoom/Room/Messages/Messages.js:569-1341

But I'll adjust it for my specific use case which was using a table:

  function TableRow({ index, style }) {
    const rowRef = useRef({});

    useEffect(() => {
      if (rowRef.current) {
        setRowHeight(index, rowRef.current.clientHeight);
      }
      // eslint-disable-next-line
    }, [rowRef]);

    return (
      <tr style={style}>
        <td ref={rowRef}>
          {messages[index].message}
        </td>
      </tr>
    );
  }

The TD element wasn't growing to the height of the content, so I ended up doing this:

  function Row({ index, style }) {
    const rowRef = useRef({});

    useEffect(() => {
      if (rowRef.current) {
        setRowHeight(index, rowRef.current.clientHeight);
      }
      // eslint-disable-next-line
    }, [rowRef]);

    return (
      <tr style={style}>
        <td>
          <span ref={rowRef}>
            {messages[index].message}
          </span>
        </td>
      </tr>
    );
  }

The SPAN element will be the height of the content, so we will set the ref there, and react-window should work properly.

batu0b commented 1 year ago

@danmolitor

Thank you very much for your reply, now my code is working without errors

ugoi commented 10 months ago

For me it worked if I passed the messages as an array like in this example but if I pass the messages as children and convert the children to an array it doesn't work. It defaults to the default value of 82. Here is my github repo:

// Modules
import React, { useEffect, useRef, FC, ReactNode, Children } from "react";
// Components
import { VariableSizeList as List } from "react-window";
import AutoSizer, { Size } from "react-virtualized-auto-sizer";
// Styles
import styles from "./Messages.css";
import { Box } from "@mui/material";

// Define the shape of a message
type Message = {
  sender: string;
  message: string;
};

// Define the props for the Messages component
interface MessagesProps {
  messages?: Message[];
  children?: ReactNode;
}

// The Messages functional component with types for props
const Messages: FC<MessagesProps> = ({ children, messages }) => {
  // References with types
  const listRef = useRef<List>(null);
  const rowHeights = useRef<{ [key: number]: number }>({});
  // Convert children to an array so we can access by index.
  const childrenArray = Children.toArray(children);

  useEffect(() => {
    if (childrenArray.length > 0) {
      scrollToBottom();
    }
    // eslint-disable-next-line
  }, [childrenArray]);

  function getRowHeight(index: number): number {
    return rowHeights.current[index] + 8 || 82;
  }

  interface RowProps {
    index: number;
    style: React.CSSProperties;
  }

  const Row: FC<RowProps> = ({ index, style }) => {
    const rowRef = useRef<HTMLDivElement>(null);

    useEffect(() => {
      if (rowRef.current) {
        setRowHeight(index, rowRef.current.clientHeight);
      }
      // eslint-disable-next-line
    }, [rowRef]);

    return (
      <div style={style} ref={rowRef}>
        {childrenArray[index]}
      </div>
    );
  };

  function setRowHeight(index: number, size: number): void {
    if (listRef.current) {
      listRef.current.resetAfterIndex(0);
      rowHeights.current = { ...rowHeights.current, [index]: size };
    }
  }

  function scrollToBottom(): void {
    if (listRef.current) {
      listRef.current.scrollToItem(childrenArray.length, "end");
    }
  }

  return (
    <Box
      sx={{ overflowY: "scroll", flexGrow: 1, p: 1 }}
      data-testid="message-list"
    >
      <AutoSizer style={styles.messagesContainer as React.CSSProperties}>
        {({ height, width }: Size) => (
          <List
            className="List"
            height={height}
            itemCount={childrenArray.length}
            itemSize={getRowHeight}
            ref={listRef}
            width={width}
          >
            {Row}
          </List>
        )}
      </AutoSizer>
    </Box>
  );
};

export default Messages;
ugoi commented 10 months ago

I used react-virtuoso instead of react-window and it woks with children array:

import React, { useCallback } from "react";
import { Virtuoso } from "react-virtuoso";
import { Box } from "@mui/material";

interface MessageListProps {
  children: React.ReactNode;
}

const MessagesList: React.FC<MessageListProps> = ({ children }) => {
  // Convert ReactNode children to an array of elements.
  const childrenArray = React.Children.toArray(children);

  const onFollowOutputHandler = useCallback((atBottom: any) => {
    if (atBottom || true) {
      return "auto";
    } else {
      return false;
    }
  }, []);

  return (
    <Box sx={{ flexGrow: 1, p: 1}} data-testid="message-list">
      <Virtuoso
        data={childrenArray}
        followOutput={onFollowOutputHandler}
        style={{ height: "100%", width: "100%" }}
        totalCount={childrenArray.length}
        initialTopMostItemIndex={childrenArray.length - 1} // Set initialTopMostItemIndex to the last item
        itemContent={(index) => (
          <Box sx={{ pb: 1 }}>{childrenArray[index]}</Box>
        )}
      />
    </Box>
  );
};

export default MessagesList;