Animate entry of (only new) elements in a virtualized list - reactjs

I am using react-window FixedSizedList and react-virtualized-auto-sizer Autosizer components to build a list/table UI element that may contain thousands of items that also receives new items via a websocket connection and prepends them to the list. I now have a requirement to animate the entry of new elements in this list.
Here is a codesandbox link with a minimal (and not quite working) example: codesandbox.
Note how the .row animation is triggered for every child of FixedSizedList each time the data list receives a new item. Also note how the .row animation is again triggered for every child of FixedSizedList when the list is scrolled.
I understand that the reason this is happening is because of how list virtualization works using absolute positioning of the children rows. Every time a new item is inserted into data, or the list is scrolled, react-window needs to re-compute the style prop for each row, which recreates every DOM element and hence re-triggers the .row animation.
I would like to animate only the new .row DOM elements when they appear.
Things I have tried:
Add animation-iteration-count: 1; to the .row class. This doesn't work for the same reason mentioned above as every element is re-created on a new item insert.
Animate only the first row (example using red background in the code sandbox). While this "works", it's not quite suitable. In my production site, updates are not guaranteed to come through one at a time - multiple rows might be inserted at the same time. All new rows should be animated when they are added to the DOM. This can be replicated in the code sandbox by inserting two UUID's at once in the hook.
Not use list virtualization. This of course works fine, but is not suitable. In my production site this list could contain thousands of items.
Reading this previous question. The previous question is sparse of information, does not have a minimal example, and has no useful answers or comments. Additionally, it is over 5 years old.
How is it possible to achieve the result I'm looking for here?
EDIT:
A further attempt I have tried, extending on 2) above:
Save a copy of the "old" list of items each render. When an update is received, subtract the length of the old list from the length of the new list (call this number n). Animate the top n items of the new list. This "works", but the production system has some intricacies making this solution insufficient - each list item is dated with an ISO timestamp, and the list is sorted according to timestamp new -> old. Updates received via websocket also have a timestamp, but there is no guarantee the new item will always be newer than the current top of the list - in some cases, new items are inserted into position 2, 3 or further down in the list. In this case, animating the top n items based on the length change would not be accurate.

DonĀ“t know how costly it can be for you, but one way to do that is on every state change add a specific className to the new insert rows and a className to the already inserted rows.
So that way you can handle using the className related to the new inserted lines.
Something like this:
const mappedObject = (data, className) => {
return {
item: data,
className
};
};
React.useEffect(() => {
const interval = setInterval(() => {
const alreadyInsertedData = data.map((item) => {
item.className = "already-inserted";
return item;
});
setData((prev) => [
mappedObject(uuidv4(), "first-insert"),
mappedObject(uuidv4(), "first-insert"),
mappedObject(uuidv4(), "first-insert"),
...alreadyInsertedData
]);
}, 3000);
return () => {
clearTimeout(interval);
};
}, [setData, data]);
Here's a code sample to you check.

Related

React table does not refresh when data in a cell changes

I have a React component in which I have a list of items that I store as a dictionary:
const [list, setList] = useState<{ [id: number]: MyProps }>({});
and I render this list as a table in the render method:
{Object.entries(list).map(v =>
<MyTableRow
// #ts-ignore
id={v[0]}
start_date={v[1].start_date}
end_date={v[1].end_date}
dollar_amount={v[1].dollar_amount}
key={v[0]} />
)}
MyTableRow is another React component and basically returns a HTML tr element whose columns are populated based on props passed to it.
So far so good and I have seen that when I add items to the list, the UI gets updated without me having to call setList.
Once I add a new item to the list, I also make some AJAX calls to fetch some additional pieces of information. This is when the problem starts to kick in. When the AJAX call completes, I update the data e.g., as follows:
var e = list[id];
e.dollar_amount = new dollar amount
But the UI does not refresh. I do not want to copy the entire list which could be quite big into a new object and then call setList. The list might have 100 elements in it. Each element corresponds to a row with say 10 columns. I want to be able to update a cell in the table and only that cell should re-render in the UI. Not the whole table. I thought that was the whole value proposition of React.
How can I make it work as described?
PS: I don't want to be making AJAX calls in the MyTableRow child component because the AJAX calls post data into a database and React can call MyTableRow many times for the same data which would result in duplicate records being added to the database.

How to hold screen position when data added in react?

I have a list in react that added more rows with interval,
my problem is that when the list grown the screen going up.
I want to hold the position of screen how he was before I added rows to list.
That is my code:
setInterval(() => {
//this row save the position of the screen before the adding row
setPrevRef({scrollHeight:tabElement.current.scrollHeight,scrollTop:tabElement.current.scrollTop})
props.setFilteredPT (prev=> [newRow,...prev])
}, 10000);
and
useEffect(()=>{
tabElement.current.scrollTop =prevRef.scrollTop + (tabElement.current.scrollHeight - prevRef.scrollHeight )
},[props.filteredPT])
After I wrote this code the screen going up to a second and than comeback to the place.
here is how react does it with class component: https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html#reading-dom-properties-before-an-update
but I want do it with function component.
Somebody can help?
I had found that my problem cause I used for map key the index, when I have changed it to uniq ID it worked good.

How do I use React refs in a map for functional components?

I have a map function which renders a list of divs and these divs are based on the components state, so it is dynamic. I need to put a ref on each of these items on the list so that the user can basically press up and press down and traverse the list starting from the top.
Something like this:
const func = () => {
const item = items.map((i, index) => {
return (
<div ref={index}>{i.name}</div>
)
});
}
This has been difficult since it seems like refs are not like state where they can change based on dynamic data. Its initialized once and set to a constant. But I don't know what to initialize it as until this list is rendered and I can't call the useRef hook in a function of course so what is the solution here?
EDIT:
Ok so, its seems there needs to be some more context around this question:
I have a dropdown which also allows the user to search to filter out the list of items. I want to make an enhancement to it so that when the user searches it focuses on the first element and allows the use of arrow keys to traverse the list of elements.
I got stuck when I tried to focus on the first element because the first element doesn't exist until the list is rendered. This list changes dynamically based on what the user types in. So how do you focus on the first element of the list if the list is always changing as the user types?
This can be done without refs, so it should probably be done without refs.
You can put the index of the currently focused element into state. Whenever the displayed data changes, reset the index to 0:
const [focusedElementIndex, setFocusedElementIndex] = useState(0);
useEffect(() => {
setFocusedElementIndex(0);
}, [items]);
// using this approach,
// make sure items only gets a new reference when the length changes
When rendering, if you need to know the focused index to change element style, for example, just compare the index against the state value:
<div class={index === focusedElementIndex ? 'focused' : ''}
And have a keydown listener that increments or decrements focusedElementIndex when the appropriate key is pressed.

ReactTable setState on columns is not changing layout

first I should mention that I'm very new to react so this might be something silly..
I've struggled to get ReactTable's column to be sortable with the help of mattlockyer's gist
What I don't understand is I call setState on columns and then the render call is triggered correctly but it doesn't update the layout.
The render function "columns" contains an updated object with the correct column order, however render does not change the column order nor does sorting columns content.
This is the code in question,
this.setState({
columns: newColLayout,
}, () => {
this.mountEvents();
});
It will trigger a render, but the columns stay the same.
Ive put together a codepen with my current work and with comments about the code in question.
Any help would be greatly appreciated.
OK so i think i figured out whats going on.
The issue is splice modifies the state without triggering a change event, then when the event is fired from setStat, react doesn't see the change made to the column layout as it matches the current value.
The way i get around this is by cloning the column array using slice.
// Clone the current column state.
const columnClone = this.state.columns.slice(0);
// Remove item from array and stick it in a new position.
columnClone.splice(i, 0, columnClone.splice(this.draggedCol, 1)[0]);
Here is the updated code...
Still might be improved by someone who has more experience with react.

React JS - shouldComponentUpdate on list items

I cannot find anywhere how to implement this feature that looks so easy to implement.
This feature is mentioned in this dev talk https://youtu.be/-DX3vJiqxm4?t=1741
He mentions that for every object in array he checks upvotes and downvotes to check if list row needs updating, but I just can't implement it.
I have an app in react JS with alot of items, and I am changing only one random item. React of course rerenders the whole list in virtual DOM and diffs the previous and current virtual DOMs of the whole list and it takes long time.
But I would like to avoid rendering the unchanged list items. In my app - if "todo" property hasn't changed, the item doesn't need to be updated.
Here is a demo of my app: https://jsfiddle.net/2Lk1hr6v/29/
shouldComponentUpdate:function(nextProps, nextState){
return this.props.todo!==nextProps.todo;
},
I am using this method in the list item component, but the this.props.todo is the same as nextProps.todo so no rows are updated when I change a random item of the first five items.
It's because you haven't updated the reference of the Todo list array this.props.todos.
changeOne:function(){
var permTodos = this.props.todos.concat([]);
permTodos[Math.floor((Math.random() * 5) + 0)]= {todo:((Math.random() * 50) + 1)};
this.setState({ todos: permTodos });
},
This is why immutability is important.
If you think this is getting too complex. Use libraries such as immutableJS which does that automagically for you.
Edit: Working fiddle link!
https://jsfiddle.net/11z2zzq6/1/

Resources