DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
11 Monitoring and Observability Tools for 2023
Learn more
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Deployment
  4. Managing Component State in NativeScript ListView

Managing Component State in NativeScript ListView

A tutorial on how to overcome the hidden traps you can hit while developing apps with ListView.

Alexander Vakrilov user avatar by
Alexander Vakrilov
·
May. 03, 19 · Tutorial
Like (1)
Save
Tweet
Share
7.28K Views

Join the DZone community and get the full member experience.

Join For Free

This article originally appeared on Medium.

A while ago I blogged about using Multiple Items Templates in a NativeScript ListView and briefly touched on the topics of UI Virtualization and View/Component Recycling. Looks like there are some hidden traps you can hit while developing apps with ListView related to this, especially if you are using Angular Components as items in your ListView and keep some state in the components.

We will take a deep dive into the problem and show some approaches to overcome it.

The Scenario

To demonstrate the problem, we will build an application that shows a list of items and we want to be able to select some of them.

We will be using a project that shares the code between Web and Mobile for this blog. The reasons:

  1. We can outline differences between Web and NativeScript templates.
  2. Code-sharing is now ridiculously easy thanks to angular-cli and @nativescript/schematics. Read more about it in this awesome blog by Sebastian Witalec

Here is how the app will look like in browser and iOS simulator:

Each item in the list is rendered by an ItemComponent — having the current items as an @Input parameter . Here is the component class:

@Component({
  selector: 'app-item',
  templateUrl: './item.component.html',
  styleUrls: ['./item.component.css']
})
export class ItemComponent {
  @Input() item: Item;
  selected: boolean = false;
}

Notice we are keeping the selected state as a field in the component. We are also using it in a couple of places in the template:

// Mobile Template (item.component.tns.html)
<StackLayout orientation="horizontal" class="item"
    (tap)="selected = !selected">
    <Label [text]="item.name"></Label>
    <Label class="select-btn" 
        [class.selected]="selected"
        [text]="selected ? 'selected' : 'unselected'">
    </Label>
</StackLayout>
// Web Template (item.component.html)
<div class="item">
  {{ item.name }}
  <span (click)="selected = !selected" 
    class="select-btn" 
    [class.selected]="selected">
    {{ selected ? 'selected' : 'unselected' }}
  </span>
</div>

The whole project, along with branches for the different sections in the blog, lives here.

Using Good Old *ngFor

We will start by showing all the items from the model in a container (a.k.a smart) component that are using *ngFor:

<app-item *ngFor="let item of items" [item]="item"></app-item>

Pretty straight-forward! This will render an ItemComponent for each item in the collection.

In the test project, there are 100 items generated and everything is super-fast for both web and mobile.

Let's try it with more items.

The web app starts to get a considerable startup lag at 10K items. In the mobile items, the threshold is much lower — around 2K. This is because the native components rendered by iOS/Android are more expensive than browser DOM elements. If we make templates more complex, these numbers will go down.

But... nobody puts 2,000 items in a list. You will probably implement an infinite scroll with load on demand mechanics. The thing is — even then you will hit performance and memory problems as you are scrolling, as *ngFor will instantiate more and more ItemComponents as you are scrolling down and pulling more data.

Here is the code so that you can play with it yourself — just tweak the item.service.ts fie to generate more items: ngFor branch.

We can do better!

Switch to ListView in NS

In NativeScript, we utilize the native controls that do UI Virtualization and **View/Component Recycling. **This means only the UI elements for the items visible will be created and these UI elements will be recycled (or reused) to show the new items that come into view.

To start using ListView we just have to change the *ngFor-based template from the above to:

<ListView [items]="items">
  <ng-template let-item="item">
    <app-item [item]="item"></app-item>
  </ng-template>
</ListView>

Great! A quick test shows that we can now scroll trough 100K items in the mobile app with no problems!

A simple counter in the ItemComponent's constructor shows that only 13 instances were ever created. They are reused to show all items as you scroll.

The Problem

Neat! Or is it? Let's see what happens when we start selecting items:

Here we see the problem, which is actually the reason for this post. I select the first three items. When I scroll down, items 13, 14, and 15 are also selected. Further down, more items that I have never even seen before are also selected.

The reason for this is that when ItemsComponents are reused the state that is inside them is reused as well. There were only 13 components ever created so if you select three of them, you will see them popping up again and again as you scroll.

When you think about it, with this implementation you are actually selecting components not items. And there is no longer a 1:1 relation between those two collections. There are 100 (or maybe 100K) items and only 13 ItemsComponent instances.

Here is the branch in the repo with the problem at hand: list-view-state-in-component branch.

The Solution

There are a couple of solutions, but they all ultimately boil down to:

Move the view-state (the selected field from our example) out of the component and make the component stateless.

We will refer to this as the view-state (for the lack of a better term) for all the information that was not originally in the model, but is still used in the component templates and application logic. In our case, this is the selected filed. This info might also be bound to any input view in you template.

Note: One alternative approach that comes to mind is to try to "clean" the components when they are reused. However, this means you will inevitably loose the state they were in. It is just not possible to store 100 items in 13 one-item boxes.

Keep View-State in the Model

Maybe the easiet way to implement solution is just to add the view-state in the model items:

export interface Item {
  name: string;
  selected?: boolean;
}

You will have to change the component template to get/set the selected field from the item:

<StackLayout orientation="horizontal" class="item"
    (tap)="item.selected = !item.selected">
    <Label [text]="item.name"></Label>
    <Label class="select-btn" 
        [class.selected]="item.selected"
        [text]="item.selected ? 'selected' : 'unselected'">
    </Label>
</StackLayout>

Problem solved! To make things clearer, we went from ngFor with stateful components:

to ListView (still ngFor in the Web version though) with stateless components:

Note: The web templates still uses ngFor. It works perfectly with the stateless version of ItemComponent. Here is the branch in the repo: list-view-state-in-model branch.

For simple cases, this is a valid solution, but you might not want to mix view-state properties with the model. Or it might be the case that you get your model object directly from a service and you want to keep them "clean" from additional fields so that you can send them back at some point.

Attach View-State to Item

Another approach would be to have the view-state as a separate view-state object and "attach" it to the model-object when it is used in the UI. This will give us some separation between model and view-state properties and an easy way to clean model objects if needed.

To make things even easier I have created a TypeScript decorator that will do the job for me. Here is how this goes:

  1. We decorate a dedicated view-state property in the component (let's call itvs for short) with our special decorator: @attachViewState.
  2. We give the decorator a factory function to create the default view-state object for items. It will use it whenever it needs to create a view state object for an item.
  3. We give the decorator the name of the actual model property of in the component. Usually this is @Input property - in our case "item."
  4. The decorator will create (using the factory) and "attach" a view-state object to each item passed to the component ("attach" is a fancy way of saying it will set a "__vs" property to the item).
  5. The decorator will also change the getter and setter for the vs property, so that they access the view-state object that lives inside the item. This will make it easier to use the view-state inside the component's template.

Sound complicated? Actually, it is quite easy to use:

interface ItemViewState {
  selected?: boolean;
}
const ItemViewStateFactory = () => { return { selected: false } };
@Component({ ... })
export class ItemComponent {
  @attachViewState<ItemViewState>("item", ItemViewStateFactory)
  vs: ItemViewState;

  @Input() item: Item;
}

And in the template we just use vs for the view-state props and item for data props:

<StackLayout orientation="horizontal" class="item"
    (tap)="vs.selected = !vs.selected">
    <Label [text]="item.name"></Label>
    <Label class="select-btn" 
        [class.selected]="vs.selected"
        [text]="vs.selected ? 'selected' : 'unselected'">
    </Label>
</StackLayout>

Here, also, is the code for the @attachViewStatedecorator (T being the type of the view-state object). There are also the getViewState and cleanViewState helper methods for getting and cleaning the view-state object from the model.

const viewStateKey = "__vs";

export function attachViewState<T>(attachTo: string, defaultValueFactory?: () => T) {
    return (target: any, key: string) => {
        const assureViewState = (obj) => {
            if (typeof obj[attachTo][viewStateKey] === "undefined") {
                // console.log("> creating default view sate");
                obj[attachTo][viewStateKey] = defaultValueFactory();
            }
        }

        // property getter
        var getter = function () {
            // console.log("> getter");
            assureViewState(this);
            return this[attachTo][viewStateKey]
        };

        // property setter
        var setter = function (newVal) {
            // console.log("> setter");
            assureViewState(this);
            this[attachTo][viewStateKey] = newVal;
        };

        // Delete property.
        if (delete target[key]) {
            // Create new property with getter and setter
            Object.defineProperty(target, key, {
                get: getter,
                set: setter,
                enumerable: true,
                configurable: true
            });
        }
    }
}

export function getViewState<T>(model: any): T {
    return model[viewStateKey];
}

export function cleanViewState(model: any) {
    return model[viewStateKey] = undefined;
}

Again, the code is here: list-view-state-in-model-decorator branch

Note: There are other tactics as well. For example:

  • Maintain a completely separated list of view-state objects in your container component and pass them both as inputs of the template.
  •  "Wrap" your model item into view-model items using composition, thus keeping the model itemscompletely untouched.

Bonus (the Case for Stateless Components)

It's worth noting that these solutions work flawlessly in the Web version of our app where *ngFor is still used. In fact, in many cases, having stateless components will actually lead to better app architecture.

Here is an example. Consider the next feature in our app: we have to gather all selected items and show them in a different view (or just alert them for now).

If the "selected" information resides inside the components we would have to either:

  • Use @ViewChildren to query the components to figure out which are the selected items. Eew!

  • Expose some kind of event to notify us whenever an item is selected and handle it in the container component. This means that we will hold the "selected" information in two different places (once in the the ItemComponent and once in the container component). Eeeew!

On the other hand, if you have a statelessItemComponent and hold the state separately, you will have an easier time working with the data. Here is how the code looks like if you are using the "decorator" approach from above (we use the getViewState method from the helper util to get the view-state):

// In container-component template (home.component.html):
...
<button (click)="checkout()">checkout</button>
...
// In container-component code (home.component.ts):
...
checkout() {
  const result = this.items
    .filter(item => {
      const vs = getViewState<ItemViewState>(item);
      return vs && vs.selected;
    })
    .map(item => item.name)
    .join("\n");
  alert("Selected items:\n" + result);
}

The code of the final project: master branch.

Summary

Here are the key take-aways:

  1. When switching from *ngFor to ListView keep in mind that it will recycle the components in your template. Any state inside them (all non-@Input properties that are not bound in the template) will survive the recycling and will probably cause unwanted behavior.
  2. Consider using stateless (a.k.a presentation) components.***It will save you from problems in 1. as all state will be passed as input. It also follows the Smart Components vs Presentational Components guides and will lead to better architecture of your app.
  3. Bonus: Sharing code between web and mobile with NativeScript is now really easy. Not really on this topic...but I'm excited about it and decided to share!
NativeScript mobile app Template Object (computer science) Web Service Branch (computer science) Property (programming)

Published at DZone with permission of Alexander Vakrilov, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Using GPT-3 in Our Applications
  • 3 Main Pillars in ReactJS
  • Build an Automated Testing Pipeline With GitLab CI/CD and Selenium Grid
  • HTTP vs Messaging for Microservices Communications

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: