React Container + Method?


#1

I have a route in my application that allows users to edit a product, but to do so there is a calculated value I need to get from a method, eg. Products.methods.getInformation.call(). This method returns some data generated from the Product that I need to pass to the Page for it to render.

I need to re-run the method whenever the product changes, but not otherwise… How would I go about adding this method call to the container code?

Here’s the current container code:

import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
import ProductEditPage from './../pages/ProductEditPage.jsx';

export default createContainer(({ params }) => {
  const handle = Meteor.subscribe('product-edit', { productId: params.productId });
  return {
    loading: !handle.ready(),
    user: Meteor.user(),
    product: Products.findOne({ _id: params.productId }),
  };
}, ProductEditPage);


Keeping React components pure in a Meteor app
#2

componentWillReceiveProps is your best bet. You can compare current vs next props then call your function


#3

In what way though?

Unless I have an incorrect understanding (which is very possible), componentWillReceiveProps would only get triggered if the container was re-run. When his method finishes and his callback is triggered, ProductEditPage would have already received the original props and thus he is stuck in the position of now having the product information he needs but without a way to tell the container to re-run so he can pass it on to his ProductEditPage.

@Siyfion is that a fair description of your issue?


#4

So to give you my way of solving this…

It depends how you want to handle the product changing… in my mind i would have a url like /products/[product_id]/edit so if I were going to edit another product I would just have a link to /products/1234/edit which would trigger the entire path/page to get re-run.

In terms of handling the above scenario though in terms of passing data form the methods callback into your component, I would do:

import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
import ProductEditPage from './../pages/ProductEditPage.jsx';

export default createContainer(({ params }) => {

    const productInfo = Session.get('product-info');

    const handle = Meteor.subscribe('product-edit', { productId: params.productId });

    if(!productInfo){
        Products.methods.getInformation.call({productId: params.productId}, (err, res) => {
            Session.set('product-info', res.productInfo);
        });
    }

    return {
        loading: !handle.ready() && !productInfo,
        user: Meteor.user(),
        product: Products.findOne({ _id: params.productId }),
        productInfo: productInfo
    };
}, ProductEditPage);

This I think would solve your issue of passing the product info which is received in the callback to your method. And once again, I would have the router be in charge of rending page views based on the url for switching products.


#5
export default class ProductEditPage extends Component {
  constructor(props) {
    super(props);
    this.updateSomething = this.updateSomething.bind(this);
  }
  
  componentWillMount() {
    this.updateSomething();
  }
  
  componentWillReceiveProps(nextProps) {
    // Compare current product and updated product (somehow)
    if (this.props.product._id !== nextProps.product._id) {
      // Different product ID - run method again
      this.updateSomething();
    }
  }
  
  updateSomething() {
    Products.methods.getInformation.call({}, (error, result) => {
      if (!error) {
        this.setState({ something: result.something });
      }
    });
  }
  
  render() {
    return <div>
      ...
    </div>
  }
}

Generally, in Meteor/React:

  • Don’t use Session
  • Don’t perform actions or method calls in the container, save it for dealing with data and reactive vars

#6

I think this is a chicken and egg problem and even more so if the method is used for fetching data.

I have long been using methods for the sole purpose of fetching non-reactive data from the server to spare me some performance and to me, it is a part of the “meteor data” stack which createContainer (formerly more verbosely named as getMeteorData which still is the underlying implementation) is designed for.

So not being able to place those method calls (without utilizing some external tracker aware state) within createContainer has always been a bummer for me.

To me, methods are a great interim path to Apollo data retrieval paradigms so it would have been awesome to find a way to use them directly within createContainer (without implementing it myself)


#7

First off let me just say thank you to everyone that chimed in with an answer; being in the UK it’s sometimes much easier for me to post a question at the end of the day and then look the next morning (hence the delay… sleep!).

I think that @abhiaiyer and @ffxsam have both come up with the solution that I intend to use, at first at least, I kinda can’t believe that I didn’t think of using componentWillRecieveProps myself, but now that you have pointed it out, it makes perfect sense.

As @serkandurusoy states, I’m interested to see what Apollo brings to this situation, if anything, as it’s the “future” of Meteor (supposedly).

Also, thank you @foxder, I think your solution achieves the same thing as the other, just I prefer keeping to the mantra of keeping methods out of containers, now that I have thought about it some more (I know, it’s not what I originally asked!).

Thanks all. :grin:


#8

@serkandurusoy I think you nailed it on the head. I was trying to solve his original question but it has been a bummer for me as well in my project. By not having your method calls in the container you are of course breaking the “pureness” of your component. This is where as a engineer you have to make a decision. Do you forsake the pureness of your component by having it be in charge of fetching non-reactive data or not?

@ffxsam for my own learning. Why "Don’t use Session" ? Is there an overarching issue here I am missing?

Also his method call returns data right? So I’m not sure its so easy as to say “Don’t perform actions or method calls in the container, save it for dealing with data and reactive vars”.


#9

Session is a global and you can’t really keep track of it very well.

If you are in dire need of tracking application state on a reactive source, you could use a reactive dictionary and if possible, scope it to a component rather than to the window.


#10

@foxder and @serkandurusoy:

Not every component has to be pure. I have a mix of containers, React classes, and stateless functions. It’s perfectly fine to have Meteor method calls in React’s lifecycle methods, but if you’d rather not, you can always use Redux for that! So, your container:

const MeteorContainer = createContainer(_ => {
  return {
    // Meteor data
  }
}, MyComponent);

// No mapping of Redux state, just passing in dispatch prop
export default connect()(MeteorContainer)

And then your MyComponent:

import { somethingHappened } from '/client/actions/basic-actions';

class MyComponent extends React.Component {
  componentWillReceiveProps(nextProps) {
    // some logic here
    this.props.dispatch(somethingHappened());
  }
  
  render() {
    // ...
  }
}

And then in /client/actions/basic-actions.js:

export const SOMETHING_HAPPENED = 'SOMETHING_HAPPENED';
export function somethingHappened() {
  // Redux thunk
  return dispatch => {
    myMethod.call(args, (error, result) => {
      // ...
    });
  }
}

#11

This is a nice example for actions but what @foxder and I emphasize is another usecase of a method

createContainer can

  • get data from session
  • get data from a reactive dict/var
  • get a subscription handle
  • get data from a collection
  • get data from Meteor.user
  • get data from any tracker tracked dependency

and pass it down to a component

all is good.

Now let’s say that dummy component also needs some data that’s available only through a method because it is not published. let’s say it is a list of “product categories” and it is not a publication since we don’t want it to be reactive and eat server resources.

It only “feels” natural to be able to do that call within the container creation because contextually, it is server data that is provided by meteor.

It has nothing to do with app (UI) state so wiring it up to redux feels off to me.

Wiring it up to session / reactivedict / (minimongo) etc also sounds off since data is data and state is state.

So the only (opinionated) “logical” option is to get it within the component lifecycle and that makes the component a not so that very dumb component, breaking some principles. And I certainly would not want to create a “method data container” just for this purpose either.


#12

But you’re not really breaking any principles though. The direct descendent of a container doesn’t need to be a dumb/stateless component. My usual structure goes like:

Container Component -> Smart/Class Component (handles Redux dispatches, lifecycle methods, etc) -> Dumb/Stateless Components

IMO Meteor method calls don’t belong in the container. The container is all about managing state and reactive data. So the next logical thing to do is to put it in the smart component:

componentWillMount() {
  mymethod.call({}, (error, result) => {
    if (!error) this.setState({ whatever: result.thing });
  });
}

And now you’ve got what you need from the method, before the component is even rendered.

The point to drive home is that the component your container is wrapping doesn’t have to be a stateless component. People far smarter than I have said this :slight_smile: (Dan Abramov). If it makes sense for your app to have a smart component, then go for it. Just take any opportunity possible to make your components stateless… and if you can’t, you can’t.


#13

Agree with everything @ffxsam, holding down the fort here.

Methods in the container === Unpredictable === good luck have fun


#14

@ffxsam that’s in fact the pattern I am currently using but I still believe what I’ve earlier said.

And in that regard, @abhiaiyer I don’t see how more unpredictable method call would be than a subscription. both could error out, both could return in a long time, or timeout and rerun etc since they share the same underlying transport mechanisms.

To reiterate, I am strictly talking about using methods for retrieving initial container data.

In the meantime, I am beginning to see less value in using createContainer due to its abstraction and considering switching over to creating and wiring up container components as standard react components, those that deal only with data. That way, it feels like it would contextually hold up better than a container wrapping a container wrapping a component, with which I have no objection when the container tree represent different types of data, for example app data and state data …


#15

@serkandurusoy I am with you on that. We are all using the same pattern to a large degree because we don’t really have any other choice besides doing what I did above and having that Session or ReactVar.

That being said, I too wish I could get the data all in one place and not have just my reactive data come from the container and have my non-reactive data come from my ‘SmartComponent’. But like a lot of things, its just a matter of opinion and how things feel organized to me.

Now no one has really talked about testing here. Which is also another aspect where to me testing a Dumb or Pure Component fairs much better (at least in theory) than having a Smart Component where I need to mock out some call or something of that nature.

But seriously, appreciate everyone’s opinions and desire to discuss these things! :slight_smile: I think @Siyfion got a little more than he bargained for haha


#16

@Siyfion Hey! I just wrote a post on Medium about using decorators to fetch data from Meteor using methods. The package we use is orionsoft:reat-meteor-data.

Check it out! It deals with the problem of re-fetching data after the decorated component has changed its props.

Hope it helps

Best regards.


#17

react-komposer (https://github.com/kadirahq/react-komposer) is similar to meteor’s createContainer (also with tracker-support), but you pass data with a callback (onData) there, instead of returning the data. So it should be easy to use a method with that. Just call onData when your data arrives.