Refresh data after redirect (go back)

Hi,
I need te refresh data when I redirect page. Let me explain
I have 2 pages;
“products” listing the products
“add-product” that creates products

When I create a product, I would like to go back to the list of products and update this list via the API

This is my button to go to

<ion-button color="light" fill="clear" [routerLink]="['/add-product']">
    <ion-icon slot="icon-only" name="bag-add-outline"></ion-icon>
</ion-button>

Everything I tested to get back to my list

this.router.navigate([`/products`], {state: {reload: true}}).then();
this.navController.navigateRoot([`/products`], {state: {reload: true}}).then();
this.navController.back();
this.navController.navigateBack([`/products`]).then();

And on my products.page.ts I tried this but none work, they never get called

    ngOnInit() {
        console.log('ngOnInit', history.state);
        this.load();
    }

    ionViewWillEnter(): void {
        console.log('ionViewWillEnter', history.state);
    }

    ionViewDidEnter(): void {
        console.log('ionViewDidEnter', history.state);
    }

    ngAfterViewInit(): void {
        console.log('ngAfterViewInit', history.state);
    }

How can i refresh my data please ?

I disagree. You need to refresh data when data changes. It may seem like a trivial distinction, but please bear with me.

“When I change pages” is a view concept. “Refresh data” is a model concept. Clear separation between view and model is crucial to clean design. I would structure this like so:

interface Product {
  id: string;
  name: string;
  ...
}

class ProductService {
  private allProducts$ = new BehaviorSubject<Product[] | undefined>();

  constructor(private http: HttpClient, ...) {}

  watchAllProducts(): Observable<Product[]> {
    return this.allProducts$.pipe(filter(maybe => !!maybe)); 
  }

  productById(id: string): Observable<Product> {
    // could use allProducts$ as a cache here if desired
  }

  fetchAllProducts(): Observable<Product[]> {
    this.http.get<Product[]>(API_URL).subscribe(products => this.allProducts$.next(products));
    return this.watchAllProducts();
  }

  // there are a bunch of reasonable things for this to return
  addProduct(newprod: Product): Observable<Product[]> {
    // could do the HTTP post first, or you could just add to allProducts$ optimistically
    return this.watchAllProducts();
  }
}

@UntilDestroy()
class ProductListPage {
  products: Product[] = [];

  constructor(private producter: ProductService) {
    producter.watchAllProducts().pipe(untilDestroyed(this)).subscribe(ps => this.products = ps);
  }
}

class ProductDetailPage {
  constructor(private producter: ProductService) {
  }

  registerNewProduct(p: Product): void {
    this.producter.addProduct(p).subscribe();
  }
}

Now your view level is free to do whatever it wants whenever it wants. You don’t care about lifecycle events, you don’t need to fight with the router. When new products are added from anywhere, they get reflected in the ProductListPage when it is being rendered. If you add a refresh button somewhere - anywhere in the app - that calls fetchAllProducts, the newly fetched data gets reflected in ProductListPage.

You can change caching and fetching strategies, add offline storage, with absolutely zero impact on any of your page logic.

2 Likes

OMG I’m a noob I think. Thank you for your answer, I can see more clearly in the way of doing things.
There are however some things that I don’t understand, notably “@UntilDestroy ()”, is this a plugin?
What is “filter (maybe => !! maybe)” on the watchAllProducts function?

In the ProductListPage, if I subscribe to watchAllProducts, fetchAllProducts is never called. So I never have any data ?!
If I add a refresher that calls fetchAllProducts, the observable watchAllProducts this mulitplie and I get loads of calls each time

There are two types of people in the world: those who go around dividing everything they see into two types, and those who don’t. As you can see, I tend to be one of those who does that.

So one way of dividing Observables into two types is: (a) single-use ones that are really glorified Promises, from which they can also be easily converted back and forth, and (b) long-lived ones that the subscriber has basically no clue when will stop emitting things.

An HttpClient request is an (a). You don’t need to stress out about unsubscribing from (a) Observables. It is best to avoid keeping subscriptions to (b) Observables hanging around because they represent a resource leak. Dealing with that is a bit unwieldy, so various idioms sprang up to make it somewhat easier, and @UntilDestroy is the smoothest I’ve seen, so I use it. It comes from ngneat/until-destroy.

We fundamentally have a tristate situation here:

  1. App has just started, nobody has asked the server for anything yet, we have no idea what products there are.
  2. For some reason (overly-aggressive search criteria, for example), the set of products that we are considering “all” is empty.
  3. We have some products.

3 is easy. Distinguishing between 1 and 2 is less so. One way of doing so is to reserve [] for state 2, and use null or undefined as a sentinel for state 1. Ordinarily when a service is providing arrays of things, it’s likely that consumers of it are going to try to iterate across it (using *ngFor, for example). Feeding null or undefined to them in that situation is dangerous, because it’ll make Angular puke. There’s another way of doing this with ReplaySubjects instead of BehaviorSubjects, but a BehaviorSubject needs an initial state. allProducts$ is allowed to hold a Product[] | undefined for that reason. However, we don’t want anybody calling watchAllProducts to know this, because it just burdens them with implementation details. So, that filter effectively makes the return value from watchAllProducts wait until it has something to say before it says anything. It won’t just say “I have nothing to say”. The double negation operator (!!) is one of an admittedly bad bunch of alternatives for saying “be true if my argument is truthy”, because JavaScript has very loosey-goosey notions of “truthiness” and “falsiness” that can get us into trouble.

The timing on when fetchAllProducts should be called is outside the scope of our conversation so far. Maybe you want to do it at app startup, maybe you want to do it periodically, maybe there’s a separate product search page that makes more sense: I don’t know. One nice thing about this design is that you can do it whenever and wherever makes sense to you. You’re right: it has to be done at least sometime.

It’s true that the Observable that you get from watchAllProducts will emit the freshest set of data whenever anybody calls fetchAllProducts. That’s sort of the point of all this.

There is a potentially problematic situation where subscribing to the same HTTP request in multiple places causes multiple HTTP requests. It happens because of hot and cold Observables, and can be mitigated in many ways - the share operator is probably the simplest. However, it shouldn’t be biting you here. Avoiding that trap is why the Observable coming back from the HTTP get isn’t ever returned to the outside world. fetchAllProducts doesn’t return what one would ordinarily think it would - it returns the same BehaviorSubject as watchAllProducts precisely so that there’s only one subscription to that request.

So by “calls” do you mean “HTTP requests over the network” or something else?

1 Like

Thank you very much for your response and your time. I understand better the logic there must be.

Yes that makes sense… :sweat_smile:

After spamming my refresh button which calls fetchAllProducts, I add a product and for validation I have a single HTTP request, 1 response from watchAllProducts and 25 responses from the fetchAllProducts obervable. I need to unsubscribe from this one after each “resfresh click”? Or does it not represent a performance risk?

Last questions and I don’t bother you anymore:
For the update of the product I need to return that the modified and up-to-date product, would this be a good practice?

updateProduct(id: number, product: Product) {
        return this.http.put(`${this.urlApi}/${this.resource}/${id}?apiVersion=2`, product).pipe(
            map(res => {
                // Update product list
                this.fetchAllProducts().subscribe();
                return res as Product;
            })
        );
    }

So, unlike adding a product, I subscribe to the HTTP request and not to the “watchAllProducts”

Do you have a resource where I can learn more about this concept? I have never seen him in my classes. :cry:

OK, maybe this would be less error-prone if we did this instead:

fetchAllProducts(): Observable<Product[]> {
  return this.http.get<Product[]>(API_URL).pipe(
    share(),
    tap(prods => this.allProducts$.next(prods));
}

That should make it harder to accidentally leak subscriptions, but keep the same basic behavior of “whoever calls fetchAllProducts, it causes all subscribers to watchAllProducts to receive the result”.

A few notes:

  • you shouldn’t need to be passing id here, it would typically be included in the Product. The fewer arguments you have to a function (I do my best to avoid having more than one, if I can), the less you have to worry about putting them in the proper order.
  • do everything in your power to avoid casts (res as Product), and you really shouldn’t need one here, because…
  • PUT requests shouldn’t be returning anything, IMHO. I find following this rule helps me avoid accidentally breaking the idempotance part of the fundamental contract of PUT on the server side: you should be able to make the same PUT request 1, 5, or 1000 times in a row and end up in the same place.
  • I would not call fetchAllProducts in updateProduct, because it’s too heavy. Instead, I would manually walk through allProducts$.value (or keep it in a hash instead of an array) and update the local notion of allProducts$ via next.

So, I would do something more like this:

updateProduct(newb: Product): Observable<Product> {
  return this.http.put(URL).pipe(map() => {
    let allprods = clone(this.allProducts$.value) || [];
    let nprods = allprods.length;
    let foundit = false;
    for (let i = 0; i < nprods; ++i) {
      if (allprods[i].id === newb.id) {
        allprods.splice(i, 1, newb);
        foundit = true;
        break;
      }
    }
    if (!foundit) {
      allprods.push(newb);
    }
    this.allProducts$.next(allprods);
    return newb;
  });
}

thank you very much for your feedback!