React scrollIntoView() not working with ref - reactjs

I have a react functional component defined as
export const NavMenuHorizontal: React.FunctionComponent<NavMenuProps> = (
props
) => {
const selectedItemRef = React.createRef<HTMLDivElement>();
React.useEffect(() => {
selectedItemRef.current?.scrollIntoView({
behavior: "smooth",
inline: "center",
})
});
return (
<List>
{map(...) => (
<div ref={isActive ? selectedItemRef : undefined}>
some text
</div>
)}
</List>
);
};
//isActive is true for only one item in the map
I am trying to make the selected list item scroll into view using a ref but unable to get the expected result. However, if I try to add debug breakpoint at the hook, it's working as expected and scrolling the element in viewport.
I also tried wrapping my scrollIntoView inside a setTimeout() which works but it scrolls it more than once and leads to a jarring effect.

Related

React virtualized infiniteLoader scrolling to top on load using List

I am currently using react-virtualized InfiniteLoader component to scroll over a List of elements that could be variable all the time.
This is my component now:
<VirtualizedInfiniteLoader isRowLoaded={isRowLoaded} loadMoreRows={loadMoreRows} rowCount={rowCount}>
{({ onRowsRendered, registerChild }) => (
<AutoSizer>
{({ width, height }) => (
<List
key={`${width}${height}${rowCount}`}
height={height - headerHeight}
onRowsRendered={onRowsRendered}
scrollToIndex={scrollToIndex}
scrollToAlignment={scrollToAlignment}
ref={registerChild}
rowCount={rowCount}
rowHeight={rowHeight}
rowRenderer={rowRenderer}
width={width}
onScroll={onListScroll}
/>
)}
</AutoSizer>
)}
</VirtualizedInfiniteLoader>
And i'm using this in a container:
renderInfiniteLoader = currentStatus => {
const { cache, headerHeight } = this.state;
const { scrollToIndex, params, allTasks, selectedFilterFields, searchFilters } = this.props;
const list = allTasks[currentStatus];
console.log(list.length, params);
return (
<InfiniteLoaderWrapper
diffHeight={marginBottomToAdd({ searchFilters, selectedFilterFields, customerID: params.customerID })}
ref={this.cardsContainer}
>
<InfiniteLoader
loadMoreRows={options => this.loadMoreRows(options, status)}
rowRenderer={this.renderTasks(list, currentStatus)}
isRowLoaded={this.isRowLoaded(list)}
scrollToIndex={scrollToIndex}
scrollToAlignment="start"
rowCount={list.length + 1}
headerHeight={150}
deferredMeasurementCache={cache}
rowHeight={cache.rowHeight}
onListScroll={this.onInfiniteLoaderScroll}
/>
</InfiniteLoaderWrapper>
);
};
The problem here is that everytime i scroll down and a loader appear if i'm scrolling too fast, page scroll to first element on top.
The prop scrollToIndex seems to be 0 everytime so probably that's the problem.
If i remove that prop, scrolling works correctly, but comes up another issue:
I can click on list's element to go to a detail page, and if i come back i expect to be in the correct position of the list, but i'm on top (again, because scrollToIndex is 0).
What is the correct value to pass to scrollToIndex to be always in the right point?
I can see there was this bug in some version of react-virtualized but can't find any workaround.
Thanks in advance!

Two instances of the same element seem to share state

In my React application I have a Collapsible component that I use more than once, like so:
const [openFAQ, setOpenFAQ] = React.useState("");
const handleFAQClick = (question: string) => {
if (question === openFAQ) {
setOpenFAQ("");
} else {
setOpenFAQ(question);
}
};
return ({
FAQS.map(({ question, answer }, index) => (
<Collapsible
key={index}
title={question}
open={openFAQ === question}
onClick={() => handleFAQClick(question)}
>
{answer}
</Collapsible>
))
})
And the Collapsible element accepts open as a prop and does not have own state:
export const Collapsible = ({
title,
open,
children,
onClick,
...props
}: Props) => {
return (
<Container {...props}>
<Toggle open={open} />
<Title onClick={onClick}>{title}</Title>
<Content>
<InnerContent>{children}</InnerContent>
</Content>
</Container>
);
};
However, when I click on the second Collapsible, the first one opens... I can't figure out why.
A working example is available in a sandbox here.
You will need to ensure that the label and id for each checkbox is the same. Here's a working example
But if you're trying to implement an accordion style, you may need another approach.
on Collapsible.tsx line 36, the input id is set the same for both the Collapsibles. you need to make them different from each other and the problem would be solved.
One thing is that you have the same id which is wrong BUT it can still work. Just change the checkbox input from 'defaultChecked={...}' to 'checked={...}'.
The reason is that, if you use defaultSomething - it tells react that even if this value will be changed - do not change this value in the DOM - https://reactjs.org/docs/uncontrolled-components.html#default-values
Changing the value of defaultValue attribute after a component has mounted will not cause any update of the value in the DOM

Is there any pitfall of using ref as conditional statement?

Context: I am trying to scroll view to props.toBeExpandItem item which keeps changing depending on click event in parent component. Every time a user clicks on some button in parent component I want to show them this list and scroll in the clicked item to view port. Also I am trying to avoid adding ref to all the list items.
I am using react ref and want to add it conditionally only once in my component. My code goes as below. In all cases the option.id === props.toBeExpandItem would be truthy only once in loop at any given point of time. I want to understand will it add any overhead if I am adding ref=null for rest of the loop elements?
export const MyComponent = (
props,
) => {
const rootRef = useRef(null);
useEffect(() => {
if (props.toBeExpandItem && rootRef.current) {
setTimeout(() => {
rootRef.current?.scrollIntoView({ behavior: 'smooth' });
});
}
}, [props.toBeExpandItem]);
return (
<>
{props.values.map((option) => (
<div
key={option.id}
ref={option.id === props.toBeExpandItem ? rootRef : null}
>
{option.id}
</div>
))}
</>
);
};
Depending upon your recent comment, you can get the target from your click handler event. Will this work according to your ui?
const handleClick = (e) => {
e.target.scrollIntoView()
}
return (
<ul>
<li onClick={handleClick}>Milk</li>
<li onclick={handleClick}>Cheese </li>
</ul>
)

node.current shows up as null when using useRef(), despite using within useEffect hook

My goal is to large scrollable modal where if the item provided to the modal changes (there is a "More projects" section at the bottom which should change the modal content), the modal automatically scrolls to the top. Since I can't use the window object, other sources seem to indicate a ref is the best way to go. However, I keep getting the error node.current is undefined at the time of compiling.
I saw elsewhere that this should be avoidable by working with the ref within a useEffect hook, since this will ensure the ref has been initialized by the time it runs, however this is not happening here.
const PortfolioModal = ({
open,
handleClose,
item,
setItem,
...props
}) => {
const node = useRef();
useEffect(() => {
node.current.scrollIntoView()
}, [item]);
return (
<Dialog onClose={handleClose} open={open} fullWidth={true} maxWidth='lg'>
<div ref={node}></div>
<Content>
{a bunch of stuff is here}
<PortfolioFooter
backgroundImage={`${process.env.PUBLIC_URL}/images/backgrounds/panel5.png`}
item={item}
setItem={setItem}
/>
</FlexContainer>
</Content>
</Dialog>
)
};
EDIT: Additional note -- I initially wrapped the entire component with a div with a ref and tried to use scrollTop and I did not receive an error, but there was also no scrolling, so I thought I would try using scrollIntoView with an invisible element.
This is what worked for me, based on this post:
const nodeRef = useRef();
useEffect(() => {
if (nodeRef.current) {
nodeRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}, [item]);
return (
<Dialog onClose={handleClose} open={open} fullWidth={true} maxWidth='lg' scroll="body">
<div ref={node => {
nodeRef.current = node;
}}>
<Content>
{content}
</Content>
</div>
</Dialog>

React use refs on dynamic tab panel

I am using React, material-ui and material-table.
I have a closable tab panel. And when I change tabs I do not unmount the corresponding panel component. I just hide it with css. Which means that when I use my material-table component inside multiple tabs and I want to access the current table ref I just get the table ref which is inside of the recently opened tab panel.
How can I update the ref to correspond the current panels table when I cange the tab?
As I think I should update this ref on tab change event. But how can I do that? I use functional components and useRef.
I tried to use useCallback with no luck
This is my parent component:
const MainTabPanel = ({
tabs,
activeTab,
setTabs,
setActiveTab
}) => {
const tableRef = useRef(null)
const handleTabClose = (event, tabId) => {
event.stopPropagation()
const activeTabIndex = tabs.length > 1 ? tabs.length - 2 : 0
setActiveTab(activeTabIndex)
setTabs([
...tabs.filter(tab => tab.id !== tabId)
])
}
const handleTabChange = (event, newValue) => {
setActiveTab(newValue)
}
return (
<div>
<TabsToolbar
tabs={tabs}
open={open}
activeTab={activeTab}
onTabClose={handleTabClose}
onTabChange={handleTabChange}
/>
<TableRefProvider value={tableRef}>
{tabs.map(({ component: Component, id }, index) => (
<Panel value={activeTab} index={index} key={id} padding={2}>
<Component />
</Panel>
)
)}
</TableRefProvider>
</div>
)
}
export default MainTabPanel
You can try to unmount the table and set the ref to the new table by using the given selected tab as key to the component.
This will force a rerender of the component on key/tab change and will reset the tableRef.
On a side note, why do you not unmount, but hide it with css?

Resources