createContainer and infinite scrolling

Hey,
I’ve created a container for my chat application and try to implement infinite scrolling. In my case, the subscription limit will be changed until the user reaches the top. This is my container component:

var ChatContainer = createContainer((props) => {

const limit = new ReactiveVar(15);
const chatSub = Meteor.subscribe("chat", props.doc._id,limit.get());
const messages = Messages.find({chatId: props.doc._id},{sort:{createdAt:1}}).fetch();

const changeLimit = function() {

    var curLimit = limit.get();
    limit.set(curLimit + 20);
};


return {
    chatSub,
    messages,
    changeLimit
};

}, Chat);

Now I’m having some trouble: Everytime I call changeLimit() within my chat component, the limit constant is defined again and set to 15. I think this is usual Tracker behavior, because the reactive value forces a rerun of the container function. But what’s the recommended way of handling such cases?

Just move the definition to a non-reactive context: (note usage of var)

var limit = new ReactiveVar(15);

var ChatContainer = createContainer((props) => {...};

Yep, that was my temp fix, the problem here is that the value is stored even if I’ve left the chat. So if I open another chat, it uses the last limit of the chat before.

Do I have to use componentWillUnmount to set the limit to it’s old value again? I’m just wondering if this is the right way, it feels uncomfortable.

The problem here is that you are keeping state inside the createContainer function. Like you mentioned, this function get’s rerun every time the data it is tracking is invalidated so the limit const will always be 15. I’d recommend storing the limit state in a component that wraps the ChatContainer. Here is an example:

Store state in top level non-reactive component:

class OuterChatContainer extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      limit: 15
    }
  }
  changeLimit(newLimit) {
    this.setState({limit: newLimit});
  }
  render() {
    const { limit } = this.state;
    return <ChatContainer limit=limit changeLimit=changeLimit />
  }
}

Whenever you need to update the limit just call the changeLimit function in a child component and the new limit will be passed down to the ChatContainer.

4 Likes

To store multiple state , use ReactiveDict instead of ReactiveVar

const initialState = {
  limit: 15,
};
const AppState = new ReactiveDict(initialState);

class Chat extends Component {
  // ...
}

var ChatContainer = createContainer((props) => {
  const { _id } = this.props;
  const limit = AppState.get('limit');
  const chatSub = Meteor.subscribe("chat", _id, limit);
  const messages = Messages.find({ chatId: _id }, { sort: { createdAt: 1 } }).fetch();

  return {
    isLoading: !chatSub.ready(),
    messages,
    loadMore: () => {
      AppState.set('limit', limit + 15)
    }
  };

}, Chat);

2 Likes

Here is code from my app.

It is a loadMore pagination model for products.

I don’t use ReactiveVar. Just use normal variable and increment it when loadMore executed.
And when loadMore executed, I subscribe to new publication with new page and limit parameter to get the new products. So we get new products in client Collection.

See my code.

createContainer

const initPage = 1;
let nextPage = initPage;
let productsHandles = [];
let currentRoutePath = null;

export default createContainer((params) => {
    const pageHandle = Meteor.subscribe('front.pages.products');

    if (currentRoutePath !== FlowRouter.current().path) {
        currentRoutePath = FlowRouter.current().path;
        clearProducts();
    }

    const limit = 20;
    //First subscription
    const productsHandle = Meteor.subscribe('front.products.all', initPage, limit, tag);
    productsHandles.push(productsHandle);
    const countsHandle = Meteor.subscribe('front.products.count', tag);
    const tagsHandle = Meteor.subscribe('front.tags.all');

    const allProductsCount = Counts.get('front.products.count');
    const data = {
        loading: !productsHandle.ready() || !countsHandle.ready(),
        page: Pages.findOne({name: 'products'}),
        products: ProductsCollection.find({}, {sort: {code: 1, name: 1}}).fetch(),
        allProductsCount,
        isLoadMore: ProductsCollection.find().count() === allProductsCount,

        //When loadMore add another subscription
        loadMore(){
            ///
            //Add another subscription
            ///
            const productsHandle = Meteor.subscribe('front.products.all', ++nextPage, limit);
            productsHandles.push(productsHandle);
        }
    };
    return data;

    function clearProducts() {
        productsHandles.forEach((item) => item.stop());
        productsHandles = [];
        nextPage = initPage;
    }
}, Products);

Subscription

Meteor.publish('front.products.all', function (page, limit, tag) {
    check(page, Match.OneOf(Number, String));
    check(limit, Match.OneOf(Number, String));
    check(tag, Match.Maybe(String));

    page = +page;
    limit = +limit;

    const skip = (page - 1) * limit;
    const sort = {name: 1};
    let filter = {};
    if (tag) {
        filter.tags = new RegExp(`^${tag}$`, 'i');
    }

    return Products.find(filter, {sort, skip, limit});
});

Meteor.publish('front.products.count', function (tag) {
    check(tag, Match.Maybe(String));
    let filter = {};
    if (tag) {
        filter.tags = new RegExp(`^${tag}$`, 'i');
    }
    Counts.publish(this, 'front.products.count', Products.find(filter));
});
1 Like

Yep, doing now the same thing: Setting a new subscription instead of using a reactive variable.

Are you guys providing a button to click to Load More, or are you using a technique to detect the user’s browser scroll position within the component? I would be interested to find out your coding technique in detecting the browser scroll position to load more items within your react component.

We’ve implemented an infinite scrolling layout (bottom to top). I didn’t find any good React component for it, so I’ve added an event handler to it.

Chat.jsx

class Chat extends React.Component {


   componentDidMount() {



    //Handling infinite scrolling

    var oldHeight;

    var runForcePosition = () => {

        if(this.state.loadingMessages) {
            var newHeight = $("#messages").get(0).scrollHeight;
            $('#messages').scrollTop(newHeight - oldHeight); //subtract the 50px of the loading indicator



                setTimeout(runForcePosition,10);


        }

    };


    $('#messages').scroll((e) => {

        if(e.target.scrollTop < 50 && this.props.messages.length >= initialLimit) {


                //Here we force infinite scrolling...

                if(!this.state.loadingMessages) {
                    this.setState({loadingMessages:true});



                    window.setTimeout(() => {
                        oldHeight = $("#messages").get(0).scrollHeight;
                        runForcePosition();

                        this.initLimit+=20;

                        this.props.changeLimit(this.initLimit);
                    },1500);

                }

        }
    });


}

render() {
  return <div>
       <div id="messages">
         <Message/>
         <Message/>
         .....
      </div>

  </div>;
  }
}
1 Like

I see a few problems with this solution. First, setTimeout is generally a bad idea and you’re using it twice. You should avoid setTimeouts because if your processor is pegged, there’s no guarantee that you setTimeout will finish in exactly that time; a better approach would be to set up an asyc call.

Secondly, it looks like you’re using jQuery with React. This is another bad practice (in my opinion) because it does quite a bit of searching for elements in the DOM. What if the #messages component hasn’t rendered yet? You should typically avoid jQuery altogether when writing React apps because React is constantly “diffing” the virtual DOM on it’s own. Let React take care of it, try using a ref instead.

Not trying to pick apart your code, just a couple of things to think about :slight_smile: cheers!