Yanick Rochon Yanick Rochon - 3 months ago 14
Javascript Question

React does not render recursive reactive components

Given this component :

import React, { Component } from 'react';
import TrackerReact from 'meteor/ultimatejs:tracker-react';

export default class SubscriptionView extends TrackerReact(Component) {

constructor(props) {
super(props);

let params = props.params || [];

if (!Array.isArray(params)) {
params = [params];
}

this.state = {
subscription: {
collection: Meteor.subscribe(props.subscription, ...params)
}
};
}

componentWillUnmount() {
this.state.subscription.collection.stop();
}


render() {
let loaded = this.state.subscription.collection.ready();

if (!loaded) {
return (
<section className="subscription-view">
<h3>Loading...</h3>
</section>
);
}

return (
<section className="subscription-view">
{ this.props.children }
</section>
);
}
};


And another component :

import SubscriptionView from './SubscriptionView.jsx';

export const Foo = () => (
<SubscriptionView subscription="allFoo">
<SubscriptionView subscription="singleBar" params={ 123 }>
<div>Rendered!</div>
</SubscriptionView>
</SubscriptionView>
);


The first
Subscription
is re-rendered when the data is available, however the second one is rendered only once and nothing more. If I place a
console.log(this.props.subscription, ready);
inside the render function of
SubscriptionView
, I see

allFoo false
allFoo true
singleBar false


and that's it.

On the server side, both publish methods are

Meteor.publish('allFoo', function () {
console.log("Subscribing foos");
return Foos.find();
});

Meteor.publish('singleBar', function (id) {
console.log("Subscribing bar", id);
return Bars.find({ _id: id });
});


Both of the publish methods are being called.

Why isn't the second
SubscriptionView
reactive?




* Solution *



This is based on alexi2's comment :

import React, { Component } from 'react';
import TrackerReact from 'meteor/ultimatejs:tracker-react';

export default class SubscriptionLoader extends TrackerReact(Component) {

constructor(props) {
super(props);

let params = props.params || [];

if (!Array.isArray(params)) {
params = [params];
}

this.state = {
done: false,
subscription: {
collection: Meteor.subscribe(props.subscription, ...params)
}
};
}

componentWillUnmount() {
this.state.subscription.collection.stop();
}

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

this.props.onReady && this.props.onReady();
}
}


render() {
let loaded = this.state.subscription.collection.ready();

if (!loaded) {
return (
<div>Loading...</div>
);
}

return null;
}
};


Then, inside the parent component's
render
method :

<section className="inventory-item-view">
<SubscriptionLoader subscription='singleBar' params={ this.props.id } onReady={ this.setReady.bind(this, 'barReady') } />
<SubscriptionLoader subscription='allFoos' onReady={ this.setReady.bind(this, 'foosReady') } />
{ content }
</section>


Where
setReady
merely sets the component's state, and
content
has a value only if
this.state.barReady && this.state.foosReady
is true.

It works!

Answer

Try separating out your SubscriptionView Components like this:

import SubscriptionView from './SubscriptionView.jsx';

export const Foo = () => (
    <div>
      <SubscriptionView subscription="singleBar" params={ 123 }>
        <div>Rendered!</div>
      </SubscriptionView>
      <SubscriptionView subscription="allFoo">
        <div>Rendered Again!</div>
      </SubscriptionView>
    </div>
);

Edit from comments conversation

Not sure if I am on the right track but you could build Foo as a 'smart' component that passes props to each SubscriptionView as required, and then use Foo as a reusable component.

Let's say that what I need to render is FooBarForm, which requires both Foos and Bars to be registered, in that specific use case. How would you do that?

You could create Components Foos and Bars that took props as required and create a parent component FooBarForm that contained those Components and passed the necessary data.

FooBarForm would handle the state and as that changed pass it to the props of its child components.

Now state is being centrally managed by the parent component, and the child components render using props passed from the parent.

The child components would re-render as their props changed depending on whether the state being passed from the parent component had changed.

Comments