I am trying to use react-virtualized to virtualize a list where some rows have varying heights, and also the list takes up all the space in the parent. I am trying to accomplish this with the CellMeasurer, AutoSizer and List components.
My package versions are as follows:
react: "16.8.6"
react-dom: "16.8.6"
react-virtualized: "^9.21.1"
import React, { PureComponent } from 'react';
import 'react-virtualized/styles.css';
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
import { CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
class Table extends PureComponent {
rowRenderer = ({ index, style, key }) => {
return (
<CellMeasurer
cache={this.cache}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}
>
<div style={style} key={key}>
content
</div>
</CellMeasurer>
);
}
cache = new CellMeasurerCache({
defaultHeight: 24,
fixedWidth: true,
});
renderAutoSizerContent = () => {
return this.RenderList;
}
RenderList = ({ height, width }) => {
return (<List
items={this.props.items}
width={width}
height={height}
rowCount={this.props.items.length}
rowHeight={this.cache.rowHeight}
rowRenderer={this.rowRenderer}
deferredMeasurementCache={this.cache}
/>
);
}
render() {
return (
<AutoSizer
items={ this.props.items}
>
{this.renderAutoSizerContent()}
</AutoSizer>
);
}
}
export default Table;
After the initial render everything looks like this. For some reason the top attribute is 0 for every element in the array:
After scrolling or triggering a rerender the items seem to get their top property and the following renders. In the actual code some of my elements have height variance, but the height seems to default to the defaultHeight I give to the CellMeasurerCache constructor anyway.
How do I get the initial render to have top property for each element, and how do I get the heights to calculate correctly? What am I doing wrong in the code I have shown here?
In your rowRenderer component, you provided parent prop to CellMeasurer but parent is undefined.
You get parent from rowRenderer as written in the docs: https://github.com/bvaughn/react-virtualized/blob/master/docs/List.md#rowrenderer
So your rowRenderer component should be:
rowRenderer = ({ index, style, key, parent }) => {
return (
<CellMeasurer
cache={this.cache}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}
>
<div style={style} key={key}>
{items[index]}
</div>
</CellMeasurer>
);
}
You can also check this code sandbox with working example: https://codesandbox.io/s/material-demo-yw1k8?fontsize=14
Related
How I want to setup my component hierarchy to be setup like this:
<Page>
<Page.Section>
<Page.Item/>
</Page.Section>
</Page>
How do I reference the nested children of children components (aka Page.Item) to put in a different location because I don't want specific styling to effect it? See Code Comments. Due to the styling, I do not want to change it.
How do you only allow Page.Section and Page.Item to be used in Page Component and not standalone?
Page.js
const Page = ({ children }) => {
return (
<>
<div
className='bg-gray-100 border-b-2 flex flex-col top-0 fixed w-screen transition-height duration-300 mt-16'
>
<SomeComponent/>
// Section Children I want here so it takes styling
{children}
</div>
// Item Children I want here so it does not take styling, but I want the Item Children to always be nested in The Page.Section Component.
{children.children?}
</>
);
};
// I want his Section element to be
const Section = ({ children }) => {
return (
<>
<ConfiguratorTab/>
{children}
</>
);
};
const Item = () => {
return (
<>
<Form/>
</>
);
};
Page.Section = Section;
Page.Item = Item;
export default Page;
children returns an object with the following keys and values.
type: ƒ Section() {}
key: null
ref: null
props: Object
_owner: FiberNode
_store: Object
The props object holds the prop values passed to the current(parent) component. So the children of children component could be accessed using props. And since Item is a child of Page (although not a direct child), you need not include it again in the Section component, if you don't want certain styles to be applied to it. This could be accomplished by removing children from Section.
const Section = ({ children }) => {
return (
<>
<ConfiguratorTab/>
{//Don't render any children elements here.
}
</>
);
};
const Page = ({ children }) => {
return (
<>
<div
className='bg-gray-100 border-b-2 flex flex-col top-0 fixed w-screen transition-height duration-300 mt-16'
>
<SomeComponent/>
// Section Children I want here so it takes styling
{children}
</div>
// Item Children I want here so it does not take styling, but I want the Item Children to always be nested in The Page.Section Component.
{children.props.children}
</>
);
};
In my react project I am using react-window package to render nested lists. Each parent FixedSizeList row renders a component which uses another FixedSizeList. Parent List doesn't have more than 14 rows at the moment. But the child List may contain upto 2000 rows. Now my problem is, when I try to scroll through the parent List, all the child list items in the viewport seem to re rendering. This is a little bit problematic for me because in my child list item I am using d3js to draw bar chart with transition effect. So these unnecessary re rendering is giving a overall weird UI. Can anyone help me how can I stop these unnecessary renders.
Here is codesandbox link to a very simple example of my problem.
Please open the console log. After initial load the topmost log should be like this: initial console log.
Then if you clear the console and scroll the parent list, you will see log like this: console log after parent scrolling. Here you can see that the child list items of child list 0 is re rendering which is not needed for me.
Can anyone give me a solution that can stop these re rendering?
*P.S. I am not using memo since every row is updating the dom on its own.
Edit
I think this problem would solve if the parent list would stop propagating scroll event to child. I tried to add event.stopPropagation() and event.stopImmediatePropagation() in the parent list row but the output was the same as earlier.
We can use memo to get rid of components being re-rendered unnecessarily for same set of props. And use useCallback to prevent re-creation of a function and thus secure child components being re-rendered. Applying those, we can get this solution:
import "./styles.css";
import { FixedSizeList as List } from "react-window";
import { memo, useCallback } from "react";
const Row = memo(({ index: parentIndex, style: parentStyle }) => {
console.log("rendering child list", parentIndex);
const InnerRow = useCallback(({ index, style }) => {
console.log("rendering child list item", index, "of parent ", parentIndex);
return <div style={style}>Child Row {index}</div>;
}, []);
return (
<div style={parentStyle}>
<List height={200} itemCount={1000} itemSize={35} width={270}>
{InnerRow}
</List>
</div>
);
});
const Example = () => {
console.log("rendering parent list");
return (
<List height={400} itemCount={16} itemSize={300} width={300}>
{Row}
</List>
);
};
export default function App() {
return (
<div className="App">
<Example />
</div>
);
}
although the above code works fine, it can be optimized more if we use areEqual method from react-window as react memo dependency. And for more if we want to use other react hooks inside InnerRow component, we must add a middleware component of InnerRow. The full example is given below:
import { FixedSizeList as List, areEqual } from "react-window";
import { memo, useCallback } from "react";
const Row = memo(({ index: parentIndex, style: parentStyle }) => {
console.log("mounting child list", parentIndex);
const data = new Array(15).fill(new Array(500).fill(1));
const InnerRowCallback = useCallback(
({ index, style }) => {
return <InnerRow index={index} style={style} />;
},
[data]
);
const InnerRow = ({ index, style }) => {
console.log("mounting child list item", index, "of parent ", parentIndex);
return <div style={style}>Child Row {index}</div>;
};
return (
<div style={parentStyle}>
<List height={200} itemCount={1000} itemSize={35} width={270}>
{InnerRowCallback}
</List>
</div>
);
}, areEqual);
const Example = () => {
console.log("mounting parent list");
return (
<List height={400} itemCount={16} itemSize={300} width={300}>
{Row}
</List>
);
};
export default function App() {
return (
<div className="App">
<Example />
</div>
);
}
Here I am passing data array as useCallBack dependency because I want to re render the InnerRow component if data gets changed.
I have a dynamic component in which I pass in children as prop.
So the props look something like:
interface Props {
...some props
children: React.ReactNode
}
export default Layout({...some props, children}: Props) {...}
I need to access the size of the children elements (height and width), in the Layout component. Note that the children are from completely different components and are non-related.
I can use the Layout component as follow:
<Layout ...some props>
<Child1 /> // I need to know the height and width of this child
<Child2 /> // as well as this child
<Child3 /> // and this child.
</Layout>
How can I do so dynamically? Do I somehow have to convert ReactNode to HTMLDivElement? Note that there is no way I can pass in an array of refs as a prop into Layout. Because that the pages which use Layout are dynamically generated.
Since many doesn't really understand what I meant by dynamically generated. It means that the pages which are using the Layout component can pass in x amount of children. The amount of children is unknown but never 0.
You can achieve this by using React.Children to dynamically build up a list of references before rendering the children. If you have access to the children element references, you can follow the below approach. If you don't then you can follow the bit at the bottom.
You have access to the children element references
If the children components pass up their element reference, you can use React.Children to loop through each child and get each element reference. Then use this to perform calculations before the children components are rendered.
i.e. This is a very simple example on how to retrieve the references and use them.
interface LayoutWrapperProps {
onMount: () => void;
}
const LayoutWrapper: React.FC<LayoutWrapperProps> = ({ onMount, children }) => {
React.useEffect(() => {
onMount();
}, [onMount]);
return <>{children}</>;
};
const Layout: React.FC = ({ children }) => {
const references = React.useRef<HTMLElement[]>([]);
React.useEffect(() => {
references.current = [];
});
function getReference(ref: HTMLElement) {
references.current = references.current.filter(Boolean).concat(ref);
}
function getHeights() {
const heights = references.current.map((ref) =>
ref?.getBoundingClientRect()
);
console.log(heights);
}
const clonedChildren = React.Children.map(children, (child) => {
return React.cloneElement(child as any, {
ref: getReference
});
});
return <LayoutWrapper onMount={getHeights}>{clonedChildren}</LayoutWrapper>;
};
If you don't have access to the children element references
If the children components aren't passing up an element as the reference, you'll have to wrap the dynamic children components in a component so we can get an element reference. i.e.
const WrappedComponent = React.forwardRef((props, ref) => {
return (
<div ref={ref}>
{props.children}
</div>
)
});
When rendering the children components, then the code above that gets the references will work:
<Layout>
<WrappedComponent>
<Child1 />
</WrappedComponent>
</Layout>
Since we don't know how your children is built, here is what I can propose you :
import React from 'react';
import { render } from 'react-dom';
const App = () => {
const el1Ref = React.useRef();
const el2Ref = React.useRef();
const [childrenValues, setChildrenValues] = React.useState([]);
React.useEffect(() => {
setChildrenValues([
el1Ref.current.getBoundingClientRect(),
el2Ref.current.getBoundingClientRect()
]);
}, []);
return (
<Parent childrenVals={childrenValues}>
<span ref={el1Ref}>
<Child value="Hello" />
</span>
<span ref={el2Ref}>
<Child value="<div>Hello<br />World</div>" />
</span>
</Parent>
);
};
const Parent = ({ children, childrenVals }) => {
React.useEffect(() => {
console.log('children values from parent = ', childrenVals);
});
return <>{children}</>;
};
const Child = ({ value }) => {
return <div dangerouslySetInnerHTML={{ __html: value }} />;
};
render(<App />, document.getElementById('root'));
And here is the repro on Stackblitz.
The idea is to manipulate how your children is built.
When wrapping my virtual list <FixedSizeList> component (react-beautiful-dnd + react-window) with an <AutoSizer> component (react-virtualized-auto-sizer) I'm receiving the following error:
react-beautiful-dnd
A setup problem was encountered.
Invariant failed: provided.innerRef has not been provided with a HTMLElement.
The error doesn't occur if I don't wrap the <FixedSizeList> component with <AutoResizer> and supply hard-coded values instead.
My program implements 2 separate, non-draggable lists that I can drag-and-drop to and from. Becasue the lists aren't draggable it's not a typical "board", but I used React-Beautiful-DND's CodeSandBox for React-Window Basic Board as a guide to make it work just as well.
List.js:
import { Draggable, Droppable } from "react-beautiful-dnd";
import { FixedSizeList } from "react-window";
import AutoSizer from "react-virtualized-auto-sizer";
import ListItem from "./ListItem";
import React, { memo, useCallback } from "react";
const List = ({
ID,
data
}) => {
const listItemRenderer = useCallback(({ data, index, style }) => {
const item = (data && data[index]);
if (!item) {
return null;
}
return (
<Draggable
key={item.ID}
draggableId={item.ID}
index={index}
>
{(provided) =>
<ListItem
data={item}
draggableProvided={provided}
virtualStyle={style}
/>
}
</Draggable>
);
}, []);
return (
<Droppable
droppableId={ID}
mode={"virtual"}
renderClone={(provided, snapshot, rubric) => (
<ListItem
data={data[rubric.source.index]}
draggableProvided={provided}
isDragging={snapshot.isDragging}
/>
)}
>
{(provided, snapshot) => {
const dataLength = (data)
? data.length
: 0;
const itemCount = (snapshot.isUsingPlaceholder)
? dataLength + 1
: dataLength;
return (
<AutoSizer> //Error here caused by wrapping <FixedSizeList> with <AutoSizer>
{({ width, height }) => (
<FixedSizeList
width={width} //AutoSizer supplied value
height={height} //AutoSizer supplied value
itemSize={100}
itemCount={itemCount}
itemData={data}
outerRef={provided.innerRef}
>
{listItemRenderer}
</FixedSizeList>
)}
</AutoSizer>
);
}}
</Droppable>
);
};
export default memo(List);
ListItem.js:
import React, { memo } from "react";
const ListItem = ({
data,
draggableProvided,
virtualStyle
}) => {
return (
<div
{...draggableProvided.draggableProps}
{...draggableProvided.dragHandleProps}
ref={draggableProvided.innerRef}
style={{
...draggableProvided.draggableProps.style,
...virtualStyle
}}
>
{data.name}
</div>
);
};
export default memo(ListItem);
Regardless of the error, everything seems to still function as it should, but I'd really like to understand the problem before moving forward.
I dug into the AutoSizer component to find the answer.
The error was logged because the children property of the AutoSizer HOC was not being rendered since the width and height values were 0. This is also why everything still functioned normally, as the width and height state values were eventually updated, but only after the initial render.
AutoSizer (index.esm.js):
// Avoid rendering children before the initial measurements have been collected.
// At best this would just be wasting cycles.
var bailoutOnChildren = false;
if (!disableHeight) {
if (height === 0) {
bailoutOnChildren = true;
}
outerStyle.height = 0;
childParams.height = height;
}
if (!disableWidth) {
if (width === 0) {
bailoutOnChildren = true;
}
outerStyle.width = 0;
childParams.width = width;
}
return createElement(
'div',
{
className: className,
ref: this._setRef,
style: _extends({}, outerStyle, style) },
!bailoutOnChildren && children(childParams)
);
Therefore, the solution is to supply defaultWidth and defaultHeight props with non-zero value to ensure that the component renders on mount, albeit with non-automated sizes:
//...
return (
<AutoSizer
defaultWidth={1}
defaultHeight={1}
>
{({ width, height }) => (
<FixedSizeList
width={width}
height={height}e
itemSize={100}
itemCount={itemCount}
itemData={data}
outerRef={provided.innerRef}
>
{listItemRenderer}
</FixedSizeList>
)}
</AutoSizer>
);
I am making this class called Scrollable which enables scrolling if the width/height of the children elements exceeds a certain value. Here is the code.
import React, { Component } from 'react';
const INITIAL = 'initial';
class Scrollable extends Component {
render() {
let outter_styles = {
overflowX: (this.props.x? 'auto': INITIAL),
overflowY: (this.props.y? 'auto': INITIAL),
maxWidth: this.props.width || INITIAL,
maxHeight: this.props.height || INITIAL,
};
return (
<div ref={el => this.outterEl = el} style={outter_styles}>
<div ref={el => this.innerEl = el}>
{this.props.children}
</div>
</div>
);
}
};
export default Scrollable;
// To use: <Scrollable y><OtherComponent /></Scrollable>
This works great. Except now I wish to add one more functionality which makes the scrollable always scroll to the bottom. I have some idea of how to do it:
this.outterEl.scrollTop = this.innerEl.offsetHeight;
But this only need to be called when this.props.children height changes. Is there any idea on how to achieve this?
Thanks in advance.
I would recommend a package element-resize-detector. It is not React-specific but you can easily build a high-order component around it or integrate your Scrollable component with it.
Now I have an idea of solving this.
Since I am using react-redux. The problem is that I could not use lifecycle hooks on this Scrollable component since this.props.children might not necessarily be changed when the content is updated.
One way to achieve this is to make Scroll component aware of the corresponding values in the redux state. So that when that relevant value is updated, we can scroll down to the bottom.
Scrollable component:
import React, { Component } from 'react';
const INITIAL = 'initial';
class Scrollable extends Component {
componentWillUpdate(){
if(this.props.autoScroll){
// only auto scroll when the scroll is already at bottom.
this.autoScroll = this.outterEl.scrollHeight - this.outterEl.scrollTop - Number.parseInt(this.props.height) < 1;
}
}
componentDidUpdate(){
if(this.autoScroll) this.outterEl.scrollTop = this.outterEl.scrollHeight;
}
render() {
let styles = {
overflowX: (this.props.x? 'auto': INITIAL),
overflowY: (this.props.y? 'auto': INITIAL),
maxWidth: this.props.width || INITIAL,
maxHeight: this.props.height || INITIAL,
};
return (
<div ref={el => this.outterEl = el} style={styles}>
<div ref={el => this.innerEl = el}>
{this.props.children}
</div>
</div>
);
}
};
export default Scrollable;
Scrollable container:
import { connect } from 'react-redux';
import Scrollable from '../components/Scrollable';
const mapStateToProps = (state, ownProps) => Object.assign({
state: state[ownProps.autoScroll] || false
}, ownProps);
export default connect(mapStateToProps)(Scrollable)
With this, Scrollable's life cycle hooks will be called when the corresponding state changes.