What's the standard way to update a publication (using React)?

  constructor () {
    super()
    this.state = {
      limit: 10,
      subscriptions: {
        user: Meteor.subscribe('user'),
        posts: Meteor.subscribe('posts', limit)
      }
    }
    this.handleLoadMore = this.handleLoadMore.bind(this)
  }
  handleLoadMore () {
    const { limit } = this.state
    this.setState({
      limit: limit + 10,
      'subscriptions.posts': Meteor.subscribe('posts', limit + 10)
    })
  }

The above code works great but I’m curious if there is another established “best practice”?

Hey, try using createContainer to handle your subscriptions: https://guide.meteor.com/react.html#using-createContainer

Hope it helps :slight_smile:

If you do not want to go the redux route use createContainer.

An app setup could be like this:

ReactRouter initiates route:

<Route path="/somepath" component={someContainer}/>

Container then initiates a page

export default createContainer(() => {
  //do subscriptions here
  return {
    collection
  };
}, somePage);

The container would load the page and from there you can add components and even more containers depending on app complexity.

Cheers

A bit of code. I don’t use state to keep track of the page number, it’s recommended to use some kind of ‘store’ so that you can access those values across pages/components or in case you want to store the page number in case the client goes back to the page. For this you can use Redux or simply a Reactive Dictionary. I use a reactive dictionary (LocalSate) in the following example.

import Constants from '../../../api/constants.js';
import LocalState from '../../../api/local-state/client/api.js';

//------------------------------------------------------------------------------
// PAGE COMPONENT DEFINITION:
//------------------------------------------------------------------------------
/**
* @summary Contains all the 'Page' logic and takes care of view dispatching.
* LocalState.set() must be used here and can't be used in any child component!
*/
class FeedPage extends Component {
  // See ES6 Classes section at: https://facebook.github.io/react/docs/reusable-components.html
  constructor(props) {
    super(props);
    this.handleLoadMore = this.handleLoadMore.bind(this);
  }

  handleLoadMore {
    // Get context
    const { pageNumber } = this.props;
    LocalState.set('feed.pageNumber', pageNumber + 1);
  }

  render() {
    return (
      <View
        {...this.props}
        handleLoadMore={this.handleLoadMore}
      />
    );
  }
}

FeedPage.propTypes = {
  pageNumber: PropTypes.number.isRequired,
  markers: PropTypes.array.isRequired,
};
//------------------------------------------------------------------------------
// PAGE CONTAINER DEFINITION:
//------------------------------------------------------------------------------
/**
* @summary Wrapper around the 'Page' component to handle Meteor reactivity
* (component-level subscriptions etc etc), and pass data down to 'Page'
* component. Warning, LocalState.set() can't be used here!
*/
const FeedPageContainer = createContainer(() => {
  // Get local state
  const pageNumber = LocalState.get('feed.pageNumber') || 1;

  // Subscribe
  const params = {
      limit: pageNumber * Constants.FEED_RECORDS_PER_PAGE,
   };
  const subs1 = Meteor.subscribe('Markers.publications.getMarkersForFeedPage', params);
  const markers = Markers.collection.find({}, { sort: { date: 1, time: 1 } }).fetch();

  return {
    pageNumber,
    markers,
  };
}, FeedPage);

export default FeedPageContainer;

Definition of LocalState (LocalState.api.init() must be called on client startup in order to initialize LocalState)

imports/api/local-state/client/api.js

import { ReactiveDict } from 'meteor/reactive-dict';
import Constants from '../../constants.js';

/**
* @summary Handles client side app state across pages / components.
* @namespace LocalState.
*/
const LocalState = new ReactiveDict('LocalState');
LocalState.api = {};

//------------------------------------------------------------------------------
/**
* @summary Initialize local state for the given field name.
*/
LocalState.api.clearField = (fieldName) => {
  console.log(`[local-state] initialize state for ${fieldName}`);

  switch (fieldName) {
 
    case 'feed':
       LocalState.set('feed.pageNumber', 1);
      break;

    /* Initialize state for other pages here */

    default:
      break;
  }
};
//------------------------------------------------------------------------------
/**
* @summary Initialize local state.
*/
LocalState.api.init = () => {
  console.log('[local-state] init');

  LocalState.api.clearField('feed');
  /* Initialize localState for other fields/pages here */
};
//------------------------------------------------------------------------------

export default LocalState;

1 Like

Ok so I took the advice in this thread and am switching from TrackerReact to createContainer. However, updating publications/subscriptions is even more unclear now. Here’s what I’ve got for one of my components. If I can figure this out, all other pub/subs in my app will be a piece of cake.

I’m trying to let the user filter through a list based on various criteria. Pretty common use case.

Here’s the publication on the server:

Meteor.publish('challenges.explore', (language, tag, sort, limit) => {
  return ChallengesCollection.find(
    { language: language, tags: { $in: tag } },
    { sort: sort, limit: limit }
  )
})

The rest is the client side component with container (relevent parts):

  constructor () {
    super()
    this.state = {
      language: 'javascript',
      sortBy: { 'stats.completed.total': -1 },
      tag: defaultTags,
      limit: 5
    }
  }
  handleUpdateFilter (e) {
    e.preventDefault()
    const { language, tag, sortBy, limit } = this.state
    Meteor.subscribe('challenges.explore', language, tag, sortBy, limit)
  }
export default createContainer(() => {
  const subscriptions = {
    challenges: Meteor.subscribe('challenges.explore', 'javascript', defaultTags, { 'stats.completed.total': -1 }, 5),
    currentUserPlaylists: Meteor.subscribe('playlists.user')
  }

  return {
    challenges: ChallengesCollection.find().fetch(),
    currentUser: Meteor.user(),
    currentUserPlaylists: PlaylistsCollection.find().fetch(),
    isReady: subscriptions.challenges.ready()
  }
}, Explore)

When I invoke handleUpdateFilter, nothing happens.

PS: Once I figure out containers, I swear I’m doing an in depth YouTube video on the process. It’s something that needs better documentation and examples.

Why not split up your components? Filters can be passed by a parent component. I usually build a filter component (SomethingSearch) containing filter state options (search, load more, sort) and pass them to a data component (SomethingList). No need for handleUpdateFilter and overriding a subscriptions inside a component.

Here’s an old example (ignore Mantra implementations):
Filter component: https://github.com/BitSherpa/EverestCamp/blob/master/client/modules/posts/components/PostSearch.jsx
Something similar createContainer: https://github.com/BitSherpa/EverestCamp/blob/master/client/modules/posts/containers/PostList.js

In my opinion there are 2 issues with your approach:

1- All subscription must be inside your Container component, not in your page component. The container is in charge of re-runnig every time some reactive variable changes inside it (for instance, a session variable, a reactive dictionary, subscriptions, etc.). I haven’t checked the implementation of createContainer but I guess it’s a wrapper around Tracker.autorun…

2- You are using ‘this.state’ rather than some reactive variable(s) to hold the client state; you can do it that way but for that you’ll need to follow the approach proposed by @janikvonrotz, ie, your page should be a parent component of your container so that you can pass state down (as props) from your page component to container component. Going back to the approach you are trying to follow, we are exactly in the opposite situation: your container is a parent component of our page so you won’t be able to pass ‘this.state’ to the container. In other to accomplish that, we’ll need to use some GLOBAL variables such as session variables, reactive dictionary or the store provided by Redux. In the example below I’ll consider the simplest approach using reactive variables. Now, notice that this is an anti-patern but I think it’s the simplest way to understand the flow :slight_smile:

// Using session vars is an anti-patern! Once you get the idea try to replace reactive variables with a reactive dictionary or redux
// These are GLOBAL variables so we can set and access them wherever we want to!
Session.set('language', 'javascript');
Session.set('sortBy', { 'stats.completed.total': -1 });
Session.set('tag', defaultTags);
Session.set('limit', 5);

class Explore extends Component {
  constructor () {
    super()
    /* this.state = {
      language: 'javascript',
      sortBy: { 'stats.completed.total': -1 },
      tag: defaultTags,
      limit: 5
    } */
    this.handleLimitChange = this.handleLimitChange.bind(this);
    // add more methods for handling the rest of the variables
  }

  handleLimitChange (e) {
     // Previous value for limit
     const { limit } = this.props;
     // 2- By doing this, we'll force the container to re-run
     Session.set('limit', limit + 5);
  }

  /* handleUpdateFilter (e) {
    e.preventDefault()
    const { language, tag, sortBy, limit } = this.state
    Meteor.subscribe('challenges.explore', language, tag, sortBy, limit)
  } */

  render () {
   // 6- we'll get the new values for language, tag, ... here to be used in you view component
   const { language, sortBy, tag, limit } = this.props;
   return (
    // Use variable in your component here
    // Call this.handleLimitChange inside your view to trigger variable change
    // 1- By clicking the button we'll call handleLimitChange method
    <button onClick={this.handleLimitChange}>
     LOAD MORE
    </button>
  );
}

const ExploreContainer = createContainer(() => {
  // 3- Every time any of these variables changes, it will force the container to re-run
  const language = Session.get('language');
  const sortBy = Session.get('sortBy');
  const tag = Session.get('tag');
  const limit = Session.get('limit');

  // 4- By injecting the new values, we'll force the subscription to rerun
  const subscriptions = {
    challenges: Meteor.subscribe('challenges.explore', language, defaultTags, tag, limit),
    currentUserPlaylists: Meteor.subscribe('playlists.user')
  }

  return {
    // 5- Re-inject values as props for the page component
    language,
    sortBy,
    tag,
    limit,
    challenges: ChallengesCollection.find().fetch(),
    currentUser: Meteor.user(),
    currentUserPlaylists: PlaylistsCollection.find().fetch(),
    isReady: subscriptions.challenges.ready()
  }
}, Explore)

export default ExploreContainer;
1 Like