Meteor/Svelte Reactive Subscriptions

Hey all!

I am currently rebuilding an app that was built in Blaze to Svelte, and WOW what a difference! I am working on pagination for a simple user list. Currently I just want to give users the ability to decide how many users to display at one time. It seems to work just fine when increasing in increments; however, I am seeing that when I go down in increments the number of users published does not change.

Here is how I am currently handling it…obviously Meteor/Svelte is super new…and I am sure there may be a better way:

ManageUsers.svelte

<script>
  import { Meteor } from 'meteor/meteor';
  import { resultCount,filterResults } from '../../Partials/Stores/pageCount';
  import { onMount,afterUpdate } from 'svelte';
  import { useTracker } from 'meteor/rdb:svelte-meteor-data';

  let users;
  let numberPerPage;
  let pageObj;

  resultCount.subscribe(value => {
		numberPerPage = value;
  });

  filterResults.subscribe(value => {
    pageObj = value;
  });

  $: users = useTracker(() => Meteor.users.find({
      _id: {
        $ne: Meteor.userId()
      }
    }).fetch());

  afterUpdate(async () => {
    if(pageObj){
      Meteor.subscribe('AllUsersPaged', pageObj);
    }
  });

  function numberOfResults(event) {
    const target = event.target;
    
    resultCount.update(n => n = target.value);
    filterResults.update(n => n = {
        sortMod: {sort: {"profile.lastName" : 1}},
        pageCount: 1,
        numberPerPage: numberPerPage
    });
  };
</script>

{#if $users}
    {#each $users as user}
        <SingleUserList user={user} />
    {/each}
{/if}
<label for="resultsNumber">Number of Results {numberPerPage}</label>
<select id="resultsNumber" class="form-control" on:change={numberOfResults}>
  <option value="10" selected>10</option>
  <option value="20">20</option>
  <option value="50">50</option>
  <option value="100">100</option>
</select>

pageCount.js

import { writable } from 'svelte/store';

export const resultCount = writable(10);

export const filterResults = writable({
  sortMod: {sort: {"profile.lastName" : 1}},
  pageCount: 1,
  numberPerPage: '10'
});

publish.js (on server)

Meteor.publish('AllUsersPaged', function(pageObj){
  pageObj.numberPerPage = parseInt(pageObj.numberPerPage);
  let skipCount = pageObj.pageCount * pageObj.numberPerPage;

  console.log(pageObj)

  if(skipCount >= pageObj.numberPerPage){
    skipCount = skipCount - pageObj.numberPerPage;
  }

  if(pageObj.numberPerPage){
    return Meteor.users.find({},{
      sort: pageObj.sortMod.sort,
      skip: skipCount,
      limit:pageObj.numberPerPage,
      collation: {
        caseFirst: "off"
      }
    });
  }
});

Just trying to sort out how to update publications really. It seems like the afterUpdate method gets me 1/2 of the way there…but I am confused why after pushing a lesser number to the limit in Mongo it does not “re-subscribe” to the publication. I have been logging the “pageObj” in the publication and can verify that the data is being received properly.

Any help would be GREATLY appreciated!!

Minimongo in the client merges all the content already sent to it that belongs to the same collection.

Any filtering must be adjusted on the client side as if you are filtering the actual collection

1 Like

Hmmmm I suppose I could write extra functionality that could re-filter the already published data. Though this was possible with Blaze…

You shouldn’t put Meteor.subscribe in your afterUpdate(), or in any Svelte callback for that matter. You can put it in a reactive update, like this:

$: pageObj && Meteor.subscribe('AllUsersPaged, pageObj);

However, that won’t work quite the way you might expect from a Tracker.autorun, because it will not automatically unsubscribe from the previous subscribe call, until the component is destroyed. (I have looked into making rdb:svelte-meteor-data handle this case, but this is not possible without instrumentation to the Svelte compiled output.)

You will need to do something like this instead, though I’m not 100% sure whether the circular dependency on subscription might cause a problem:

let subscription;
$: {
  if (pageObj) {
    subscription.stop();
    subscription = Meteor.subscribe('AllUsersPaged', pageObj);
  }
};

Finally, as a sidenote, my package automatically makes all cursors behave as Svelte stores, so you can simplify this code:

  $: users = useTracker(() => Meteor.users.find({
      _id: {
        $ne: Meteor.userId()
      }
    }).fetch());

To this:

  $: users = Meteor.users.find({
      _id: {
        $ne: Meteor.userId()
      }
    });

You should also bear in mind that the way you are passing the user object to SingleUserList will cause your entire user list to be rerendered whenever any one of them changes. The way to work around this is to either just pass in an identifier (having SingleUserList fetch the data on its own), give SingleUserList the immutable flag, or have the object be destructured by Svelte when passing it in.

2 Likes

Oh wow! Now this is a GREAT answer! Just as an FYI I got the idea of putting Meteor.subscribe inside of the Svelte callback from the current Svelte tutorial here on meteor.com. Not sure who we should tell about that…but being the fact that you wrote the package…I have a strong feeling I can take your word for it! :crazy_face::rofl:

I am going to give all of this a try…but thank you for all of your development help to make the package in the first place! Dudes like you keep dudes like me excited about Meteor!!

1 Like

Addendum: I just realised that by changing the useTracker in the last example I gave to a regular cursor, the call to Meteor.userId() will no longer be made reactively, so switching users would not automatically update the query. So ignore that suggestion unless the user cannot switch accounts while the component is active,

1 Like

Ahhhh yes I will use .fetch()

@rdb

In step 10.3 we do the following

  onMount(async () => {
    Meteor.subscribe('tasks');
  });

Are you saying this should be refactored to the following

$:  Meteor.subscribe('tasks'); 

and it would have the same effect? when the component loads it will subscribe to the published data? and is that more inline with what you had in mind for your package?

3 Likes

One additional thing I might add…I was not able to get ready() to log true on the subscription. Also the subscription.stop() also did not have any impact, though the immutable idea seems to have worked.

I don’t recommend putting anything reactive inside a Svelte callback because the behaviour of Svelte inside them is not consistent (there are some pending issues and PRs regarding fixing the current behaviour).

If you put the Meteor.subscribe in the component (no $: prefix needed) scope, everything will work as it should, and the auto-unsubscribe will also take effect.

3 Likes

What have you tried? You would need to put the ready() call inside a useTracker for it to be reactive.

1 Like

Ok cool thanks for the info, I am working on the React tutorial update at the moment and then I wanted to get the Angular tutorials up to date. so this might take me a minute to get on the top of my to do list but I will make that change as soon as i can :slight_smile:

1 Like

Hi! Here’s a helper function that I personally developped and use in my Svelte/Meteor projects.

import { Tracker } from "meteor/tracker";
import { onDestroy } from "svelte";
import { writable } from "svelte/store";

/* Store the result of a Meteor reactive function in a read-only store
 * that exposes a `refresh` method to invalidate the computation.
 * Could be used with Svelte reactive statements like so:
 * $: store.refresh(...dependencies) */
export default function trackable(reactiveFn) {
  const store = writable();
  const computation = Tracker.autorun(() => store.set(reactiveFn()));
  const { subscribe } = store;

  onDestroy(() => computation && computation.stop());

  return {
    subscribe,
    refresh: () => computation && computation.invalidate(),
  };
}

And here’s how I would use it with your use case:

const usersSubStore = trackable(() => {
  if (pageObj) return Meteor.subscribe("AllUsersPaged", pageObj);
});
$: usersSubStore.refresh(pageObj);

const usersStore = trackable(() =>
  Meteor.users.find({ _id: { $ne: Meteor.userId() } }).fetch()
);
$: users = $usersStore

In a nutshell, a trackable store exposes a refresh method that invalidates the computation. When used in a Svelte reactive statement, it then invalidates each time the dependencies change. To access the reactive function’s return value, just subscribe to it with $.

I use it a lot and it works very well for me. I’m amazed at how easily I can create such neat abstractions with so little code.

1 Like

Hi @davidsavoie1, did you see the rdb:svelte-meteor-data package? It implements these exact things.

3 Likes

I did see it a while back, but at the time, when I tested it, I had some issues with it. I really needed to get going and had some ideas how to make it work, so I went on my own. Sorry I didn’t contribute to the package. :wink: I’ll check it out again soon.

1 Like

I’m using the reactive svelte package and it works great. For example, if I want to get a reactive cursor, I add something like:

  $: list = useTracker(() => Lists.find({ _id: id }).fetch());
  $: users = useTracker(() => Meteor.users.find().fetch());

I end up using these inside {#each} blocks. However, when I want a single document to be reactive it seems like overkill.

It would be great to do one or both of the following:

  1. Change my reactive query to something like findOne instead of find().fetch().
  2. Avoid using an {#each} loop when all I really want is to pass the document context into the HTML. Imagine something like {#with}.

Perhaps these are solved issues? Has anyone run into these?

You can use findOne inside useTracker, e.g.

$: list = useTracker(() => Lists.findOne(id));

You can wrap the HTML with {#if $list} to make sure there’s data and then use {$list.foo} to retrieve values.

3 Likes

Oh nice. I’ll give that a go. Would save a lot of each loops for sure!

1 Like

Just one other little tid-bit to go along with @jam answer (which is correct)…in case your newer to Meteor. Be sure that if you only need 1 document, its best to try and have your publication only return’s 1 document. :slight_smile:

Just worth mentioning!

1 Like

@rdb I just had a look at this package and it seems very promising, especially more concise and integrated. However, this line in the README makes me unconfortable using it in production:

This package is still experimental. Use at your peril.

Is the package stable and reliable?