Context: My current Meteor-React project is a teaching app where a teacher can observe remotely what the learner is doing. There are many different views that the learner can use, so I need to separate the data-sharing aspect from the views themselves. The same views will be displayed on the teacher's device, with the display controlled by the student's actions.
Questions: * Is the technique I am using sound? * How do I prevent a component from being re-rendered when its input has not changed?
Details: I have created a bare-bones prototype (see below). This uses a Source instance (which in the app itself will be updated through a MongoDB collection) to provide reactive data for a view component. In my prototype, I simply generate random data.
I have had two surprises.
One: I discover that if I call .get()
on a ReactiveVar in the source, this is enough to trigger the Tracker object to read in new values even if I return the value of a completely non-reactive variable. As can be expected, if the value of the ReactiveVar does not change, then the Tracker ignores any changes to the non-reactive variable.
Two: The value obtained by the Tracker is forwarded to the components
props` causing a re-render even if the value is unchanged.
Code:
import React, { Component } from 'react'
import { ReactiveVar } from 'meteor/reactive-var'
import { withTracker } from 'meteor/react-meteor-data'
/// SOURCE ——————————————————————————————————————————————————————————
class Source {
constructor() {
this.updateData = this.updateData.bind(this)
this.updateData()
}
updateData() {
const reactive = Math.floor(Math.random() * 1.25) // 4 times as many 0s as 1s
data.set(reactive)
console.log("reactive:", reactive)
this.usable = ["a", "b", "c"][Math.floor(Math.random() * 3)]
console.log("usable: ", this.usable)
setTimeout(this.updateData, 1000)
}
get() {
data.get() // We MUST get a reactive value to trigger Tracker...
return this.usable // ... but we CAN return a non-reactive value
}
}
let data = new ReactiveVar(0)
const source = new Source()
/// COMPONENT ———————————————————————————————————————————————————————
class Test extends Component{
render() {
console.log("rendered:", this.props.data)
return (
<div>
{this.props.data}
</div>
)
}
}
export default withTracker(() => {
const data = source.get()
console.log("UPDATE: ", data)
console.log("")
const props = {
data
}
return props
})(Test)
Sample Console output, with annotations:
reactive: 1
usable: b
UPDATE: b
rendered: b <<< initial value rendered
reactive: 1 <<< no change to reactive value...
usable: a <<< ...so usable value is ignored
reactive: 0 <<< reactive value changes...
usable: c <<< ... so an update is sent to the component
UPDATE: c
rendered: c <<< c rendered
reactive: 0 <<< no change to the reactive value...
usable: c
reactive: 0
usable: b
reactive: 0
usable: c
reactive: 0
usable: b
reactive: 1 <<< but when reactive value changes
usable: c <<< the usable value does not
UPDATE: c
rendered: c <<< c re-rendered, although unchanged
To recap: My plan is to increment a ReactiveVar in my Source instance each time a new datum arrives from the student. However, if the student is simply moving the cursor, then I want only the component that displays the student's cursor to re-render, and not the entire view.
I would appreciate any insights into how I can achieve this elegantly.
The behaviour you are seeing is part of the Meteor 'magic' - it sees that your reactive variable depends on a plain variable, and makes that reactive too, or more correctly it sets up a watch on it.
withTracker will often trigger multiple renders, so the best way to optimise these is use of React.memo()
I don't personally like React.memo, it feels clumsy, and makes the developer do work that feels unnecessary. There is a good article here which explains it:
https://dmitripavlutin.com/use-react-memo-wisely/