I have an extensive list of items in an application, so it is rendered using a virtual list provided by react-virtuoso. The content of the list itself changes based on API calls made by a separate component. What I am trying to achieve is whenever a new item is added to the list, the list automatically scrolls to that item and then highlights it for a second.
What I managed to come up with is to have the other component place the id of the newly created item inside a context that the virtual list has access to. So the virtual list looks something like this:
function MyList(props) {
const { collection } = props;
const { getLastId } useApiResultsContext();
cosnt highlightIndex = useRef();
const listRef = useRef(null);
const turnHighlightOff = useCallback(() => {
highlighIndex.current = undefined;
}, []);
useEffect(() => {
const id = getLastId();
// calling this function also resets the lastId inside the context,
// so next time it is called it will return undefined
// unless another item was entered
if (!id) return;
const index = collection.findIndex((item) => item.id === if);
if (index < 0) return;
listRef.current?.scrollToIndex({ index, align: 'start' });
highlightIndex.current = index;
}, [collection, getLastId]);
return (
<Virtuoso
ref={listRef}
data={collection}
itemContent={(index, item) => (
<ItemRow
content={item}
toHighlight={highlighIndex.current}
checkHighlight={turnHighlightOff}
/>
)}
/>
);
}
I'm using useRef instead of useState here because using a state breaks the whole thing - I guess because Virtuouso doesn't actually re-renders when it scrolls. With useRef everything actually works well. Inside ItemRow the highlight is managed like this:
function ItemRow(props) {
const { content, toHighlight, checkHighligh } = props;
const highlightMe = toHighlight;
useEffect(() => {
toHighlight && checkHighlight && checkHighligh();
});
return (
<div className={highlightMe ? 'highligh' : undefined}>
// ... The rest of the render
</div>
);
}
In CSS I defined for the highligh class a 1sec animation with a change in background-color.
Everything so far works exactly as I want it to, except for one issue that I couldn't figure out how to solve: if the list scrolls to a row that was out of frame, the highlight works well because that row gets rendered. However, if the row is already in-frame, react-virtuoso does not need to render it, and so, because I'm using a ref instead of a state, the highlight never gets called into action. As I mentioned above, using useState broke the entire thing so I ended up using useRef, but I don't know how to force a re-render of the needed row when already in view.
I kinda solved this issue. My solution is not the best, and in some rare cases doesn't highlight the row as I want, but it's the best I could come up with unless someone here has a better idea.
The core of the solution is in changing the idea behind the
getLastIdthat is exposed by the context. Before it used to reset the id back toundefinedas soon as it is drawn by the component inuseEffect. Now, instead, the context exposes two functions - one function to get the id and another to reset it. Basically, it throws the responsibility of resetting it to the component. Behind the scenes,getLastIdandresetLastIdmanipulate a ref object, not a state in order to prevent unnecessary renders. So, now,MyListcomponent looks like this:Now, setting the
highlightIndexinsideuseEffecttakes care of items outside the viewport, and feeding thegetLastIdcall into the properties of eachItemRowtakes care of those already in view.