How do I prevent a re-render of a large list? - lit-element

I have a 60x30 grid for a game editor and as cells are updated, a new array is created to hold the state.
The problem is that when I update that grid array, this changes the property and it causes render() to recreate the grid. This seems almost obvious but then what do my options become?
If this is overly specific, imagine just a huge list of items and you have an immutable array in which one of the items properties must change.
render() {
return html`
${this.data?.cells.map((row) => {
return row.map((cell) => {
return html`<editor-cell .data="${cell}"></editor-cell>`;
});
})}
`;
}
Coincidentally, I had the same problem on Angular with a for loop only it had trackBy which used the index or item.id to prevent the recreation of a list of items. I just accepted the unicorns for that but here it is the same issue.
Question:
What am I missing about immutable states here? I totally understand why this is happening in that, its a new array and so lit element just renders what it deems a new array. I want that, but once the grid has been rendered, I don't understand the separation between rendering and data updates. I'm either missing a key lifecycle understanding, or my approach to state is just totally whack.

If you update the complete array this change the memory reference and then lit-html has to re-render the whole array because it doesn't know what item changes.
In the lit-html documentation you have a section about Repeating templates that explain that very well.
In your case you should use repeat directive, that performs efficient updates of lists based on user-supplied keys:
render() {
return html`
${repeat(this.data?.cells, row => row.id,
row => html`${repeat(row, cell => cell.id,
cell => html`<editor-cell .data="${cell}"></editor-cell>`
)}`
)}
`;
}
Notice the importance of the second argument, that it's the guaranteed unique key for each item.

Related

Accessing an element of an array in TSX

I have a TSX file, with a state including:
tickets: Ticket[],
I now want to change one specific element inside the array, and reset the state, my idea:
onClick = (ticket: Ticket, i: number) => {
var newTitle = window.prompt('hello')
ticket.title = newTitle ? newTitle : ticket.title
var tickets = [this.state.tickets]
tickets[i] = ticket
// set state
}
Besides the usual "OBject could be undefined" errors, I'm mainly getting stuck at:
Type 'Ticket' is missing the following properties from type 'Ticket[]': length, pop, push, concat, and 28 more. TS2740
It's as if they still consider tickets[i] to be of type Tickets[]. (I've done other checks and that seems to be the problem).
Do you know why this is the case? And how can still achieve my goal?
Thank you
There's a lot that's wrong here including multiple mutations of state.
Array of Arrays
The particular error that you've posted:
Type 'Ticket' is missing the following properties from type 'Ticket[]': length, pop, push, concat, and 25 more.
Is caused by this line:
var tickets = [this.state.tickets]
You are taking the array of tickets from state and putting it into an array. This variable tickets is an array with one element where that element is the array from your state. In typescript terms, it is [Ticket[]] or Ticket[][]. So each element of that array should be Ticket[] instead of Ticket. When you try to set an element with a Ticket then you get an error that it should be Ticket[].
State Mutations
As a rule of thumb, don't mutate anything in React if you aren't certain that it's safe. Just setting ticket.title is an illegal mutation of state which will prevent your app from re-rendering properly. The Ticket object that is passed to onClick is (presumably) the same object as the one in your state so you cannot mutate it.
Instead, we use array.map (which creates a copy of the array) to either return the same Ticket object or a copied one if we are changing it. We don't actually need the ticket as an argument. If the tickets have some unique property like an id then you could also pass just the ticket and not i.
onClick = (i: number) => {
const newTitle = window.prompt("hello");
if (newTitle) {
this.setState((prevState) => ({
tickets: prevState.tickets.map((ticket, index) =>
index === i ? { ...ticket, title: newTitle } : ticket
)
}));
}
};

How to use CellMeasurer correctly?

I have an interactive component inside "react-virtualized" List that acts on clicks. When the component is clicked, the cell transforms i.e. height changes.
My first version of the rowRenderer:
rowRenderer ({index, isScrolling, key, style}) {
let message = this.props.messages[index];
return <Message key={message.id} text={message.text} />
}
When the message is clicked, a text field appears. This changes the height. What however happens is that component renders over the next message.
This happens because the instance of Message is different in UI and in CellMeasurer as you can see:
<CellMeasurer
cellRenderer={
// Here we return instance 1
({ rowIndex, ...rest }) => this.rowRenderer({ index: rowIndex, ...rest })
}
columnCount={1}
rowCount={messages.length}
>
{({ getRowHeight, resetMeasurementForRow }) => {
this.resetMeasurementForRow = resetMeasurementForRow;
return <List
height={height}
overscanRowCount={50}
rowCount={messages.length}
rowHeight={getRowHeight}
rowRenderer={this.rowRenderer} // Here we create another instance
width={width}
ref={(ref)=>{
this.list = ref;
}}
/>
}}
</CellMeasurer>
The instance created by List will obviously contain correct state but CellMeasurer is not aware of this state.
I tested the following approach but I highly doubt that this is the correct way to do this? I simply cache the UI component instance myself:
rowRenderer ({index, isScrolling, key, style}) {
let message = this.props.messages[index];
if(!this.componentCache[index]) {
this.componentCache[index] = <Message key={message.id} text={message.text} />
}
return this.componentCache[index];
}
This fixes this problem but probably introduces many other issues. What is the correct way to do this?
(I'm aware that using Flux/Redux/global state could fix this but I'm wondering is there some fundamental react-virtualized feature/aspect that I'm missing here.)
Answer to this is actually in the documentation:
The current implementation of CellMeasurer creates cells on demand,
measures them, and then throws them away. Future versions of this
component may try to clone or in some other way share cells with their
parent Grid in order to improve performance. However until that
happens, be wary of using CellMeasurer to measure stateful components.
Since cells are just-in-time created for measuring purposes they will
only be measured with their default state. To avoid this issue for
now, use controlled props (instead of state) for cell rendering
behavior.
So there is no other solution than handling the state outside the component. In practice, I fixed it as follows:
rowRenderer ({index, isScrolling, key, style}) {
let message = this.props.messages[index];
let selected = message.id === store.getState().selectedMessage;
return <Message key={message.id} text={message.text} selected={selected} />
}

Pagination Ideas for Angular2 and ReactiveX

I'm learning ReactiveX. I've snipped the error checking, logging, and other bits out to make this easier to read.
I have a service that returns a collection of objects as JSON:
getPanels() {
return this.http.get(this._getPanelsUrl)
.map(panels => <Panel[]> panels.json());
}
My component calls the service method and stores the data in an array:
panels: Panel[] = [];
ngOnInit(){
this._PanelService.getPanels()
.subscribe(data => this.panels = data);
}
The goal is to display this data in groups in my template:
<ol>
<li *ngFor="#panel of panels">
<h3>{{panel.startDate}}</h3>
</li>
</ol>
Now I want to add pagination and display only three or four panels at a time.
My first idea was to use bufferCount to emit the objects in groups:
getPanels() {
return this.http.get(this._getPanelsUrl)
.map(panels => <Panel[]> panels.json())
.bufferCount(3,3);
}
Now I have a multidimensional array, so I have to update the component accordingly:
panels: Array<Panel[]> = [];
ngOnInit(){
this._PanelService.getPanels()
.subscribe( data => this.panels = data );
}
I thought I would have a nice and tidy array with each index having three members of the collection. I was wrong, and the entire collection is now stored in data[0]. Next, I tried switching the chain up a bit:
getNextPanel() {
return this.http.get(this._nextPanelUrl)
.bufferCount(3,3)
.map(res => <Panel[]> res.map(r => <Panel> r.json()));
}
Whoa. I obviously need someone to save me from myself at this point. Look at my lambdas! Data isn't even going to flow all the way back to the component at this point. This is when I started thinking maybe I don't need to learn how to do this the ReactiveX way … .
My next course was to try and iterate through the values with Angular. I tried using a few variables with the slice pipe:
<ol>
<li *ngFor="#panel of (panels | slice:start:items)">
<h3>{{panel.startDate}}
</li>
</ol>
<button (click)="start = start + start"></button>
Even though Angular 2 is still in beta, I could tell that I was getting tired when the parser kept barking at me for using operators and expressions where they don't belong.
I'm ready to learn from these mistakes so I can make bigger ones. Any suggestions?
[EDIT]
I've decided to use ng2-pagination because it does exactly what I want it to do. I'm not going to post that as the answer, however, because I still want to try and implement it with rxjs.
So if you've come this far, and you just need something that works, ng2-pagination (in beta 2 as of this writing) works very well.
Really late, but I hope this might help someone else with this issue.
I think the problem with your implementation is that you are overwriting the this.panel variable with each onNext event on the subscriber.
With this change should work as expected:
getPanels() {
return this.http.get(this._getPanelsUrl)
.map(panels => <Panel[]> panels.json())
.bufferCount(3)
.toArray();
}
and then:
panels: Panel[][] = [];
ngOnInit(){
this._PanelService.getPanels()
.subscribe( data => { this.panels = data } );
}
The idea is to merge all onNext events into an array (using the toArray method) that will be emited as the single onNext of that new Observer, and will contain all events.

Is it better to reuse or recreate a reactive source in Meteor

I have a template called 'contacts'. Inside is an #each which renders the template 'contact'. The user can press the 'edit' button which sets a session variable with the mongo id of the edited row. The row then reactively re-renders into "edit" mode.
Template.contact.viewEditing = function() {
return Session.get("contactViewEditingId") === this._id;
}
The html uses the viewEditing helper a few times, for instance:
{{#if viewEditing}}
<div class="panel-heading">EDITING!</div>
{{/if}}
I need to bind some javascript in the .rendered(). I would like to check again if we are editing. I can think of 2 options:
Should I call Template.content.viewEditing() inside my template.rendered() ? Does this save on reactivity calculations?
Or should I just copy pasta the if statement. This option seems to violate DRY.
Option 1:
Template.contact.rendered = function() {
if( Template.contact.viewEditing.call(this.data) ) {
// Bind some fancy jQuery
bindEditInPlace(this.data);
}
}
Option 2:
Template.contact.rendered = function() {
if( Session.get("contactViewEditingId") === this._id ) {
// Bind some fancy jQuery
bindEditInPlace(this.data);
}
}
I think that putting {{viewEditing}} multiple times in your template doesn't "cost" anything extra. So logically I would think that using this helper elsewhere is better. Maybe I need more help understanding reactivity calculations. Thanks!
Helpers are run inside a Deps.Computation, which means that every time a reactive variable is referenced and modified in a helper, it will re-run.
Template.rendered is a callback that runs each time the template is re-rendered (which usually happens when a helper in the template is re-run reactively), but it is not itself a reactive computation.
So it doesn't matter using either the template helper or copy-pasting its code inside your rendered callback : both ways won't trigger a reactive computation invalidation because we are not inside one.
As far as DRY is concerned, you could refactor your code like this :
function isContactViewEditing(contactView){
return Session.equals("contactViewEditingId",contactView._id);
}
Template.contact.helpers({
viewEditing:isContactViewEditing
});
Template.contact.rendered=function(){
if(isContactViewEditing(this.data)){
//
}
};
I think saimeunt's answer is correct, especially if you have more complex logic in the function which you don't want to replicate.
Create a local function which you can re-use in both the helper and the .rendered callback.
If you had a case where you wanted to use a reactive source minus the reactivity you could make it non-reactive by wrapping it in a Deps.nonreactive function likes so:
Deps.nonreactive(function(){
//Reactive stuff here
});
Regarding reactivity concerns, pay attention to his change from using Session.get to Session.equals. Session.get will cause any reactive computation it is used in to re-calculate on every change of the session variable. So if you use this helper in multiple places with different ids, and you change the session variable, every single one will re-calculate and re-render the templates they are used in. Session.equals only invalidates a computation when the equality changes. So changing the session variable from one non-equal id to another non-equal id will not cause the computation/template to re-run when you use Session.equals.
For your specific example where the helper is only returning the result of a Session.equals you might consider creating a global handlebars helper that can do this for you with any session variable and any value. Like the following.
Handlebars.registerHelper('sessionEquals', function (key, value) {
return Session.equals(key, value);
});
Then in the template use it like so:
{{#if sessionEquals 'contactViewEditingId' _id}}
<div class="panel-heading">EDITING!</div>
{{/if}}
In the template when rendering an item that is editable add a unique class name to mark the item as editable. Then in your Template.rendered callback when binding the javascript use a selector which looks for that class and only binds to elements with that special class.

dijit.Tree search and refresh

I can't seem to figure out how to search in a dijit.Tree, using a ItemFileWriteStore and a TreeStoreModel. Everything is declarative, I am using Dojo 1.7.1, here is what I have so far :
<input type="text" dojoType="dijit.form.TextBox" name="search_fruit" id="search_fruit" onclick="search_fruit();">
<!-- store -->
<div data-dojo-id="fruitsStore" data-dojo-type="dojo.data.ItemFileWriteStore" clearOnClose="true" urlPreventCache="true" data-dojo-props='url:"fruits_store.php"'></div>
<!-- model -->
<div data-dojo-id="fruitsModel" data-dojo-type="dijit.tree.TreeStoreModel" data-dojo-props="store:fruitsStore, query:{}"></div>
<!-- tree -->
<div id="fruitsTree" data-dojo-type="dijit.Tree"
data-dojo-props='"class":"container",
model:fruitsModel,
dndController:"dijit.tree.dndSource",
betweenThreshold:5,
persist:true'>
</div>
The json returned by fruits_store.php is like this :
{"identifier":"id",
"label":"name",
"items":[{"id":"OYAHQIBVbeORMfBNZXFGOHPdaRMNUdWEDRPASHSVDBSKALKIcBZQ","name":"Fruits","children":[{"id":"bSKSVDdRMRfEFNccfTZbWHSACWbLJZMTNHDVVcYGcTBDcIdKIfYQ","name":"Banana"},{"id":"JYDeLNIGPDBRMcfSTMeERZZEUUIOMNEYYcNCaCQbCMIWOMQdMEZA","name":"Citrus","children":[{"id":"KdDUfEDaKOQMFNJaYbSbAcAPFBBdLALFMIPTFaYSeCaDOFaEPbJQ","name":"Orange"},{"id":"SDWbXWbTWKNJDIfdAdJbbbRWcLZFJHdEWASYDCeFOZYdcZUXJEUQ","name":"Lemon"}]},{"id":"fUdQTEZaIeBIWCHMeBZbPdEWWIQBFbVDbNFfJXNILYeBLbWUFYeQ","name":"Common ","children":[{"id":"MBeIUKReBHbFWPDFACFGWPePcNANPVdQLBBXYaTPRXXcTYRTJLDQ","name":"Apple"}]}]}]}
Using a grid instead of a tree, my search_fruit() function would look like this :
function search_fruit() {
var grid = dijit.byId('grid_fruits');
grid.query.search_txt = dijit.byId('search_fruit').get('value');
grid.selection.clear();
grid.store.close();
grid._refresh();
}
How to achieve the same using the tree ? Thanks !
The refreshing of a dijit.Tree becomes a little more complicated, since there is a model involved (which in grid afaik is inbuilt, the grid component implements query functionality)
Performing search via store
But how to search, thats incredibly easy whilst using the ItemFileReadStore. Syntax is as such:
myTree.model.store.fetch({
query: {
name: 'Oranges'
},
onComplete: function(items) {
dojo.forEach(items, function(item) {
console.log(myTree.model.store.getValue(item, "ID"));
});
}
});
Displaying search results only
As shown above, the store will fetch, the full payload is put into its _allItemsArray and the store queryengine then filters out what its told by query argument to the fetch method. At any time, we could call fetch on store, even without sending an XHR for json contents - fetch with query argument can be considered as a simple filter.
It becomes slightly more interesting to let the Model know about this query.. If you do so, it will only create treeNodes to fill the tree, based on the returned results from store.fetch({query:model.query});
So, instead of sending store.fetch with a callback, lets _try to set model query and update the tree.
// seing as we are working with a multi-parent tree model (ForestTree), the query Must match a toplevel item or else nothing is shown
myTree.model.query = { name:'Fruits' };
// below method must be implemented to do so runtime
// and note, that the DnD might become invalid
myTree.update();
Refreshing tree with new xhr-request from store
You need to do exactly as you do with regards to the store. Close it but then rebuild the model. Model contains all the TreeNodes (beneath its root-node) and the Tree itself maps an itemarray which needs to be cleared to avoid memory leakage.
So, performing following steps will rebuild the tree - however this sample does not take in account, if you have DnD activated, the dndSource/dndContainer will still reference the old DOM and thereby 'keep-alive' the previous DOMNode hierachy (hidden ofc).
By telling the model that its rootNode is UNCHECKED, the children of it will be checked for changes. This in turn will produce the subhierachy once the tree has done its _load()
Close the store (So that the store will do a new fetch()).
this.model.store.clearOnClose = true;
this.model.store.close();
Completely delete every node from the dijit.Tree
delete this._itemNodesMap;
this._itemNodesMap = {};
this.rootNode.state = "UNCHECKED";
delete this.model.root.children;
this.model.root.children = null;
Destroy the widget
this.rootNode.destroyRecursive();
Recreate the model, (with the model again)
this.model.constructor(this.model)
Rebuild the tree
this.postMixInProperties();
this._load();
Creds; All together as such, scoped onto the dijit.Tree:
new dijit.Tree({
// arguments
...
// And additional functionality
update : function() {
this.model.store.clearOnClose = true;
this.model.store.close();
delete this._itemNodesMap;
this._itemNodesMap = {};
this.rootNode.state = "UNCHECKED";
delete this.model.root.children;
this.model.root.children = null;
this.rootNode.destroyRecursive();
this.model.constructor(this.model)
this.postMixInProperties();
this._load();
}
});

Resources