Fetching data once (not-reactive) observing changes afterwards (reactive) in React


#1

Hello,

I am trying to implement a React component which would load data for given Id once it is created or the Id changes but also allow reactive observing of the data changes. I also would like to prevent re-rendering of the HTML.

What I came up with is:

class MindMapContainer extends Component {
	constructor(props) {
		super(props);
		this.state = {
                    data: [],
		};
		this.dataByIdCursor = () => Data.find({matchingId: this.props.currentId});
		this.initializing = true;
	}

	fetchDataAndUpdateState() {
		this.setState((state, props) => {
			return {
				data: this.dataByIdCursor().fetch(),
			};
		});
		this.initializing = false;
	}

	startObservingDataChanges() {
		const self = this;
		this.tracker = Tracker.autorun(() => {
			this.dataByIdCursor().observeChanges({
				added(id, fields) {
					if (!self.initializing) {
						console.log('added', id, fields);
					}
				},
				changed(id, fields) {
					if (!self.initializing) {
						console.log('changed', id, fields);
					}
				},
				...
			});
		});
	}

	componentDidMount() {
		Meteor.subscribe('data', () => {
			this.fetchDataAndUpdateState();
			this.startObservingDataChanges();
		});
	}

	shouldComponentUpdate(nextProps, nextState) {
		if (this.props.currentId !== nextProps.currentId ) {
			this.initializing = true;
			this.fetchDataAndUpdateState();
			this.startObservingDataChanges();
			return false;
		}
	}

   ...

The problem is, that as the data is fetched with this.dataByIdCursor89.fetch() the observeChanges callback is being triggered as well. Therefore I introduced the this. initializing value, but cursor.fetch() is probably asynchronous. I am wondering how to fix it.

Shall I fetch the data on the server and use Meteor.method ? Is there a way to wrap the fetch() method in a Promise ? I actually would like to do something like fetchDataAndUpdateState().then(observeChanges)

I would be very happy for any hint, thank you!


#2

Can you explain a little more about what you’re trying to do, and why you think fetching data once + observe changes is the solution?

I’m struggling to understand how this is different from a regular reactive component?
If you’re observing changes and updating state, aren’t you just manually doing what withTracker gives you by default?


#3

I fetch the data and compute a fairly complex layout and visualize/animate it afterwards. This may take some time but it’s ok if done once at the beginning. Then the data starts to change - step by step, only atomic changes. An item is added/remove or changed. I can update the visualization respectively fast without re-computing the whole layout. I also do not want to transfer all the data to the client only when only one item changes.

If I call fetchDataAndUpdateState from componentDidMount than this.initializing = false; is called after the state is updated. If I call it from shouldComponentUpdateit is the other way around.

I changed the code to

Meteor.subscribe('data', () => {
			this.fetchDataAndUpdateState();
			this.startObservingDataChanges();
		});
		this.initializing = false;

and now I do not get all the added-callbacks when the data loads. The problem so far is still, that dataByIdCursor.observeChanges is not firing after the props changed


#4

Meteor.subscribe has an onReady() callback and it also returns a handle object with a ready() method which basically means an initial snapshot of the data is sent to the client and updates will then come in the form of added/changed/removed.

You can use this information to set your client state to whether you’ve loaded the initial data yet, use that to decide what you want to do with the ui.

But this is essentially how withTracker leverages this same mechanism in its original example.

Or am I looking at this sideways and missing something?

PS: I’m not on my computer so I might indeed have not paid sufficient attention


#5

Ok, I figured it out. Basically my code fro above works just fine. I fetch the data and initiate the cursor observation in In Reacts componentDidMount lifecycle-method and in shouldComponentUpdate method. I set the initializing variable to true first and set it to false afterwards. This way I do not get notified about added data upon fetching.

The issue was, that the cursors parameter currentId is passed via props to my component. And in the shouldComponentUpdate method the component instance props are still the old props So I have to pass the next_props to the cursor.

I solved it like this:

class MindMapContainer extends Component {
	constructor(props) {
		super(props);
		this.state = {
                    data: [],
		};
		this.dataByIdCursor = (dataId) => Data.find({matchingId: dataId || this.props.currentId});
		this.initializing = true;
	}

	fetchDataAndUpdateState(dataId) {
		this.setState((state, props) => {
			return {
				data: this.dataByIdCursor(dataId).fetch(),
			};
		});
		this.initializing = false;
	}

	startObservingDataChanges(dataId) {
		const self = this;
		this.tracker = Tracker.autorun(() => {
			this.dataByIdCursor().observeChanges({
				added(id, fields) {
					if (!self.initializing) {
						console.log('added', id, fields);
					}
				},
				changed(id, fields) {
					if (!self.initializing) {
						console.log('changed', id, fields);
					}
				},
				...
			});
		});
	}

	componentDidMount() {
		Meteor.subscribe('data', () => {
			this.fetchDataAndUpdateState();
			this.startObservingDataChanges();
		});
	}

	shouldComponentUpdate(nextProps, nextState) {
		if (this.props.currentId !== nextProps.currentId ) {
			this.initializing = true;
			this.fetchDataAndUpdateState(nextProps.currentId);
			this.startObservingDataChanges(nextProps.currentId);
			return false;
		}
	}

   ...

Now, I see the data being loaded once the component is loaded or the currentId changes and I can also track atomic data changes.


#6

Ok I found an additional bug. It is not enough to re-create the data-observation, It is also necessary to stop the previous tracker if it exists.

startObservingNodeChanges(mapId) {
		if (this.tracker) {this.tracker.stop()}
...