Providing a source of reactive data

134 views Asked by At

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 componentsprops` 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.

1

There are 1 answers

2
Mikkel On

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/

When a component is wrapped in React.memo(), React renders the component and memoizes the result. Before the next render, if the new props are the same, React reuses the memoized result skipping the next rendering.