React Reactive Render

I couldn’t shake the feeling that the current withTracker solution was redundant, so I’ve been fiddling away at this today.

It’s a replacement for withTracker that essentially makes the render function of your react component the tracker’s autorun function.

https://atmospherejs.com/cereal/reactive-render

For example:

import React, { Component } from 'react';
import { ReactiveVar } from 'meteor/reactive-var';
import { autorun } from 'meteor/cereal:reactive-render';

const Count = new ReactiveVar(0);

@autorun
export default class Hello extends Component {
  increment() {
    Count.set(Count.get() + 1);
  }

  render() {
    return (
      <div>
        <button onClick={this.increment}>Click Me</button>
        <p>You've pressed the button {Count.get()} times.</p>
      </div>
    );
  }
}

I wrote it for a project I’m working on right now and figured other people could use it.

Edit: Added a hook for functional components:

import React, { Component } from 'react';
import { ReactiveVar } from 'meteor/reactive-var';
import { makeTracker } from 'meteor/cereal:reactive-render';

const Count = new ReactiveVar(0);

const useTracker = makeTracker(() => {
  return {
    count: Count.get()
  }
})

export default () =>{
  const { count } = useTracker();
  return (
    <div>
      <button onClick={() => Count.set(count + 1))}>Click Me</button>
      <p>You've pressed the button {count} times.</p>
    </div>
  );
}
6 Likes

I don’t get it. Why do you need withTracker/autorun to do this kind of react stuffs?

I think he’s showing that it works with that example. I like the idea of doing everything in render. Using the meteor hook does something similar by default, where it always runs when react renders, and also forces re-render if something changes reactively. With deps it only reruns if the reactive source changes (or if deps change - the React folks use fancy sounding words like “memoize” to describe this).

Pretty neat really. I have similar but more complicated ideas about how to set up use in render methods type stuff, and this has me thinking of ways to do it.

2 Likes

A more appropriate example would be

import React, { Component } from 'react';
import { Person } from '/imports/api/Person'; 
import { autorun } from 'meteor/cereal:reactive-render';

@autorun
export default class Hello extends Component {
  render() {
    const person = Person.findOne({});
    return (
      <div>
        <p>{person.name}</p>
      </div>
    );
  }
}

Without letting meteor track the the records, the component wouldn’t automatically update when the record changes.

1 Like

Getting it to work as a hook eluded me. I think you might have to do something like material-ui’s makeStyles method, that creates a hook for you. I haven’t really looked at it though.

Something like

const useTracker = makeTracker(() => {
  return {
    person: Person.findOne({})
  }
});

export default () => {
  const { person } = useTracker();
  return(
    <div>{person.name}</div>
  );
};

Yeah that worked exactly how I thought it would.

I added a hook to the package. You can use it like this:

import React, { Component } from 'react';
import { ReactiveVar } from 'meteor/reactive-var';
import { makeTracker } from 'meteor/cereal:reactive-render';

const Count = new ReactiveVar(0);

const useTracker = makeTracker(() => {
  return {
    count: Count.get()
  }
})

export default () =>{
  const { count } = useTracker();
  return (
    <div>
      <button onClick={() => Count.set(count + 1))}>Click Me</button>
      <p>You've pressed the button {count} times.</p>
    </div>
  );
}

A few of us have implemented a hook in this PR (from this fork). You just use it like a normal hook (with an interface that mimics useEffect or useMemo in its use of a deps param) :

export const MyComponent = ({ personId }) => {
  const count = useTracker(() => Count.get())

  const [person, isLoading] = useTracker(() => {
    const subscription = Meteor.subscribe('person', personId)
    const person = Persons.findOne({ _id: personId })
    return [person, !subscription.ready()]
  }, [personId])
  
    return <div>
      <button onClick={() => Count.set(count + 1))}>Click Me</button>
      <p>{person.name} tapped the button {count} times.</p>
    </div>
}

You can even carve it up more granularly, or combine everything into one tracker - however you like is fine.

export const MyComponent = ({ personId }) => {
  const count = useTracker(() => Count.get(), [])

  const isLoading = useTracker(() => {
    const subscription = Meteor.subscribe('person', personId)
    return !subscription.ready()
  }, [personId])

  const person = useTracker(() => Persons.findOne({ _id: personId }), [personId])
  
  return <div>
    <button onClick={() => Count.set(count + 1))}>Click Me</button>
    <p>{person.name} tapped the button {count} times.</p>
  </div>
}

This is not as integrated feeling as your solution, which allows you to simply use reactive meteor sources in the entire render function. I like that approach, and I’m now thinking about how to make that work for various sources without requiring the hook at all, or at least without tracker. We could create a distinct hook for each API - for example, a new set of search hooks could be used. Instead of MyCollection.find we could have a MyCollection.useFind which would set up reactivity using the observeChanges API directly, instead of using Tracker (I’m thinking we can gain some render efficiency doing this, but I’m not sure yet). Similarly, a subscription hook could manage the subscription without even needing to use Tracker (or could use a simple tracker implementation based on useEffect which would simplify many of the stickier implementation details in our useTracker hook…).

This is a great innovation and it seems like it should be integrated into the official Meteor-React integration!

2 Likes

I made a couple of improvements including stopping the subscription when the component unmounts, and fixed a bug that caused the tracker to duplicate itself every render :sweat_smile:

1 Like