Make a momentary highlight inside a virtualized list when render is not triggered

485 Views Asked by At

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.

1

There are 1 best solutions below

0
On

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 getLastId that is exposed by the context. Before it used to reset the id back to undefined as soon as it is drawn by the component in useEffect. 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, getLastId and resetLastId manipulate a ref object, not a state in order to prevent unnecessary renders. So, now, MyList component looks like this:

function MyList(props) {

  const { collection } = props;
  
  const { getLastId, resetLastId } useApiResultsContext();
  
  cosnt highlightIndex = useRef();
  const listRef = useRef(null);
  
  const turnHighlightOff = useCallback(() => {
    highlighIndex.current = undefined;
  }, []);
  
  useEffect(() => {
    const id = getLastId();
    resetLastId();
    
    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 === index || getLastId() === item.id}
          checkHighlight={turnHighlightOff}
        />
      )}
    />
  ); 
}

Now, setting the highlightIndex inside useEffect takes care of items outside the viewport, and feeding the getLastId call into the properties of each ItemRow takes care of those already in view.