React JS - shouldComponentUpdate on list items - reactjs

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/

Related

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

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.

How to model 1'000'000'000 cells in React?

So I want to display a large table, say 1 billion cells.
The component receives remote updates, each addressing one cell.
Each cell is represented via a div
Here's a naive implementation, which won't be practical, because on each update a large array is created and all cells are updated.
const Table = props => {
const [cells, setCells] = useState(new Array[1_000_000_000])
// ... 'useEffect()' receives remote data and 'setCells()' it.
return cells.map(cell => <div id={cell.id}>{cell.text}</div>)
}
How to implement this efficiently so that performance is top notch?
Ideally, I would like that on each update only 1 array element is updated and only 1 table cell in DOM is updated 🤷‍♂️ Ideally the solution should be O(1) and work great for tables of 10 to 1'000'000'000 cells. Is it possible in React?
By 1'000'000'000 cells I mean a large number of cells that browser can display at once if cells are created and updated with Vanilla JavaScript.
I'm looking not for a library but for a pattern. I want to find out what is the general approach in React for such cases. It is obvious how to do this in Vanilla JavaScript, you just create divs and then access the required div and update its innerText 🤷‍♂️. But how this case can be modeled in React?
For very long lists, "virtualized list" is the typical approach so that only some of the cells are actually mounted and the rest are temporarily rendered with placeholders. A couple libraries that implement this are:
https://react-window.now.sh/
https://bvaughn.github.io/react-virtualized
If you are looking for strictly O(1) then typical React patterns may not suitable since React cannot partially render a component. In other words, the render method of your Table component will need to iterate over the list in O(n). Virtualization of the list could reduce from O(size of list) to O(size of window).
You may be able to workaround this by maintaining a list of React refs of each mounted cell, then calling an update method on the single cell by index to trigger a re-render of a single cell, but this is likely not recommended usage of React.
Another option could be the Vanilla JS approach inside of a stateful React component. You'll want to implement the relevant lifecycle methods for mounting and un-mounting, but the update would be handled in Vanilla JS.
class Table {
constructor(props) {
super(props);
this.container = React.createRef();
}
componentDidMount() {
// Append 1_000_000_000 cells into this.container.current
// Later, modify this.container.current.item(index) text
}
componentWillUnmount() {
// Perform any cleanup
}
render() {
return (
<div ref={this.container} />
)
}
}

Store checkbox values as array in React

I have created the following demo to help me describe my question: https://codesandbox.io/s/dazzling-https-6ztj2
I have a form where I submit information and store it in a database. On another page, I retrieve this data, and set the checked property for the checkbox accordingly. This part works, in the demo this is represented by the dataFromAPI variable.
Now, the problem is that when I'd like to update the checkboxes, I get all sorts of errors and I don't know how to solve this. The ultimate goal is that I modify the form (either uncheck a checked box or vice versa) and send it off to the database - essentially this is an UPDATE operation, but again that's not visible in the demo.
Any suggestions?
Also note that I have simplified the demo, in the real app I'm working on I have multiple form elements and multiple values in the state.
I recommend you to work with an array of all the id's or whatever you want it to be your list's keys and "map" on the array like here https://reactjs.org/docs/lists-and-keys.html.
It also helps you to control each checkbox element as an item.
Neither your add or delete will work as it is.
Array.push returns the length of the new array, not the new array.
Array.splice returns a new array of the deleted items. And it mutates the original which you shouldn't do. We'll use filter instead.
Change your state setter to this:
// Since we are using the updater form of setState now, we need to persist the event.
e.persist();
setQuestion(prev => ({
...prev,
[e.target.name]: prev.topics.includes(e.target.value)
// Return false to remove the part of the array we don't want anymore
? prev.topics.filter((value) => value != e.target.value)
// Create a new array instead of mutating state
: [...prev.topics, e.target.value]
}));
As regard your example in the codesandbox you can get the expected result using the following snippet
//the idea here is if it exists then remove it otherwise add it to the array.
const handleChange = e => {
let x = data.topics.includes(e.target.value) ? data.topics.filter(item => item !== e.target.value): [...data.topics, e.target.value]
setQuestion({topics:x})
};
So you can get the idea and implement it in your actual application.
I noticed the problem with your code was that you changed the nature of question stored in state which makes it difficult to get the attribute topics when next react re-renders Also you were directly mutating the state. its best to alway use functional array manipulating methods are free from side effects like map, filter and reduce where possible.

ReactJS issue on my test app

So, I've been working through my first ReactJS app. Just a simple form where you type in a movie name and it fetches the data from IMDB and adds them as a module on the page. That's all working fine.
However each movie module also had a remove button which should remove that particular module and trigger a re-render. That's not working great as no matter which button you click it always removes the last movie module added rather than the one you're clicking on.
App:
http://lukeharrison.net/react/
Github codebase:
https://github.com/WebDevLuke/React-Movies
I'm just wondering if anybody can spot the reasoning behind this?
Cheers!
Just a hunch, but you should use a unique key, not just the index of the map function. This way React will understand that the movies are identified not by some iterating index, but an actual value, and that will probably solve your issue.
var movies = this.state.movies.map(function(movie, index){
return (
<Movie key={movie} useKey={index} removeMovieFunction={component.removeMovie} search={movie} toggleError={component.toggleError} />
);
});
This is because React re-evaluates your properties, sees that nothing has changed, and just removes the last <Movie /> from the list. Each Movie's componentDidMount function never runs more than once, and the state of Movie 1, Movie 2 and Movie 3 persists. So even if you supply search={movie} it doesn't do anything, because this.props.search is only used in componentDidMount.
I'm not exactly sure why it isn't rendering correctly as the dataset looks fine.
Looking at the code, I would change your remove function to this...
var index = this.state.movies.indexOf(movieToRemove);
console.log(this.state.movies);
if (index > -1) {
this.state.movies.splice(index, 1);
}
console.log(this.state.movies);
this.setState(this.state.movies);
My assumption is that, the state isn't being updated correctly. Whenever updating state, you should always use setState (unless the convention changed and I wasn't aware).
Also, you shouldn't need to explicitly call forceUpdate. Once setState is called, React will automatically do what it needs to and rerender with the new state.
State should be unidirectional (passed top down) from your top level component (known as a container). In this instance, you have state in your top level component for search strings and then you load individual movie data from within the "Movie" component itself via the IMDB API.
You should refactor your code to handle all state at the top level container and only pass the complete movie data to the dumb "Movie" component. all it should care about is rendering what you pass in it's props and not about getting it's own data.

Should I update lists in place with React?

To delete an item from a list, I created a new list that does not contain the deleted item, and replaced the old list with the new. Is this the "right" way or should I edit the list in place? I suspect this may be inefficient for JS.
destroy: function(chosenItem) {
var newItems = this.state.items.filter(function(item) {
return chosenItem.id != item.id;
});
this.setState({items:newItems});
}
A couple of things:
if such items have any sort of persistence mechanism attached, consider using any action architecture [see Flux, Reflux...], so that you do not set the state of your component directly, but you delegate deletion to a separate entity, that will later on notify your component of the update;
creators of React evangelise about immutable objects in order to work w/ React, so your choice is definitely fine.

Resources